PageRenderTime 41ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 1ms

/libraries/classes/Display/Results.php

http://github.com/phpmyadmin/phpmyadmin
PHP | 4765 lines | 2967 code | 549 blank | 1249 comment | 489 complexity | b31e4de92fd72b5b613e9f896aa4caf0 MD5 | raw file
Possible License(s): GPL-2.0, MIT, LGPL-3.0
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin\Display;
  4. use PhpMyAdmin\Config\SpecialSchemaLinks;
  5. use PhpMyAdmin\Core;
  6. use PhpMyAdmin\DatabaseInterface;
  7. use PhpMyAdmin\FieldMetadata;
  8. use PhpMyAdmin\Html\Generator;
  9. use PhpMyAdmin\Index;
  10. use PhpMyAdmin\Message;
  11. use PhpMyAdmin\Plugins\Transformations\Output\Text_Octetstream_Sql;
  12. use PhpMyAdmin\Plugins\Transformations\Output\Text_Plain_Json;
  13. use PhpMyAdmin\Plugins\Transformations\Output\Text_Plain_Sql;
  14. use PhpMyAdmin\Plugins\Transformations\Text_Plain_Link;
  15. use PhpMyAdmin\Plugins\TransformationsPlugin;
  16. use PhpMyAdmin\Relation;
  17. use PhpMyAdmin\ResponseRenderer;
  18. use PhpMyAdmin\Sanitize;
  19. use PhpMyAdmin\Sql;
  20. use PhpMyAdmin\SqlParser\Parser;
  21. use PhpMyAdmin\SqlParser\Statements\SelectStatement;
  22. use PhpMyAdmin\SqlParser\Utils\Query;
  23. use PhpMyAdmin\Table;
  24. use PhpMyAdmin\Template;
  25. use PhpMyAdmin\Theme;
  26. use PhpMyAdmin\Transformations;
  27. use PhpMyAdmin\Url;
  28. use PhpMyAdmin\Util;
  29. use PhpMyAdmin\Utils\Gis;
  30. use function __;
  31. use function _pgettext;
  32. use function array_filter;
  33. use function array_keys;
  34. use function array_merge;
  35. use function array_shift;
  36. use function bin2hex;
  37. use function ceil;
  38. use function class_exists;
  39. use function count;
  40. use function explode;
  41. use function file_exists;
  42. use function floor;
  43. use function htmlspecialchars;
  44. use function implode;
  45. use function in_array;
  46. use function intval;
  47. use function is_array;
  48. use function is_numeric;
  49. use function json_encode;
  50. use function mb_check_encoding;
  51. use function mb_strlen;
  52. use function mb_strpos;
  53. use function mb_strtolower;
  54. use function mb_strtoupper;
  55. use function mb_substr;
  56. use function md5;
  57. use function mt_getrandmax;
  58. use function pack;
  59. use function preg_match;
  60. use function preg_replace;
  61. use function random_int;
  62. use function str_contains;
  63. use function str_replace;
  64. use function strcasecmp;
  65. use function strip_tags;
  66. use function stripos;
  67. use function strlen;
  68. use function strpos;
  69. use function strtoupper;
  70. use function substr;
  71. use function trim;
  72. /**
  73. * Handle all the functionalities related to displaying results
  74. * of sql queries, stored procedure, browsing sql processes or
  75. * displaying binary log.
  76. */
  77. class Results
  78. {
  79. // Define constants
  80. public const NO_EDIT_OR_DELETE = 'nn';
  81. public const UPDATE_ROW = 'ur';
  82. public const DELETE_ROW = 'dr';
  83. public const KILL_PROCESS = 'kp';
  84. public const POSITION_LEFT = 'left';
  85. public const POSITION_RIGHT = 'right';
  86. public const POSITION_BOTH = 'both';
  87. public const POSITION_NONE = 'none';
  88. public const DISPLAY_FULL_TEXT = 'F';
  89. public const DISPLAY_PARTIAL_TEXT = 'P';
  90. public const HEADER_FLIP_TYPE_AUTO = 'auto';
  91. public const HEADER_FLIP_TYPE_CSS = 'css';
  92. public const HEADER_FLIP_TYPE_FAKE = 'fake';
  93. public const RELATIONAL_KEY = 'K';
  94. public const RELATIONAL_DISPLAY_COLUMN = 'D';
  95. public const GEOMETRY_DISP_GEOM = 'GEOM';
  96. public const GEOMETRY_DISP_WKT = 'WKT';
  97. public const GEOMETRY_DISP_WKB = 'WKB';
  98. public const SMART_SORT_ORDER = 'SMART';
  99. public const ASCENDING_SORT_DIR = 'ASC';
  100. public const DESCENDING_SORT_DIR = 'DESC';
  101. public const TABLE_TYPE_INNO_DB = 'InnoDB';
  102. public const ALL_ROWS = 'all';
  103. public const QUERY_TYPE_SELECT = 'SELECT';
  104. public const ROUTINE_PROCEDURE = 'procedure';
  105. public const ROUTINE_FUNCTION = 'function';
  106. public const ACTION_LINK_CONTENT_ICONS = 'icons';
  107. public const ACTION_LINK_CONTENT_TEXT = 'text';
  108. /**
  109. * @psalm-var array{
  110. * server: int,
  111. * db: string,
  112. * table: string,
  113. * goto: string,
  114. * sql_query: string,
  115. * unlim_num_rows: int|numeric-string,
  116. * fields_meta: FieldMetadata[],
  117. * is_count: bool|null,
  118. * is_export: bool|null,
  119. * is_func: bool|null,
  120. * is_analyse: bool|null,
  121. * num_rows: int|numeric-string,
  122. * fields_cnt: int,
  123. * querytime: float|null,
  124. * text_dir: string|null,
  125. * is_maint: bool|null,
  126. * is_explain: bool|null,
  127. * is_show: bool|null,
  128. * is_browse_distinct: bool|null,
  129. * showtable: array<string, mixed>|null,
  130. * printview: string|null,
  131. * highlight_columns: array|null,
  132. * display_params: array|null,
  133. * mime_map: array|null,
  134. * editable: bool|null,
  135. * unique_id: int,
  136. * whereClauseMap: array,
  137. * }
  138. */
  139. public $properties = [
  140. /* server id */
  141. 'server' => 0,
  142. /* Database name */
  143. 'db' => '',
  144. /* Table name */
  145. 'table' => '',
  146. /* the URL to go back in case of errors */
  147. 'goto' => '',
  148. /* the SQL query */
  149. 'sql_query' => '',
  150. /* the total number of rows returned by the SQL query without any appended "LIMIT" clause programmatically */
  151. 'unlim_num_rows' => 0,
  152. /* meta information about fields */
  153. 'fields_meta' => [],
  154. 'is_count' => null,
  155. 'is_export' => null,
  156. 'is_func' => null,
  157. 'is_analyse' => null,
  158. /* the total number of rows returned by the SQL query */
  159. 'num_rows' => 0,
  160. /* the total number of fields returned by the SQL query */
  161. 'fields_cnt' => 0,
  162. /* time taken for execute the SQL query */
  163. 'querytime' => null,
  164. 'text_dir' => null,
  165. 'is_maint' => null,
  166. 'is_explain' => null,
  167. 'is_show' => null,
  168. 'is_browse_distinct' => null,
  169. /* table definitions */
  170. 'showtable' => null,
  171. 'printview' => null,
  172. /* column names to highlight */
  173. 'highlight_columns' => null,
  174. /* display information */
  175. 'display_params' => null,
  176. /* mime types information of fields */
  177. 'mime_map' => null,
  178. 'editable' => null,
  179. /* random unique ID to distinguish result set */
  180. 'unique_id' => 0,
  181. /* where clauses for each row, each table in the row */
  182. 'whereClauseMap' => [],
  183. ];
  184. /**
  185. * This variable contains the column transformation information
  186. * for some of the system databases.
  187. * One element of this array represent all relevant columns in all tables in
  188. * one specific database
  189. *
  190. * @var array<string, array<string, array<string, string[]>>>
  191. * @psalm-var array<string, array<string, array<string, array{string, class-string, string}>>> $transformationInfo
  192. */
  193. public $transformationInfo;
  194. /** @var Relation */
  195. private $relation;
  196. /** @var Transformations */
  197. private $transformations;
  198. /** @var Template */
  199. public $template;
  200. /**
  201. * @param string $db the database name
  202. * @param string $table the table name
  203. * @param int $server the server id
  204. * @param string $goto the URL to go back in case of errors
  205. * @param string $sqlQuery the SQL query
  206. *
  207. * @access public
  208. */
  209. public function __construct($db, $table, $server, $goto, $sqlQuery)
  210. {
  211. global $dbi;
  212. $this->relation = new Relation($dbi);
  213. $this->transformations = new Transformations();
  214. $this->template = new Template();
  215. $this->setDefaultTransformations();
  216. $this->properties['db'] = $db;
  217. $this->properties['table'] = $table;
  218. $this->properties['server'] = $server;
  219. $this->properties['goto'] = $goto;
  220. $this->properties['sql_query'] = $sqlQuery;
  221. $this->properties['unique_id'] = random_int(0, mt_getrandmax());
  222. }
  223. /**
  224. * Sets default transformations for some columns
  225. */
  226. private function setDefaultTransformations(): void
  227. {
  228. $jsonHighlightingData = [
  229. 'libraries/classes/Plugins/Transformations/Output/Text_Plain_Json.php',
  230. Text_Plain_Json::class,
  231. 'Text_Plain',
  232. ];
  233. $sqlHighlightingData = [
  234. 'libraries/classes/Plugins/Transformations/Output/Text_Plain_Sql.php',
  235. Text_Plain_Sql::class,
  236. 'Text_Plain',
  237. ];
  238. $blobSqlHighlightingData = [
  239. 'libraries/classes/Plugins/Transformations/Output/Text_Octetstream_Sql.php',
  240. Text_Octetstream_Sql::class,
  241. 'Text_Octetstream',
  242. ];
  243. $linkData = [
  244. 'libraries/classes/Plugins/Transformations/Text_Plain_Link.php',
  245. Text_Plain_Link::class,
  246. 'Text_Plain',
  247. ];
  248. $this->transformationInfo = [
  249. 'information_schema' => [
  250. 'events' => ['event_definition' => $sqlHighlightingData],
  251. 'processlist' => ['info' => $sqlHighlightingData],
  252. 'routines' => ['routine_definition' => $sqlHighlightingData],
  253. 'triggers' => ['action_statement' => $sqlHighlightingData],
  254. 'views' => ['view_definition' => $sqlHighlightingData],
  255. ],
  256. 'mysql' => [
  257. 'event' => [
  258. 'body' => $blobSqlHighlightingData,
  259. 'body_utf8' => $blobSqlHighlightingData,
  260. ],
  261. 'general_log' => ['argument' => $sqlHighlightingData],
  262. 'help_category' => ['url' => $linkData],
  263. 'help_topic' => [
  264. 'example' => $sqlHighlightingData,
  265. 'url' => $linkData,
  266. ],
  267. 'proc' => [
  268. 'param_list' => $blobSqlHighlightingData,
  269. 'returns' => $blobSqlHighlightingData,
  270. 'body' => $blobSqlHighlightingData,
  271. 'body_utf8' => $blobSqlHighlightingData,
  272. ],
  273. 'slow_log' => ['sql_text' => $sqlHighlightingData],
  274. ],
  275. ];
  276. $cfgRelation = $this->relation->getRelationsParam();
  277. if (! $cfgRelation['db']) {
  278. return;
  279. }
  280. $relDb = [];
  281. if (! empty($cfgRelation['history'])) {
  282. $relDb[$cfgRelation['history']] = ['sqlquery' => $sqlHighlightingData];
  283. }
  284. if (! empty($cfgRelation['bookmark'])) {
  285. $relDb[$cfgRelation['bookmark']] = ['query' => $sqlHighlightingData];
  286. }
  287. if (! empty($cfgRelation['tracking'])) {
  288. $relDb[$cfgRelation['tracking']] = [
  289. 'schema_sql' => $sqlHighlightingData,
  290. 'data_sql' => $sqlHighlightingData,
  291. ];
  292. }
  293. if (! empty($cfgRelation['favorite'])) {
  294. $relDb[$cfgRelation['favorite']] = ['tables' => $jsonHighlightingData];
  295. }
  296. if (! empty($cfgRelation['recent'])) {
  297. $relDb[$cfgRelation['recent']] = ['tables' => $jsonHighlightingData];
  298. }
  299. if (! empty($cfgRelation['savedsearches'])) {
  300. $relDb[$cfgRelation['savedsearches']] = ['search_data' => $jsonHighlightingData];
  301. }
  302. if (! empty($cfgRelation['designer_settings'])) {
  303. $relDb[$cfgRelation['designer_settings']] = ['settings_data' => $jsonHighlightingData];
  304. }
  305. if (! empty($cfgRelation['table_uiprefs'])) {
  306. $relDb[$cfgRelation['table_uiprefs']] = ['prefs' => $jsonHighlightingData];
  307. }
  308. if (! empty($cfgRelation['userconfig'])) {
  309. $relDb[$cfgRelation['userconfig']] = ['config_data' => $jsonHighlightingData];
  310. }
  311. if (! empty($cfgRelation['export_templates'])) {
  312. $relDb[$cfgRelation['export_templates']] = ['template_data' => $jsonHighlightingData];
  313. }
  314. $this->transformationInfo[$cfgRelation['db']] = $relDb;
  315. }
  316. /**
  317. * Set properties which were not initialized at the constructor
  318. *
  319. * @param int|string $unlimNumRows the total number of rows returned by the SQL query without
  320. * any appended "LIMIT" clause programmatically
  321. * @param FieldMetadata[] $fieldsMeta meta information about fields
  322. * @param bool $isCount statement is SELECT COUNT
  323. * @param bool $isExport statement contains INTO OUTFILE
  324. * @param bool $isFunction statement contains a function like SUM()
  325. * @param bool $isAnalyse statement contains PROCEDURE ANALYSE
  326. * @param int|string $numRows total no. of rows returned by SQL query
  327. * @param int $fieldsCount total no.of fields returned by SQL query
  328. * @param double $queryTime time taken for execute the SQL query
  329. * @param string $textDirection text direction
  330. * @param bool $isMaintenance statement contains a maintenance command
  331. * @param bool $isExplain statement contains EXPLAIN
  332. * @param bool $isShow statement contains SHOW
  333. * @param array<string, mixed>|null $showTable table definitions
  334. * @param string|null $printView print view was requested
  335. * @param bool $editable whether the results set is editable
  336. * @param bool $isBrowseDistinct whether browsing distinct values
  337. * @psalm-param int|numeric-string $unlimNumRows
  338. * @psalm-param int|numeric-string $numRows
  339. */
  340. public function setProperties(
  341. $unlimNumRows,
  342. array $fieldsMeta,
  343. $isCount,
  344. $isExport,
  345. $isFunction,
  346. $isAnalyse,
  347. $numRows,
  348. $fieldsCount,
  349. $queryTime,
  350. $textDirection,
  351. $isMaintenance,
  352. $isExplain,
  353. $isShow,
  354. ?array $showTable,
  355. $printView,
  356. $editable,
  357. $isBrowseDistinct
  358. ): void {
  359. $this->properties['unlim_num_rows'] = $unlimNumRows;
  360. $this->properties['fields_meta'] = $fieldsMeta;
  361. $this->properties['is_count'] = $isCount;
  362. $this->properties['is_export'] = $isExport;
  363. $this->properties['is_func'] = $isFunction;
  364. $this->properties['is_analyse'] = $isAnalyse;
  365. $this->properties['num_rows'] = $numRows;
  366. $this->properties['fields_cnt'] = $fieldsCount;
  367. $this->properties['querytime'] = $queryTime;
  368. $this->properties['text_dir'] = $textDirection;
  369. $this->properties['is_maint'] = $isMaintenance;
  370. $this->properties['is_explain'] = $isExplain;
  371. $this->properties['is_show'] = $isShow;
  372. $this->properties['showtable'] = $showTable;
  373. $this->properties['printview'] = $printView;
  374. $this->properties['editable'] = $editable;
  375. $this->properties['is_browse_distinct'] = $isBrowseDistinct;
  376. }
  377. /**
  378. * Defines the parts to display for a print view
  379. *
  380. * @param array $displayParts the parts to display
  381. *
  382. * @return array the modified display parts
  383. *
  384. * @access private
  385. */
  386. private function setDisplayPartsForPrintView(array $displayParts)
  387. {
  388. // set all elements to false!
  389. $displayParts['edit_lnk'] = self::NO_EDIT_OR_DELETE; // no edit link
  390. $displayParts['del_lnk'] = self::NO_EDIT_OR_DELETE; // no delete link
  391. $displayParts['sort_lnk'] = '0';
  392. $displayParts['nav_bar'] = '0';
  393. $displayParts['bkm_form'] = '0';
  394. $displayParts['text_btn'] = '0';
  395. $displayParts['pview_lnk'] = '0';
  396. return $displayParts;
  397. }
  398. /**
  399. * Defines the parts to display for a SHOW statement
  400. *
  401. * @param array $displayParts the parts to display
  402. *
  403. * @return array the modified display parts
  404. *
  405. * @access private
  406. */
  407. private function setDisplayPartsForShow(array $displayParts)
  408. {
  409. preg_match(
  410. '@^SHOW[[:space:]]+(VARIABLES|(FULL[[:space:]]+)?'
  411. . 'PROCESSLIST|STATUS|TABLE|GRANTS|CREATE|LOGS|DATABASES|FIELDS'
  412. . ')@i',
  413. $this->properties['sql_query'],
  414. $which
  415. );
  416. $bIsProcessList = isset($which[1]);
  417. if ($bIsProcessList) {
  418. $str = ' ' . strtoupper($which[1]);
  419. $bIsProcessList = $bIsProcessList
  420. && strpos($str, 'PROCESSLIST') > 0;
  421. }
  422. if ($bIsProcessList) {
  423. // no edit link
  424. $displayParts['edit_lnk'] = self::NO_EDIT_OR_DELETE;
  425. // "kill process" type edit link
  426. $displayParts['del_lnk'] = self::KILL_PROCESS;
  427. } else {
  428. // Default case -> no links
  429. // no edit link
  430. $displayParts['edit_lnk'] = self::NO_EDIT_OR_DELETE;
  431. // no delete link
  432. $displayParts['del_lnk'] = self::NO_EDIT_OR_DELETE;
  433. }
  434. // Other settings
  435. $displayParts['sort_lnk'] = '0';
  436. $displayParts['nav_bar'] = '0';
  437. $displayParts['bkm_form'] = '1';
  438. $displayParts['text_btn'] = '1';
  439. $displayParts['pview_lnk'] = '1';
  440. return $displayParts;
  441. }
  442. /**
  443. * Defines the parts to display for statements not related to data
  444. *
  445. * @param array $displayParts the parts to display
  446. *
  447. * @return array the modified display parts
  448. *
  449. * @access private
  450. */
  451. private function setDisplayPartsForNonData(array $displayParts)
  452. {
  453. // Statement is a "SELECT COUNT", a
  454. // "CHECK/ANALYZE/REPAIR/OPTIMIZE/CHECKSUM", an "EXPLAIN" one or
  455. // contains a "PROC ANALYSE" part
  456. $displayParts['edit_lnk'] = self::NO_EDIT_OR_DELETE; // no edit link
  457. $displayParts['del_lnk'] = self::NO_EDIT_OR_DELETE; // no delete link
  458. $displayParts['sort_lnk'] = '0';
  459. $displayParts['nav_bar'] = '0';
  460. $displayParts['bkm_form'] = '1';
  461. if ($this->properties['is_maint']) {
  462. $displayParts['text_btn'] = '1';
  463. } else {
  464. $displayParts['text_btn'] = '0';
  465. }
  466. $displayParts['pview_lnk'] = '1';
  467. return $displayParts;
  468. }
  469. /**
  470. * Defines the parts to display for other statements (probably SELECT)
  471. *
  472. * @param array $displayParts the parts to display
  473. *
  474. * @return array the modified display parts
  475. *
  476. * @access private
  477. */
  478. private function setDisplayPartsForSelect(array $displayParts)
  479. {
  480. // Other statements (ie "SELECT" ones) -> updates
  481. // $displayParts['edit_lnk'], $displayParts['del_lnk'] and
  482. // $displayParts['text_btn'] (keeps other default values)
  483. /** @var FieldMetadata[] $fieldsMeta */
  484. $fieldsMeta = $this->properties['fields_meta'];
  485. $previousTable = '';
  486. $displayParts['text_btn'] = '1';
  487. $numberOfColumns = $this->properties['fields_cnt'];
  488. for ($i = 0; $i < $numberOfColumns; $i++) {
  489. $isLink = ($displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE)
  490. || ($displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE)
  491. || ($displayParts['sort_lnk'] != '0');
  492. // Displays edit/delete/sort/insert links?
  493. if (
  494. $isLink
  495. && $previousTable != ''
  496. && $fieldsMeta[$i]->table != ''
  497. && $fieldsMeta[$i]->table != $previousTable
  498. ) {
  499. // don't display links
  500. $displayParts['edit_lnk'] = self::NO_EDIT_OR_DELETE;
  501. $displayParts['del_lnk'] = self::NO_EDIT_OR_DELETE;
  502. /**
  503. * @todo May be problematic with same field names
  504. * in two joined table.
  505. */
  506. if ($displayParts['text_btn'] == '1') {
  507. break;
  508. }
  509. }
  510. // Always display print view link
  511. $displayParts['pview_lnk'] = '1';
  512. if ($fieldsMeta[$i]->table == '') {
  513. continue;
  514. }
  515. $previousTable = $fieldsMeta[$i]->table;
  516. }
  517. if ($previousTable == '') { // no table for any of the columns
  518. // don't display links
  519. $displayParts['edit_lnk'] = self::NO_EDIT_OR_DELETE;
  520. $displayParts['del_lnk'] = self::NO_EDIT_OR_DELETE;
  521. }
  522. return $displayParts;
  523. }
  524. /**
  525. * Defines the parts to display for the results of a SQL query
  526. * and the total number of rows
  527. *
  528. * @see getTable()
  529. *
  530. * @param array $displayParts the parts to display (see a few
  531. * lines above for explanations)
  532. *
  533. * @return array the first element is an array with explicit indexes
  534. * for all the display elements
  535. * the second element is the total number of rows returned
  536. * by the SQL query without any programmatically appended
  537. * LIMIT clause (just a copy of $unlim_num_rows if it exists,
  538. * else computed inside this function)
  539. *
  540. * @access private
  541. */
  542. private function setDisplayPartsAndTotal(array $displayParts)
  543. {
  544. global $dbi;
  545. $theTotal = 0;
  546. // 1. Following variables are needed for use in isset/empty or
  547. // use with array indexes or safe use in foreach
  548. $db = $this->properties['db'];
  549. $table = $this->properties['table'];
  550. $unlimNumRows = $this->properties['unlim_num_rows'];
  551. $numRows = $this->properties['num_rows'];
  552. $printView = $this->properties['printview'];
  553. // 2. Updates the display parts
  554. if ($printView == '1') {
  555. $displayParts = $this->setDisplayPartsForPrintView($displayParts);
  556. } elseif (
  557. $this->properties['is_count'] || $this->properties['is_analyse']
  558. || $this->properties['is_maint'] || $this->properties['is_explain']
  559. ) {
  560. $displayParts = $this->setDisplayPartsForNonData($displayParts);
  561. } elseif ($this->properties['is_show']) {
  562. $displayParts = $this->setDisplayPartsForShow($displayParts);
  563. } else {
  564. $displayParts = $this->setDisplayPartsForSelect($displayParts);
  565. }
  566. // 3. Gets the total number of rows if it is unknown
  567. if ($unlimNumRows > 0) {
  568. $theTotal = $unlimNumRows;
  569. } elseif (
  570. ($displayParts['nav_bar'] == '1')
  571. || ($displayParts['sort_lnk'] == '1')
  572. && (strlen($db) > 0 && strlen($table) > 0)
  573. ) {
  574. $theTotal = $dbi->getTable($db, $table)->countRecords();
  575. }
  576. // if for COUNT query, number of rows returned more than 1
  577. // (may be being used GROUP BY)
  578. if ($this->properties['is_count'] && $numRows > 1) {
  579. $displayParts['nav_bar'] = '1';
  580. $displayParts['sort_lnk'] = '1';
  581. }
  582. // 4. If navigation bar or sorting fields names URLs should be
  583. // displayed but there is only one row, change these settings to
  584. // false
  585. if ($displayParts['nav_bar'] == '1' || $displayParts['sort_lnk'] == '1') {
  586. // - Do not display sort links if less than 2 rows.
  587. // - For a VIEW we (probably) did not count the number of rows
  588. // so don't test this number here, it would remove the possibility
  589. // of sorting VIEW results.
  590. $tableObject = new Table($table, $db);
  591. if ($unlimNumRows < 2 && ! $tableObject->isView()) {
  592. $displayParts['sort_lnk'] = '0';
  593. }
  594. }
  595. return [
  596. $displayParts,
  597. $theTotal,
  598. ];
  599. }
  600. /**
  601. * Return true if we are executing a query in the form of
  602. * "SELECT * FROM <a table> ..."
  603. *
  604. * @see getTableHeaders(), getColumnParams()
  605. *
  606. * @param array $analyzedSqlResults analyzed sql results
  607. *
  608. * @access private
  609. */
  610. private function isSelect(array $analyzedSqlResults): bool
  611. {
  612. return ! ($this->properties['is_count']
  613. || $this->properties['is_export']
  614. || $this->properties['is_func']
  615. || $this->properties['is_analyse'])
  616. && ! empty($analyzedSqlResults['select_from'])
  617. && ! empty($analyzedSqlResults['statement']->from)
  618. && (count($analyzedSqlResults['statement']->from) === 1)
  619. && ! empty($analyzedSqlResults['statement']->from[0]->table);
  620. }
  621. /**
  622. * Get a navigation button
  623. *
  624. * @see getMoveBackwardButtonsForTableNavigation(),
  625. * getMoveForwardButtonsForTableNavigation()
  626. *
  627. * @param string $caption iconic caption for button
  628. * @param string $title text for button
  629. * @param int $pos position for next query
  630. * @param string $htmlSqlQuery query ready for display
  631. * @param bool $back whether 'begin' or 'previous'
  632. * @param string $onsubmit optional onsubmit clause
  633. * @param string $inputForRealEnd optional hidden field for special treatment
  634. * @param string $onclick optional onclick clause
  635. *
  636. * @return string html content
  637. *
  638. * @access private
  639. */
  640. private function getTableNavigationButton(
  641. $caption,
  642. $title,
  643. $pos,
  644. $htmlSqlQuery,
  645. $back,
  646. $onsubmit = '',
  647. $inputForRealEnd = '',
  648. $onclick = ''
  649. ) {
  650. $captionOutput = '';
  651. if ($back) {
  652. if (Util::showIcons('TableNavigationLinksMode')) {
  653. $captionOutput .= $caption;
  654. }
  655. if (Util::showText('TableNavigationLinksMode')) {
  656. $captionOutput .= '&nbsp;' . $title;
  657. }
  658. } else {
  659. if (Util::showText('TableNavigationLinksMode')) {
  660. $captionOutput .= $title;
  661. }
  662. if (Util::showIcons('TableNavigationLinksMode')) {
  663. $captionOutput .= '&nbsp;' . $caption;
  664. }
  665. }
  666. return $this->template->render('display/results/table_navigation_button', [
  667. 'db' => $this->properties['db'],
  668. 'table' => $this->properties['table'],
  669. 'sql_query' => $htmlSqlQuery,
  670. 'pos' => $pos,
  671. 'is_browse_distinct' => $this->properties['is_browse_distinct'],
  672. 'goto' => $this->properties['goto'],
  673. 'input_for_real_end' => $inputForRealEnd,
  674. 'caption_output' => $captionOutput,
  675. 'title' => $title,
  676. 'onsubmit' => $onsubmit,
  677. 'onclick' => $onclick,
  678. ]);
  679. }
  680. /**
  681. * Possibly return a page selector for table navigation
  682. *
  683. * @return array ($output, $nbTotalPage)
  684. *
  685. * @access private
  686. */
  687. private function getHtmlPageSelector(): array
  688. {
  689. $pageNow = (int) floor($_SESSION['tmpval']['pos'] / $_SESSION['tmpval']['max_rows']) + 1;
  690. $nbTotalPage = (int) ceil($this->properties['unlim_num_rows'] / $_SESSION['tmpval']['max_rows']);
  691. $output = '';
  692. if ($nbTotalPage > 1) {
  693. $urlParams = [
  694. 'db' => $this->properties['db'],
  695. 'table' => $this->properties['table'],
  696. 'sql_query' => $this->properties['sql_query'],
  697. 'goto' => $this->properties['goto'],
  698. 'is_browse_distinct' => $this->properties['is_browse_distinct'],
  699. ];
  700. $output = $this->template->render('display/results/page_selector', [
  701. 'url_params' => $urlParams,
  702. 'page_selector' => Util::pageselector(
  703. 'pos',
  704. $_SESSION['tmpval']['max_rows'],
  705. $pageNow,
  706. $nbTotalPage,
  707. 200,
  708. 5,
  709. 5,
  710. 20,
  711. 10
  712. ),
  713. ]);
  714. }
  715. return [
  716. $output,
  717. $nbTotalPage,
  718. ];
  719. }
  720. /**
  721. * Get a navigation bar to browse among the results of a SQL query
  722. *
  723. * @see getTable()
  724. *
  725. * @param int $posNext the offset for the "next" page
  726. * @param int $posPrevious the offset for the "previous" page
  727. * @param bool $isInnodb whether its InnoDB or not
  728. * @param array $sortByKeyData the sort by key dialog
  729. *
  730. * @return array
  731. */
  732. private function getTableNavigation(
  733. $posNext,
  734. $posPrevious,
  735. $isInnodb,
  736. array $sortByKeyData
  737. ): array {
  738. $isShowingAll = $_SESSION['tmpval']['max_rows'] === self::ALL_ROWS;
  739. // Move to the beginning or to the previous page
  740. $moveBackwardButtons = '';
  741. if ($_SESSION['tmpval']['pos'] && ! $isShowingAll) {
  742. $moveBackwardButtons = $this->getMoveBackwardButtonsForTableNavigation(
  743. htmlspecialchars($this->properties['sql_query']),
  744. $posPrevious
  745. );
  746. }
  747. $pageSelector = '';
  748. $numberTotalPage = 1;
  749. if (! $isShowingAll) {
  750. [
  751. $pageSelector,
  752. $numberTotalPage,
  753. ] = $this->getHtmlPageSelector();
  754. }
  755. // Move to the next page or to the last one
  756. $moveForwardButtons = '';
  757. if (
  758. $this->properties['unlim_num_rows'] === -1 // view with unknown number of rows
  759. || (! $isShowingAll
  760. && intval($_SESSION['tmpval']['pos']) + intval($_SESSION['tmpval']['max_rows'])
  761. < $this->properties['unlim_num_rows']
  762. && $this->properties['num_rows'] >= $_SESSION['tmpval']['max_rows'])
  763. ) {
  764. $moveForwardButtons = $this->getMoveForwardButtonsForTableNavigation(
  765. htmlspecialchars($this->properties['sql_query']),
  766. $posNext,
  767. $isInnodb
  768. );
  769. }
  770. $hiddenFields = [
  771. 'db' => $this->properties['db'],
  772. 'table' => $this->properties['table'],
  773. 'server' => $this->properties['server'],
  774. 'sql_query' => $this->properties['sql_query'],
  775. 'is_browse_distinct' => $this->properties['is_browse_distinct'],
  776. 'goto' => $this->properties['goto'],
  777. ];
  778. return [
  779. 'move_backward_buttons' => $moveBackwardButtons,
  780. 'page_selector' => $pageSelector,
  781. 'move_forward_buttons' => $moveForwardButtons,
  782. 'number_total_page' => $numberTotalPage,
  783. 'has_show_all' => $GLOBALS['cfg']['ShowAll'] || ($this->properties['unlim_num_rows'] <= 500),
  784. 'hidden_fields' => $hiddenFields,
  785. 'session_max_rows' => $isShowingAll ? $GLOBALS['cfg']['MaxRows'] : 'all',
  786. 'is_showing_all' => $isShowingAll,
  787. 'max_rows' => $_SESSION['tmpval']['max_rows'],
  788. 'pos' => $_SESSION['tmpval']['pos'],
  789. 'sort_by_key' => $sortByKeyData,
  790. ];
  791. }
  792. /**
  793. * Prepare move backward buttons - previous and first
  794. *
  795. * @see getTableNavigation()
  796. *
  797. * @param string $htmlSqlQuery the sql encoded by html special characters
  798. * @param int $posPrev the offset for the "previous" page
  799. *
  800. * @return string html content
  801. *
  802. * @access private
  803. */
  804. private function getMoveBackwardButtonsForTableNavigation(
  805. $htmlSqlQuery,
  806. $posPrev
  807. ) {
  808. return $this->getTableNavigationButton(
  809. '&lt;&lt;',
  810. _pgettext('First page', 'Begin'),
  811. 0,
  812. $htmlSqlQuery,
  813. true
  814. )
  815. . $this->getTableNavigationButton(
  816. '&lt;',
  817. _pgettext('Previous page', 'Previous'),
  818. $posPrev,
  819. $htmlSqlQuery,
  820. true
  821. );
  822. }
  823. /**
  824. * Prepare move forward buttons - next and last
  825. *
  826. * @see getTableNavigation()
  827. *
  828. * @param string $htmlSqlQuery the sql encoded by htmlspecialchars()
  829. * @param int $posNext the offset for the "next" page
  830. * @param bool $isInnodb whether it's InnoDB or not
  831. *
  832. * @return string html content
  833. *
  834. * @access private
  835. */
  836. private function getMoveForwardButtonsForTableNavigation(
  837. $htmlSqlQuery,
  838. $posNext,
  839. $isInnodb
  840. ) {
  841. // display the Next button
  842. $buttonsHtml = $this->getTableNavigationButton(
  843. '&gt;',
  844. _pgettext('Next page', 'Next'),
  845. $posNext,
  846. $htmlSqlQuery,
  847. false
  848. );
  849. // prepare some options for the End button
  850. if ($isInnodb && $this->properties['unlim_num_rows'] > $GLOBALS['cfg']['MaxExactCount']) {
  851. $inputForRealEnd = '<input id="real_end_input" type="hidden" name="find_real_end" value="1">';
  852. // no backquote around this message
  853. $onclick = '';
  854. } else {
  855. $inputForRealEnd = $onclick = '';
  856. }
  857. $maxRows = (int) $_SESSION['tmpval']['max_rows'];
  858. $onsubmit = 'onsubmit="return '
  859. . (intval($_SESSION['tmpval']['pos'])
  860. + $maxRows
  861. < $this->properties['unlim_num_rows']
  862. && $this->properties['num_rows'] >= $maxRows
  863. ? 'true'
  864. : 'false') . '"';
  865. // display the End button
  866. return $buttonsHtml . $this->getTableNavigationButton(
  867. '&gt;&gt;',
  868. _pgettext('Last page', 'End'),
  869. @((int) ceil(
  870. $this->properties['unlim_num_rows']
  871. / $_SESSION['tmpval']['max_rows']
  872. ) - 1) * $maxRows,
  873. $htmlSqlQuery,
  874. false,
  875. $onsubmit,
  876. $inputForRealEnd,
  877. $onclick
  878. );
  879. }
  880. /**
  881. * Get the headers of the results table, for all of the columns
  882. *
  883. * @see getTableHeaders()
  884. *
  885. * @param array $displayParts which elements to display
  886. * @param array $analyzedSqlResults analyzed sql results
  887. * @param array $sortExpression sort expression
  888. * @param array $sortExpressionNoDirection sort expression
  889. * without direction
  890. * @param array $sortDirection sort direction
  891. * @param bool $isLimitedDisplay with limited operations
  892. * or not
  893. * @param string $unsortedSqlQuery query without the sort part
  894. *
  895. * @return string html content
  896. *
  897. * @access private
  898. */
  899. private function getTableHeadersForColumns(
  900. array $displayParts,
  901. array $analyzedSqlResults,
  902. array $sortExpression,
  903. array $sortExpressionNoDirection,
  904. array $sortDirection,
  905. $isLimitedDisplay,
  906. $unsortedSqlQuery
  907. ) {
  908. // required to generate sort links that will remember whether the
  909. // "Show all" button has been clicked
  910. $sqlMd5 = md5($this->properties['server'] . $this->properties['db'] . $this->properties['sql_query']);
  911. $sessionMaxRows = $isLimitedDisplay
  912. ? 0
  913. : $_SESSION['tmpval']['query'][$sqlMd5]['max_rows'];
  914. // Following variable are needed for use in isset/empty or
  915. // use with array indexes/safe use in the for loop
  916. $highlightColumns = $this->properties['highlight_columns'];
  917. /** @var FieldMetadata[] $fieldsMeta */
  918. $fieldsMeta = $this->properties['fields_meta'];
  919. // Prepare Display column comments if enabled
  920. // ($GLOBALS['cfg']['ShowBrowseComments']).
  921. $commentsMap = $this->getTableCommentsArray($analyzedSqlResults);
  922. [$colOrder, $colVisib] = $this->getColumnParams($analyzedSqlResults);
  923. // optimize: avoid calling a method on each iteration
  924. $numberOfColumns = $this->properties['fields_cnt'];
  925. $columns = [];
  926. for ($j = 0; $j < $numberOfColumns; $j++) {
  927. // PHP 7.4 fix for accessing array offset on bool
  928. $colVisibCurrent = is_array($colVisib) && isset($colVisib[$j]) ? $colVisib[$j] : null;
  929. // assign $i with the appropriate column order
  930. $i = $colOrder ? $colOrder[$j] : $j;
  931. // See if this column should get highlight because it's used in the
  932. // where-query.
  933. $name = $fieldsMeta[$i]->name;
  934. $conditionField = isset($highlightColumns[$name])
  935. || isset($highlightColumns[Util::backquote($name)]);
  936. // Prepare comment-HTML-wrappers for each row, if defined/enabled.
  937. $comments = $this->getCommentForRow($commentsMap, $fieldsMeta[$i]);
  938. $displayParams = $this->properties['display_params'] ?? [];
  939. if (($displayParts['sort_lnk'] == '1') && ! $isLimitedDisplay) {
  940. [$orderLink, $sortedHeaderData] = $this->getOrderLinkAndSortedHeaderHtml(
  941. $fieldsMeta[$i],
  942. $sortExpression,
  943. $sortExpressionNoDirection,
  944. $i,
  945. $unsortedSqlQuery,
  946. $sessionMaxRows,
  947. $comments,
  948. $sortDirection,
  949. $colVisib,
  950. $colVisibCurrent
  951. );
  952. $columns[] = $sortedHeaderData;
  953. $displayParams['desc'][] = ' <th '
  954. . 'class="draggable'
  955. . ($conditionField ? ' condition' : '')
  956. . '" data-column="' . htmlspecialchars($fieldsMeta[$i]->name)
  957. . '">' . "\n" . $orderLink . $comments . ' </th>' . "\n";
  958. } else {
  959. // Results can't be sorted
  960. $columns[] = $this->getDraggableClassForNonSortableColumns(
  961. $colVisib,
  962. $colVisibCurrent,
  963. $conditionField,
  964. $fieldsMeta[$i],
  965. $comments
  966. );
  967. $displayParams['desc'][] = ' <th '
  968. . 'class="draggable'
  969. . ($conditionField ? ' condition"' : '')
  970. . '" data-column="' . htmlspecialchars((string) $fieldsMeta[$i]->name)
  971. . '"> '
  972. . htmlspecialchars((string) $fieldsMeta[$i]->name)
  973. . $comments . ' </th>';
  974. }
  975. $this->properties['display_params'] = $displayParams;
  976. }
  977. return $this->template->render('display/results/table_headers_for_columns', [
  978. 'is_sortable' => $displayParts['sort_lnk'] == '1' && ! $isLimitedDisplay,
  979. 'columns' => $columns,
  980. ]);
  981. }
  982. /**
  983. * Get the headers of the results table
  984. *
  985. * @see getTable()
  986. *
  987. * @param array $displayParts which elements to display
  988. * @param array $analyzedSqlResults analyzed sql results
  989. * @param string $unsortedSqlQuery the unsorted sql query
  990. * @param array $sortExpression sort expression
  991. * @param array|string $sortExpressionNoDirection sort expression without direction
  992. * @param array $sortDirection sort direction
  993. * @param bool $isLimitedDisplay with limited operations or not
  994. *
  995. * @return array
  996. */
  997. private function getTableHeaders(
  998. array &$displayParts,
  999. array $analyzedSqlResults,
  1000. $unsortedSqlQuery,
  1001. array $sortExpression = [],
  1002. $sortExpressionNoDirection = '',
  1003. array $sortDirection = [],
  1004. $isLimitedDisplay = false
  1005. ): array {
  1006. // Needed for use in isset/empty or
  1007. // use with array indexes/safe use in foreach
  1008. $printView = $this->properties['printview'];
  1009. $displayParams = $this->properties['display_params'];
  1010. // Output data needed for column reordering and show/hide column
  1011. $columnOrder = $this->getDataForResettingColumnOrder($analyzedSqlResults);
  1012. $displayParams['emptypre'] = 0;
  1013. $displayParams['emptyafter'] = 0;
  1014. $displayParams['textbtn'] = '';
  1015. $fullOrPartialTextLink = '';
  1016. $this->properties['display_params'] = $displayParams;
  1017. // Display options (if we are not in print view)
  1018. $optionsBlock = [];
  1019. if (! (isset($printView) && ($printView == '1')) && ! $isLimitedDisplay) {
  1020. $optionsBlock = $this->getOptionsBlock();
  1021. // prepare full/partial text button or link
  1022. $fullOrPartialTextLink = $this->getFullOrPartialTextButtonOrLink();
  1023. }
  1024. // 1. Set $colspan and generate html with full/partial
  1025. // text button or link
  1026. [$colspan, $buttonHtml] = $this->getFieldVisibilityParams($displayParts, $fullOrPartialTextLink);
  1027. // 2. Displays the fields' name
  1028. // 2.0 If sorting links should be used, checks if the query is a "JOIN"
  1029. // statement (see 2.1.3)
  1030. // See if we have to highlight any header fields of a WHERE query.
  1031. // Uses SQL-Parser results.
  1032. $this->setHighlightedColumnGlobalField($analyzedSqlResults);
  1033. // Get the headers for all of the columns
  1034. $tableHeadersForColumns = $this->getTableHeadersForColumns(
  1035. $displayParts,
  1036. $analyzedSqlResults,
  1037. $sortExpression,
  1038. $sortExpressionNoDirection,
  1039. $sortDirection,
  1040. $isLimitedDisplay,
  1041. $unsortedSqlQuery
  1042. );
  1043. // Display column at rightside - checkboxes or empty column
  1044. $columnAtRightSide = '';
  1045. if (! $printView) {
  1046. $columnAtRightSide = $this->getColumnAtRightSide($displayParts, $fullOrPartialTextLink, $colspan);
  1047. }
  1048. return [
  1049. 'column_order' => $columnOrder,
  1050. 'options' => $optionsBlock,
  1051. 'has_bulk_actions_form' => $displayParts['del_lnk'] === self::DELETE_ROW
  1052. || $displayParts['del_lnk'] === self::KILL_PROCESS,
  1053. 'button' => $buttonHtml,
  1054. 'table_headers_for_columns' => $tableHeadersForColumns,
  1055. 'column_at_right_side' => $columnAtRightSide,
  1056. ];
  1057. }
  1058. /**
  1059. * Prepare unsorted sql query and sort by key drop down
  1060. *
  1061. * @see getTableHeaders()
  1062. *
  1063. * @param array $analyzedSqlResults analyzed sql results
  1064. * @param array|null $sortExpression sort expression
  1065. *
  1066. * @return array two element array - $unsorted_sql_query, $drop_down_html
  1067. *
  1068. * @access private
  1069. */
  1070. private function getUnsortedSqlAndSortByKeyDropDown(
  1071. array $analyzedSqlResults,
  1072. ?array $sortExpression
  1073. ) {
  1074. $dropDownData = [];
  1075. $unsortedSqlQuery = Query::replaceClause(
  1076. $analyzedSqlResults['statement'],
  1077. $analyzedSqlResults['parser']->list,
  1078. 'ORDER BY',
  1079. ''
  1080. );
  1081. // Data is sorted by indexes only if it there is only one table.
  1082. if ($this->isSelect($analyzedSqlResults)) {
  1083. // grab indexes data:
  1084. $indexes = Index::getFromTable($this->properties['table'], $this->properties['db']);
  1085. // do we have any index?
  1086. if (! empty($indexes)) {
  1087. $dropDownData = $this->getSortByKeyDropDown($indexes, $sortExpression, $unsortedSqlQuery);
  1088. }
  1089. }
  1090. return [$unsortedSqlQuery, $dropDownData];
  1091. }
  1092. /**
  1093. * Prepare sort by key dropdown - html code segment
  1094. *
  1095. * @see getTableHeaders()
  1096. *
  1097. * @param Index[] $indexes the indexes of the table for sort criteria
  1098. * @param array|null $sortExpression the sort expression
  1099. * @param string $unsortedSqlQuery the unsorted sql query
  1100. *
  1101. * @return array
  1102. */
  1103. private function getSortByKeyDropDown(
  1104. $indexes,
  1105. ?array $sortExpression,
  1106. $unsortedSqlQuery
  1107. ): array {
  1108. $hiddenFields = [
  1109. 'db' => $this->properties['db'],
  1110. 'table' => $this->properties['table'],
  1111. 'server' => $this->properties['server'],
  1112. 'sort_by_key' => '1',
  1113. ];
  1114. // Keep the number of rows (25, 50, 100, ...) when changing sort key value
  1115. if (isset($_SESSION['tmpval']) && isset($_SESSION['tmpval']['max_rows'])) {
  1116. $hiddenFields['session_max_rows'] = $_SESSION['tmpval']['max_rows'];
  1117. }
  1118. $isIndexUsed = false;
  1119. $localOrder = is_array($sortExpression) ? implode(', ', $sortExpression) : '';
  1120. $options = [];
  1121. foreach ($indexes as $index) {
  1122. $ascSort = '`'
  1123. . implode('` ASC, `', array_keys($index->getColumns()))
  1124. . '` ASC';
  1125. $descSort = '`'
  1126. . implode('` DESC, `', array_keys($index->getColumns()))
  1127. . '` DESC';
  1128. $isIndexUsed = $isIndexUsed
  1129. || $localOrder === $ascSort
  1130. || $localOrder === $descSort;
  1131. $unsortedSqlQueryFirstPart = $unsortedSqlQuery;
  1132. $unsortedSqlQuerySecondPart = '';
  1133. if (
  1134. preg_match(
  1135. '@(.*)([[:space:]](LIMIT (.*)|PROCEDURE (.*)|FOR UPDATE|LOCK IN SHARE MODE))@is',
  1136. $unsortedSqlQuery,
  1137. $myReg
  1138. )
  1139. ) {
  1140. $unsortedSqlQueryFirstPart = $myReg[1];
  1141. $unsortedSqlQuerySecondPart = $myReg[2];
  1142. }
  1143. $options[] = [
  1144. 'value' => $unsortedSqlQueryFirstPart . ' ORDER BY '
  1145. . $ascSort . $unsortedSqlQuerySecondPart,
  1146. 'content' => $index->getName() . ' (ASC)',
  1147. 'is_selected' => $localOrder === $ascSort,
  1148. ];
  1149. $options[] = [
  1150. 'value' => $unsortedSqlQueryFirstPart . ' ORDER BY '
  1151. . $descSort . $unsortedSqlQuerySecondPart,
  1152. 'content' => $index->getName() . ' (DESC)',
  1153. 'is_selected' => $localOrder === $descSort,
  1154. ];
  1155. }
  1156. $options[] = [
  1157. 'value' => $unsortedSqlQuery,
  1158. 'content' => __('None'),
  1159. 'is_selected' => ! $isIndexUsed,
  1160. ];
  1161. return ['hidden_fields' => $hiddenFields, 'options' => $options];
  1162. }
  1163. /**
  1164. * Set column span, row span and prepare html with full/partial
  1165. * text button or link
  1166. *
  1167. * @see getTableHeaders()
  1168. *
  1169. * @param array $displayParts which elements to display
  1170. * @param string $fullOrPartialTextLink full/partial link or text button
  1171. *
  1172. * @return array 2 element array - $colspan, $button_html
  1173. *
  1174. * @access private
  1175. */
  1176. private function getFieldVisibilityParams(
  1177. array &$displayParts,
  1178. $fullOrPartialTextLink
  1179. ) {
  1180. $buttonHtml = '';
  1181. $displayParams = $this->properties['display_params'];
  1182. // 1. Displays the full/partial text button (part 1)...
  1183. $buttonHtml .= '<thead class="table-light"><tr>' . "\n";
  1184. $emptyPreCondition = $displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE
  1185. && $displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE;
  1186. $colspan = $emptyPreCondition ? ' colspan="4"'
  1187. : '';
  1188. $leftOrBoth = $GLOBALS['cfg']['RowActionLinks'] === self::POSITION_LEFT
  1189. || $GLOBALS['cfg']['RowActionLinks'] === self::POSITION_BOTH;
  1190. // ... before the result table
  1191. if (
  1192. ($displayParts['edit_lnk'] === self::NO_EDIT_OR_DELETE)
  1193. && ($displayParts['del_lnk'] === self::NO_EDIT_OR_DELETE)
  1194. && ($displayParts['text_btn'] == '1')
  1195. ) {
  1196. $displayParams['emptypre'] = $emptyPreCondition ? 4 : 0;
  1197. } elseif ($leftOrBoth && ($displayParts['text_btn'] == '1')) {
  1198. // ... at the left column of the result table header if possible
  1199. // and required
  1200. $displayParams['emptypre'] = $emptyPreCondition ? 4 : 0;
  1201. $buttonHtml .= '<th class="column_action sticky d-print-none" ' . $colspan
  1202. . '>' . $fullOrPartialTextLink . '</th>';
  1203. } elseif (
  1204. $leftOrBoth
  1205. && (($displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE)
  1206. || ($displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE))
  1207. ) {
  1208. // ... elseif no button, displays empty(ies) col(s) if required
  1209. $displayParams['emptypre'] = $emptyPreCondition ? 4 : 0;
  1210. $buttonHtml .= '<td ' . $colspan . '></td>';
  1211. } elseif ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_NONE) {
  1212. // ... elseif display an empty column if the actions links are
  1213. // disabled to match the rest of the table
  1214. $buttonHtml .= '<th class="column_action sticky"></th>';
  1215. }
  1216. $this->properties['display_params'] = $displayParams;
  1217. return [
  1218. $colspan,
  1219. $buttonHtml,
  1220. ];
  1221. }
  1222. /**
  1223. * Get table comments as array
  1224. *
  1225. * @see getTableHeaders()
  1226. *
  1227. * @param array $analyzedSqlResults analyzed sql results
  1228. *
  1229. * @return array table comments
  1230. *
  1231. * @access private
  1232. */
  1233. private function getTableCommentsArray(array $analyzedSqlResults)
  1234. {
  1235. if (! $GLOBALS['cfg']['ShowBrowseComments'] || empty($analyzedSqlResults['statement']->from)) {
  1236. return [];
  1237. }
  1238. $ret = [];
  1239. foreach ($analyzedSqlResults['statement']->from as $field) {
  1240. if (empty($field->table)) {
  1241. continue;
  1242. }
  1243. $ret[$field->table] = $this->relation->getComments(
  1244. empty($field->database) ? $this->properties['db'] : $field->database,
  1245. $field->table
  1246. );
  1247. }
  1248. return $ret;
  1249. }
  1250. /**
  1251. * Set global array for store highlighted header fields
  1252. *
  1253. * @see getTableHeaders()
  1254. *
  1255. * @param array $analyzedSqlResults analyzed sql results
  1256. *
  1257. * @access private
  1258. */
  1259. private function setHighlightedColumnGlobalField(array $analyzedSqlResults): void
  1260. {
  1261. $highlightColumns = [];
  1262. if (! empty($analyzedSqlResults['statement']->where)) {
  1263. foreach ($analyzedSqlResults['statement']->where as $expr) {
  1264. foreach ($expr->identifiers as $identifier) {
  1265. $highlightColumns[$identifier] = 'true';
  1266. }
  1267. }
  1268. }
  1269. $this->properties['highlight_columns'] = $highlightColumns;
  1270. }
  1271. /**
  1272. * Prepare data for column restoring and show/hide
  1273. *
  1274. * @see getTableHeaders()
  1275. *
  1276. * @param array $analyzedSqlResults analyzed sql results
  1277. *
  1278. * @return array
  1279. */
  1280. private function getDataForResettingColumnOrder(array $analyzedSqlResults): array
  1281. {
  1282. global $dbi;
  1283. if (! $this->isSelect($analyzedSqlResults)) {
  1284. return [];
  1285. }
  1286. [$columnOrder, $columnVisibility] = $this->getColumnParams($analyzedSqlResults);
  1287. $tableCreateTime = '';
  1288. $table = new Table($this->properties['table'], $this->properties['db']);
  1289. if (! $table->isView()) {
  1290. $tableCreateTime = $dbi->getTable(
  1291. $this->properties['db'],
  1292. $this->properties['table']
  1293. )->getStatusInfo('Create_time');
  1294. }
  1295. return [
  1296. 'order' => $columnOrder,
  1297. 'visibility' => $columnVisibility,
  1298. 'is_view' => $table->isView(),
  1299. 'table_create_time' => $tableCreateTime,
  1300. ];
  1301. }
  1302. /**
  1303. * Prepare option fields block
  1304. *
  1305. * @see getTableHeaders()
  1306. *
  1307. * @return array
  1308. */
  1309. private function getOptionsBlock(): array
  1310. {
  1311. if (
  1312. isset($_SESSION['tmpval']['possible_as_geometry'])
  1313. && $_SESSION['tmpval']['possible_as_geometry'] == false
  1314. ) {
  1315. if ($_SESSION['tmpval']['geoOption'] === self::GEOMETRY_DISP_GEOM) {
  1316. $_SESSION['tmpval']['geoOption'] = self::GEOMETRY_DISP_WKT;
  1317. }
  1318. }
  1319. return [
  1320. 'geo_option' => $_SESSION['tmpval']['geoOption'],
  1321. 'hide_transformation' => $_SESSION['tmpval']['hide_transformation'],
  1322. 'display_blob' => $_SESSION['tmpval']['display_blob'],
  1323. 'display_binary' => $_SESSION['tmpval']['display_binary'],
  1324. 'relational_display' => $_SESSION['tmpval']['relational_display'],
  1325. 'possible_as_geometry' => $_SESSION['tmpval']['possible_as_geometry'],
  1326. 'pftext' => $_SESSION['tmpval']['pftext'],
  1327. ];
  1328. }
  1329. /**
  1330. * Get full/partial text button or link
  1331. *
  1332. * @see getTableHeaders()
  1333. *
  1334. * @return string html content
  1335. *
  1336. * @access private
  1337. */
  1338. private function getFullOrPartialTextButtonOrLink()
  1339. {
  1340. global $theme;
  1341. $urlParamsFullText = [
  1342. 'db' => $this->properties['db'],
  1343. 'table' => $this->properties['table'],
  1344. 'sql_query' => $this->properties['sql_query'],
  1345. 'goto' => $this->properties['goto'],
  1346. 'full_text_button' => 1,
  1347. ];
  1348. if ($_SESSION['tmpval']['pftext'] === self::DISPLAY_FULL_TEXT) {
  1349. // currently in fulltext mode so show the opposite link
  1350. $tmpImageFile = 's_partialtext.png';
  1351. $tmpTxt = __('Partial texts');
  1352. $urlParamsFullText['pftext'] = self::DISPLAY_PARTIAL_TEXT;
  1353. } else {
  1354. $tmpImageFile = 's_fulltext.png';
  1355. $tmpTxt = __('Full texts');
  1356. $urlParamsFullText['pftext'] = self::DISPLAY_FULL_TEXT;
  1357. }
  1358. $tmpImage = '<img class="fulltext" src="'
  1359. . ($theme instanceof Theme ? $theme->getImgPath($tmpImageFile) : '')
  1360. . '" alt="' . $tmpTxt . '" title="' . $tmpTxt . '">';
  1361. $tmpUrl = Url::getFromRoute('/sql', $urlParamsFullText);
  1362. return Generator::linkOrButton($tmpUrl, $tmpImage);
  1363. }
  1364. /**
  1365. * Get comment for row
  1366. *
  1367. * @see getTableHeaders()
  1368. *
  1369. * @param array $commentsMap comments array
  1370. * @param FieldMetadata $fieldsMeta set of field properties
  1371. *
  1372. * @return string html content
  1373. *
  1374. * @access private
  1375. */
  1376. private function getCommentForRow(array $commentsMap, FieldMetadata $fieldsMeta)
  1377. {
  1378. return $this->template->render('display/results/comment_for_row', [
  1379. 'comments_map' => $commentsMap,
  1380. 'column_name' => $fieldsMeta->name,
  1381. 'table_name' => $fieldsMeta->table,
  1382. 'limit_chars' => $GLOBALS['cfg']['LimitChars'],
  1383. ]);
  1384. }
  1385. /**
  1386. * Prepare parameters and html for sorted table header fields
  1387. *
  1388. * @see getTableHeaders()
  1389. *
  1390. * @param FieldMetadata $fieldsMeta set of field properties
  1391. * @param array $sortExpression sort expression
  1392. * @param array $sortExpressionNoDirection sort expression without direction
  1393. * @param int $columnIndex the index of the column
  1394. * @param string $unsortedSqlQuery the unsorted sql query
  1395. * @param int $sessionMaxRows maximum rows resulted by sql
  1396. * @param string $comments comment for row
  1397. * @param array $sortDirection sort direction
  1398. * @param bool $colVisib column is visible(false) or column isn't visible(string array)
  1399. * @param string $colVisibElement element of $col_visib array
  1400. *
  1401. * @return array 2 element array - $orderLink, $sortedHeaderHtml
  1402. *
  1403. * @access private
  1404. */
  1405. private function getOrderLinkAndSortedHeaderHtml(
  1406. FieldMetadata $fieldsMeta,
  1407. array $sortExpression,
  1408. array $sortExpressionNoDirection,
  1409. $columnIndex,
  1410. $unsortedSqlQuery,
  1411. $sessionMaxRows,
  1412. $comments,
  1413. array $sortDirection,
  1414. $colVisib,
  1415. $colVisibElement
  1416. ) {
  1417. // Checks if the table name is required; it's the case
  1418. // for a query with a "JOIN" statement and if the column
  1419. // isn't aliased, or in queries like
  1420. // SELECT `1`.`master_field` , `2`.`master_field`
  1421. // FROM `PMA_relation` AS `1` , `PMA_relation` AS `2`
  1422. $sortTable = isset($fieldsMeta->table)
  1423. && strlen($fieldsMeta->table) > 0
  1424. && $fieldsMeta->orgname == $fieldsMeta->name
  1425. ? Util::backquote($fieldsMeta->table) . '.'
  1426. : '';
  1427. $nameToUseInSort = $fieldsMeta->name;
  1428. // Generates the orderby clause part of the query which is part
  1429. // of URL
  1430. [$singleSortOrder, $multiSortOrder, $orderImg] = $this->getSingleAndMultiSortUrls(
  1431. $sortExpression,
  1432. $sortExpressionNoDirection,
  1433. $sortTable,
  1434. $nameToUseInSort,
  1435. $sortDirection,
  1436. $fieldsMeta
  1437. );
  1438. if (
  1439. preg_match(
  1440. '@(.*)([[:space:]](LIMIT (.*)|PROCEDURE (.*)|FOR UPDATE|LOCK IN SHARE MODE))@is',
  1441. $unsortedSqlQuery,
  1442. $regs3
  1443. )
  1444. ) {
  1445. $singleSortedSqlQuery = $regs3[1] . $singleSortOrder . $regs3[2];
  1446. $multiSortedSqlQuery = $regs3[1] . $multiSortOrder . $regs3[2];
  1447. } else {
  1448. $singleSortedSqlQuery = $unsortedSqlQuery . $singleSortOrder;
  1449. $multiSortedSqlQuery = $unsortedSqlQuery . $multiSortOrder;
  1450. }
  1451. $singleUrlParams = [
  1452. 'db' => $this->properties['db'],
  1453. 'table' => $this->properties['table'],
  1454. 'sql_query' => $singleSortedSqlQuery,
  1455. 'sql_signature' => Core::signSqlQuery($singleSortedSqlQuery),
  1456. 'session_max_rows' => $sessionMaxRows,
  1457. 'is_browse_distinct' => $this->properties['is_browse_distinct'],
  1458. ];
  1459. $multiUrlParams = [
  1460. 'db' => $this->properties['db'],
  1461. 'table' => $this->properties['table'],
  1462. 'sql_query' => $multiSortedSqlQuery,
  1463. 'sql_signature' => Core::signSqlQuery($multiSortedSqlQuery),
  1464. 'session_max_rows' => $sessionMaxRows,
  1465. 'is_browse_distinct' => $this->properties['is_browse_distinct'],
  1466. ];
  1467. $singleOrderUrl = Url::getFromRoute('/sql', $singleUrlParams);
  1468. $multiOrderUrl = Url::getFromRoute('/sql', $multiUrlParams);
  1469. // Displays the sorting URL
  1470. // enable sort order swapping for image
  1471. $orderLink = $this->getSortOrderLink($orderImg, $fieldsMeta, $singleOrderUrl, $multiOrderUrl);
  1472. $orderLink .= $this->getSortOrderHiddenInputs($multiUrlParams, $nameToUseInSort);
  1473. $thClass = [];
  1474. $this->getClassForNumericColumnType($fieldsMeta, $thClass);
  1475. $sortedHeaderData = [
  1476. 'column_name' => $fieldsMeta->name,
  1477. 'order_link' => $orderLink,
  1478. 'comments' => $comments,
  1479. 'is_browse_pointer_enabled' => $GLOBALS['cfg']['BrowsePointerEnable'] === true,
  1480. 'is_browse_marker_enabled' => $GLOBALS['cfg']['BrowseMarkerEnable'] === true,
  1481. 'is_column_hidden' => $colVisib && ! $colVisibElement,
  1482. 'is_column_numeric' => ! empty($thClass),
  1483. ];
  1484. return [$orderLink, $sortedHeaderData];
  1485. }
  1486. /**
  1487. * Prepare parameters and html for sorted table header fields
  1488. *
  1489. * @param array $sortExpression sort expression
  1490. * @param array $sortExpressionNoDirection sort expression without direction
  1491. * @param string $sortTable The name of the table to which
  1492. * the current column belongs to
  1493. * @param string $nameToUseInSort The current column under
  1494. * consideration
  1495. * @param array $sortDirection sort direction
  1496. * @param FieldMetadata $fieldsMeta set of field properties
  1497. *
  1498. * @return array 3 element array - $single_sort_order, $sort_order, $order_img
  1499. *
  1500. * @access private
  1501. */
  1502. private function getSingleAndMultiSortUrls(
  1503. array $sortExpression,
  1504. array $sortExpressionNoDirection,
  1505. $sortTable,
  1506. $nameToUseInSort,
  1507. array $sortDirection,
  1508. FieldMetadata $fieldsMeta
  1509. ) {
  1510. $sortOrder = '';
  1511. // Check if the current column is in the order by clause
  1512. $isInSort = $this->isInSorted($sortExpression, $sortExpressionNoDirection, $sortTable, $nameToUseInSort);
  1513. $currentName = $nameToUseInSort;
  1514. if ($sortExpressionNoDirection[0] == '' || ! $isInSort) {
  1515. $specialIndex = $sortExpressionNoDirection[0] == ''
  1516. ? 0
  1517. : count($sortExpressionNoDirection);
  1518. $sortExpressionNoDirection[$specialIndex] = Util::backquote($currentName);
  1519. $isTimeOrDate = $fieldsMeta->isType(FieldMetadata::TYPE_TIME)
  1520. || $fieldsMeta->isType(FieldMetadata::TYPE_DATE)
  1521. || $fieldsMeta->isType(FieldMetadata::TYPE_DATETIME)
  1522. || $fieldsMeta->isType(FieldMetadata::TYPE_TIMESTAMP);
  1523. $sortDirection[$specialIndex] = $isTimeOrDate ? self::DESCENDING_SORT_DIR : self::ASCENDING_SORT_DIR;
  1524. }
  1525. $sortExpressionNoDirection = array_filter($sortExpressionNoDirection);
  1526. $singleSortOrder = null;
  1527. foreach ($sortExpressionNoDirection as $index => $expression) {
  1528. // check if this is the first clause,
  1529. // if it is then we have to add "order by"
  1530. $isFirstClause = ($index == 0);
  1531. $nameToUseInSort = $expression;
  1532. $sortTableNew = $sortTable;
  1533. // Test to detect if the column name is a standard name
  1534. // Standard name has the table name prefixed to the column name
  1535. if (str_contains($nameToUseInSort, '.')) {
  1536. $matches = explode('.', $nameToUseInSort);
  1537. // Matches[0] has the table name
  1538. // Matches[1] has the column name
  1539. $nameToUseInSort = $matches[1];
  1540. $sortTableNew = $matches[0];
  1541. }
  1542. // $name_to_use_in_sort might contain a space due to
  1543. // formatting of function expressions like "COUNT(name )"
  1544. // so we remove the space in this situation
  1545. $nameToUseInSort = str_replace([' )', '``'], [')', '`'], $nameToUseInSort);
  1546. $nameToUseInSort = trim($nameToUseInSort, '`');
  1547. // If this the first column name in the order by clause add
  1548. // order by clause to the column name
  1549. $queryHead = $isFirstClause ? "\nORDER BY " : '';
  1550. // Again a check to see if the given column is a aggregate column
  1551. if (str_contains($nameToUseInSort, '(')) {
  1552. $sortOrder .= $queryHead . $nameToUseInSort . ' ';
  1553. } else {
  1554. if (strlen($sortTableNew) > 0) {
  1555. $sortTableNew .= '.';
  1556. }
  1557. $sortOrder .= $queryHead . $sortTableNew
  1558. . Util::backquote($nameToUseInSort) . ' ';
  1559. }
  1560. // For a special case where the code generates two dots between
  1561. // column name and table name.
  1562. $sortOrder = preg_replace('/\.\./', '.', $sortOrder);
  1563. // Incase this is the current column save $single_sort_order
  1564. if ($currentName == $nameToUseInSort) {
  1565. if (str_contains($currentName, '(')) {
  1566. $singleSortOrder = "\n" . 'ORDER BY ' . Util::backquote($currentName) . ' ';
  1567. } else {
  1568. $singleSortOrder = "\n" . 'ORDER BY ' . $sortTable
  1569. . Util::backquote($currentName) . ' ';
  1570. }
  1571. if ($isInSort) {
  1572. [$singleSortOrder, $orderImg] = $this->getSortingUrlParams(
  1573. $sortDirection,
  1574. $singleSortOrder,
  1575. $index
  1576. );
  1577. } else {
  1578. $singleSortOrder .= strtoupper($sortDirection[$index]);
  1579. }
  1580. }
  1581. if ($currentName == $nameToUseInSort && $isInSort) {
  1582. // We need to generate the arrow button and related html
  1583. [$sortOrder, $orderImg] = $this->getSortingUrlParams($sortDirection, $sortOrder, $index);
  1584. $orderImg .= ' <small>' . ($index + 1) . '</small>';
  1585. } else {
  1586. $sortOrder .= strtoupper($sortDirection[$index]);
  1587. }
  1588. // Separate columns by a comma
  1589. $sortOrder .= ', ';
  1590. }
  1591. // remove the comma from the last column name in the newly
  1592. // constructed clause
  1593. $sortOrder = mb_substr($sortOrder, 0, -2);
  1594. if (empty($orderImg)) {
  1595. $orderImg = '';
  1596. }
  1597. return [
  1598. $singleSortOrder,
  1599. $sortOrder,
  1600. $orderImg,
  1601. ];
  1602. }
  1603. /**
  1604. * Check whether the column is sorted
  1605. *
  1606. * @see getTableHeaders()
  1607. *
  1608. * @param array $sortExpression sort expression
  1609. * @param array $sortExpressionNoDirection sort expression without direction
  1610. * @param string $sortTable the table name
  1611. * @param string $nameToUseInSort the sorting column name
  1612. *
  1613. * @access private
  1614. */
  1615. private function isInSorted(
  1616. array $sortExpression,
  1617. array $sortExpressionNoDirection,
  1618. $sortTable,
  1619. $nameToUseInSort
  1620. ): bool {
  1621. $indexInExpression = 0;
  1622. foreach ($sortExpressionNoDirection as $index => $clause) {
  1623. if (str_contains($clause, '.')) {
  1624. $fragments = explode('.', $clause);
  1625. $clause2 = $fragments[0] . '.' . str_replace('`', '', $fragments[1]);
  1626. } else {
  1627. $clause2 = $sortTable . str_replace('`', '', $clause);
  1628. }
  1629. if ($clause2 === $sortTable . $nameToUseInSort) {
  1630. $indexInExpression = $index;
  1631. break;
  1632. }
  1633. }
  1634. if (empty($sortExpression[$indexInExpression])) {
  1635. $isInSort = false;
  1636. } else {
  1637. // Field name may be preceded by a space, or any number
  1638. // of characters followed by a dot (tablename.fieldname)
  1639. // so do a direct comparison for the sort expression;
  1640. // this avoids problems with queries like
  1641. // "SELECT id, count(id)..." and clicking to sort
  1642. // on id or on count(id).
  1643. // Another query to test this:
  1644. // SELECT p.*, FROM_UNIXTIME(p.temps) FROM mytable AS p
  1645. // (and try clicking on each column's header twice)
  1646. $noSortTable = empty($sortTable) || mb_strpos(
  1647. $sortExpressionNoDirection[$indexInExpression],
  1648. $sortTable
  1649. ) === false;
  1650. $noOpenParenthesis = mb_strpos($sortExpressionNoDirection[$indexInExpression], '(') === false;
  1651. if (! empty($sortTable) && $noSortTable && $noOpenParenthesis) {
  1652. $newSortExpressionNoDirection = $sortTable
  1653. . $sortExpressionNoDirection[$indexInExpression];
  1654. } else {
  1655. $newSortExpressionNoDirection = $sortExpressionNoDirection[$indexInExpression];
  1656. }
  1657. //Back quotes are removed in next comparison, so remove them from value
  1658. //to compare.
  1659. $nameToUseInSort = str_replace('`', '', $nameToUseInSort);
  1660. $isInSort = false;
  1661. $sortName = str_replace('`', '', $sortTable) . $nameToUseInSort;
  1662. if (
  1663. $sortName == str_replace('`', '', $newSortExpressionNoDirection)
  1664. || $sortName == str_replace('`', '', $sortExpressionNoDirection[$indexInExpression])
  1665. ) {
  1666. $isInSort = true;
  1667. }
  1668. }
  1669. return $isInSort;
  1670. }
  1671. /**
  1672. * Get sort url parameters - sort order and order image
  1673. *
  1674. * @see getSingleAndMultiSortUrls()
  1675. *
  1676. * @param array $sortDirection the sort direction
  1677. * @param string $sortOrder the sorting order
  1678. * @param int $index the index of sort direction array.
  1679. *
  1680. * @return array 2 element array - $sort_order, $order_img
  1681. *
  1682. * @access private
  1683. */
  1684. private function getSortingUrlParams(array $sortDirection, $sortOrder, $index)
  1685. {
  1686. if (strtoupper(trim($sortDirection[$index])) === self::DESCENDING_SORT_DIR) {
  1687. $sortOrder .= ' ASC';
  1688. $orderImg = ' ' . Generator::getImage(
  1689. 's_desc',
  1690. __('Descending'),
  1691. [
  1692. 'class' => 'soimg',
  1693. 'title' => '',
  1694. ]
  1695. );
  1696. $orderImg .= ' ' . Generator::getImage(
  1697. 's_asc',
  1698. __('Ascending'),
  1699. [
  1700. 'class' => 'soimg hide',
  1701. 'title' => '',
  1702. ]
  1703. );
  1704. } else {
  1705. $sortOrder .= ' DESC';
  1706. $orderImg = ' ' . Generator::getImage(
  1707. 's_asc',
  1708. __('Ascending'),
  1709. [
  1710. 'class' => 'soimg',
  1711. 'title' => '',
  1712. ]
  1713. );
  1714. $orderImg .= ' ' . Generator::getImage(
  1715. 's_desc',
  1716. __('Descending'),
  1717. [
  1718. 'class' => 'soimg hide',
  1719. 'title' => '',
  1720. ]
  1721. );
  1722. }
  1723. return [
  1724. $sortOrder,
  1725. $orderImg,
  1726. ];
  1727. }
  1728. /**
  1729. * Get sort order link
  1730. *
  1731. * @see getTableHeaders()
  1732. *
  1733. * @param string $orderImg the sort order image
  1734. * @param FieldMetadata $fieldsMeta set of field properties
  1735. * @param string $orderUrl the url for sort
  1736. * @param string $multiOrderUrl the url for sort
  1737. *
  1738. * @return string the sort order link
  1739. *
  1740. * @access private
  1741. */
  1742. private function getSortOrderLink(
  1743. $orderImg,
  1744. FieldMetadata $fieldsMeta,
  1745. $orderUrl,
  1746. $multiOrderUrl
  1747. ) {
  1748. $orderLinkParams = ['class' => 'sortlink'];
  1749. $orderLinkContent = htmlspecialchars($fieldsMeta->name ?? '');
  1750. $innerLinkContent = $orderLinkContent . $orderImg
  1751. . '<input type="hidden" value="' . $multiOrderUrl . '">';
  1752. return Generator::linkOrButton($orderUrl, $innerLinkContent, $orderLinkParams);
  1753. }
  1754. private function getSortOrderHiddenInputs(
  1755. array $multipleUrlParams,
  1756. string $nameToUseInSort
  1757. ): string {
  1758. $sqlQuery = $multipleUrlParams['sql_query'];
  1759. $sqlQueryAdd = $sqlQuery;
  1760. $sqlQueryRemove = null;
  1761. $parser = new Parser($sqlQuery);
  1762. $firstStatement = $parser->statements[0] ?? null;
  1763. $numberOfClausesFound = null;
  1764. if ($firstStatement instanceof SelectStatement) {
  1765. $orderClauses = $firstStatement->order ?? [];
  1766. foreach ($orderClauses as $key => $order) {
  1767. // If this is the column name, then remove it from the order clause
  1768. if ($order->expr->column !== $nameToUseInSort) {
  1769. continue;
  1770. }
  1771. // remove the order clause for this column and from the counted array
  1772. unset($firstStatement->order[$key], $orderClauses[$key]);
  1773. }
  1774. $numberOfClausesFound = count($orderClauses);
  1775. $sqlQueryRemove = $firstStatement->build();
  1776. }
  1777. $multipleUrlParams['sql_query'] = $sqlQueryRemove ?? $sqlQuery;
  1778. $multipleUrlParams['sql_signature'] = Core::signSqlQuery($multipleUrlParams['sql_query']);
  1779. $urlRemoveOrder = Url::getFromRoute('/sql', $multipleUrlParams);
  1780. if ($numberOfClausesFound !== null && $numberOfClausesFound === 0) {
  1781. $urlRemoveOrder .= '&discard_remembered_sort=1';
  1782. }
  1783. $multipleUrlParams['sql_query'] = $sqlQueryAdd;
  1784. $multipleUrlParams['sql_signature'] = Core::signSqlQuery($multipleUrlParams['sql_query']);
  1785. $urlAddOrder = Url::getFromRoute('/sql', $multipleUrlParams);
  1786. return '<input type="hidden" name="url-remove-order" value="' . $urlRemoveOrder . '">' . "\n"
  1787. . '<input type="hidden" name="url-add-order" value="' . $urlAddOrder . '">';
  1788. }
  1789. /**
  1790. * Check if the column contains numeric data. If yes, then set the
  1791. * column header's alignment right
  1792. *
  1793. * @param FieldMetadata $fieldsMeta set of field properties
  1794. * @param array $thClass array containing classes
  1795. */
  1796. private function getClassForNumericColumnType(FieldMetadata $fieldsMeta, array &$thClass): void
  1797. {
  1798. // This was defined in commit b661cd7c9b31f8bc564d2f9a1b8527e0eb966de8
  1799. // For issue https://github.com/phpmyadmin/phpmyadmin/issues/4746
  1800. if (
  1801. ! $fieldsMeta->isType(FieldMetadata::TYPE_REAL)
  1802. && ! $fieldsMeta->isMappedTypeBit
  1803. && ! $fieldsMeta->isType(FieldMetadata::TYPE_INT)
  1804. ) {
  1805. return;
  1806. }
  1807. $thClass[] = 'text-end';
  1808. }
  1809. /**
  1810. * Prepare columns to draggable effect for non sortable columns
  1811. *
  1812. * @see getTableHeaders()
  1813. *
  1814. * @param bool $colVisib the column is visible (false)
  1815. * array the column is not visible (string array)
  1816. * @param string $colVisibElement element of $col_visib array
  1817. * @param bool $conditionField whether to add CSS class condition
  1818. * @param FieldMetadata $fieldsMeta set of field properties
  1819. * @param string $comments the comment for the column
  1820. *
  1821. * @return array
  1822. */
  1823. private function getDraggableClassForNonSortableColumns(
  1824. $colVisib,
  1825. $colVisibElement,
  1826. $conditionField,
  1827. FieldMetadata $fieldsMeta,
  1828. $comments
  1829. ) {
  1830. $thClass = [];
  1831. $this->getClassForNumericColumnType($fieldsMeta, $thClass);
  1832. return [
  1833. 'column_name' => $fieldsMeta->name,
  1834. 'comments' => $comments,
  1835. 'is_column_hidden' => $colVisib && ! $colVisibElement,
  1836. 'is_column_numeric' => ! empty($thClass),
  1837. 'has_condition' => $conditionField,
  1838. ];
  1839. }
  1840. /**
  1841. * Prepare column to show at right side - check boxes or empty column
  1842. *
  1843. * @see getTableHeaders()
  1844. *
  1845. * @param array $displayParts which elements to display
  1846. * @param string $fullOrPartialTextLink full/partial link or text button
  1847. * @param string $colspan column span of table header
  1848. *
  1849. * @return string html content
  1850. *
  1851. * @access private
  1852. */
  1853. private function getColumnAtRightSide(
  1854. array &$displayParts,
  1855. $fullOrPartialTextLink,
  1856. $colspan
  1857. ) {
  1858. $rightColumnHtml = '';
  1859. $displayParams = $this->properties['display_params'];
  1860. // Displays the needed checkboxes at the right
  1861. // column of the result table header if possible and required...
  1862. if (
  1863. ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_RIGHT)
  1864. || ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_BOTH)
  1865. && (($displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE)
  1866. || ($displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE))
  1867. && ($displayParts['text_btn'] == '1')
  1868. ) {
  1869. $displayParams['emptyafter'] = ($displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE)
  1870. && ($displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE) ? 4 : 1;
  1871. $rightColumnHtml .= "\n"
  1872. . '<th class="column_action d-print-none" ' . $colspan . '>'
  1873. . $fullOrPartialTextLink
  1874. . '</th>';
  1875. } elseif (
  1876. ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_LEFT)
  1877. || ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_BOTH)
  1878. && (($displayParts['edit_lnk'] === self::NO_EDIT_OR_DELETE)
  1879. && ($displayParts['del_lnk'] === self::NO_EDIT_OR_DELETE))
  1880. && (! isset($GLOBALS['is_header_sent']) || ! $GLOBALS['is_header_sent'])
  1881. ) {
  1882. // ... elseif no button, displays empty columns if required
  1883. // (unless coming from Browse mode print view)
  1884. $displayParams['emptyafter'] = ($displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE)
  1885. && ($displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE) ? 4 : 1;
  1886. $rightColumnHtml .= "\n" . '<td class="d-print-none" ' . $colspan
  1887. . '></td>';
  1888. }
  1889. $this->properties['display_params'] = $displayParams;
  1890. return $rightColumnHtml;
  1891. }
  1892. /**
  1893. * Prepares the display for a value
  1894. *
  1895. * @see getDataCellForGeometryColumns(),
  1896. * getDataCellForNonNumericColumns()
  1897. *
  1898. * @param string $class class of table cell
  1899. * @param bool $conditionField whether to add CSS class condition
  1900. * @param string $value value to display
  1901. *
  1902. * @return string the td
  1903. *
  1904. * @access private
  1905. */
  1906. private function buildValueDisplay($class, $conditionField, $value)
  1907. {
  1908. return $this->template->render('display/results/value_display', [
  1909. 'class' => $class,
  1910. 'condition_field' => $conditionField,
  1911. 'value' => $value,
  1912. ]);
  1913. }
  1914. /**
  1915. * Prepares the display for a null value
  1916. *
  1917. * @see getDataCellForNumericColumns(),
  1918. * getDataCellForGeometryColumns(),
  1919. * getDataCellForNonNumericColumns()
  1920. *
  1921. * @param string $class class of table cell
  1922. * @param bool $conditionField whether to add CSS class condition
  1923. * @param FieldMetadata $meta the meta-information about this field
  1924. * @param string $align cell alignment
  1925. *
  1926. * @return string the td
  1927. *
  1928. * @access private
  1929. */
  1930. private function buildNullDisplay($class, $conditionField, FieldMetadata $meta, $align = '')
  1931. {
  1932. $classes = $this->addClass($class, $conditionField, $meta, '');
  1933. return $this->template->render('display/results/null_display', [
  1934. 'align' => $align,
  1935. 'data_decimals' => $meta->decimals ?? -1,
  1936. 'data_type' => $meta->getMappedType(),
  1937. 'classes' => $classes,
  1938. ]);
  1939. }
  1940. /**
  1941. * Prepares the display for an empty value
  1942. *
  1943. * @see getDataCellForNumericColumns(),
  1944. * getDataCellForGeometryColumns(),
  1945. * getDataCellForNonNumericColumns()
  1946. *
  1947. * @param string $class class of table cell
  1948. * @param bool $conditionField whether to add CSS class condition
  1949. * @param FieldMetadata $meta the meta-information about this field
  1950. * @param string $align cell alignment
  1951. *
  1952. * @return string the td
  1953. *
  1954. * @access private
  1955. */
  1956. private function buildEmptyDisplay($class, $conditionField, FieldMetadata $meta, $align = '')
  1957. {
  1958. $classes = $this->addClass($class, $conditionField, $meta, 'text-nowrap');
  1959. return $this->template->render('display/results/empty_display', [
  1960. 'align' => $align,
  1961. 'classes' => $classes,
  1962. ]);
  1963. }
  1964. /**
  1965. * Adds the relevant classes.
  1966. *
  1967. * @see buildNullDisplay(), getRowData()
  1968. *
  1969. * @param string $class class of table cell
  1970. * @param bool $conditionField whether to add CSS class condition
  1971. * @param FieldMetadata $meta the meta-information about the field
  1972. * @param string $nowrap avoid wrapping
  1973. * @param bool $isFieldTruncated is field truncated (display ...)
  1974. *
  1975. * @return string the list of classes
  1976. */
  1977. private function addClass(
  1978. $class,
  1979. $conditionField,
  1980. FieldMetadata $meta,
  1981. $nowrap,
  1982. $isFieldTruncated = false,
  1983. bool $hasTransformationPlugin = false
  1984. ) {
  1985. $classes = [
  1986. $class,
  1987. $nowrap,
  1988. ];
  1989. if (isset($meta->internalMediaType)) {
  1990. $classes[] = preg_replace('/\//', '_', $meta->internalMediaType);
  1991. }
  1992. if ($conditionField) {
  1993. $classes[] = 'condition';
  1994. }
  1995. if ($isFieldTruncated) {
  1996. $classes[] = 'truncated';
  1997. }
  1998. $mediaTypeMap = $this->properties['mime_map'];
  1999. $orgFullColName = $this->properties['db'] . '.' . $meta->orgtable
  2000. . '.' . $meta->orgname;
  2001. if ($hasTransformationPlugin || ! empty($mediaTypeMap[$orgFullColName]['input_transformation'])) {
  2002. $classes[] = 'transformed';
  2003. }
  2004. // Define classes to be added to this data field based on the type of data
  2005. if ($meta->isEnum()) {
  2006. $classes[] = 'enum';
  2007. }
  2008. if ($meta->isSet()) {
  2009. $classes[] = 'set';
  2010. }
  2011. if ($meta->isMappedTypeBit) {
  2012. $classes[] = 'bit';
  2013. }
  2014. if ($meta->isBinary()) {
  2015. $classes[] = 'hex';
  2016. }
  2017. return implode(' ', $classes);
  2018. }
  2019. /**
  2020. * Prepare the body of the results table
  2021. *
  2022. * @see getTable()
  2023. *
  2024. * @param int $dtResult the link id associated to the query
  2025. * which results have to be displayed
  2026. * @param array $displayParts which elements to display
  2027. * @param array $map the list of relations
  2028. * @param array $analyzedSqlResults analyzed sql results
  2029. * @param bool $isLimitedDisplay with limited operations or not
  2030. *
  2031. * @return string html content
  2032. *
  2033. * @global array $row current row data
  2034. * @access private
  2035. */
  2036. private function getTableBody(
  2037. &$dtResult,
  2038. array &$displayParts,
  2039. array $map,
  2040. array $analyzedSqlResults,
  2041. $isLimitedDisplay = false
  2042. ) {
  2043. global $dbi;
  2044. // Mostly because of browser transformations, to make the row-data accessible in a plugin.
  2045. global $row;
  2046. $tableBodyHtml = '';
  2047. // query without conditions to shorten URLs when needed, 200 is just
  2048. // guess, it should depend on remaining URL length
  2049. $urlSqlQuery = $this->getUrlSqlQuery($analyzedSqlResults);
  2050. $displayParams = $this->properties['display_params'];
  2051. if (! is_array($map)) {
  2052. $map = [];
  2053. }
  2054. $rowNumber = 0;
  2055. $displayParams['edit'] = [];
  2056. $displayParams['copy'] = [];
  2057. $displayParams['delete'] = [];
  2058. $displayParams['data'] = [];
  2059. $displayParams['row_delete'] = [];
  2060. $this->properties['display_params'] = $displayParams;
  2061. // name of the class added to all grid editable elements;
  2062. // if we don't have all the columns of a unique key in the result set,
  2063. // do not permit grid editing
  2064. if ($isLimitedDisplay || ! $this->properties['editable']) {
  2065. $gridEditClass = '';
  2066. } else {
  2067. switch ($GLOBALS['cfg']['GridEditing']) {
  2068. case 'double-click':
  2069. // trying to reduce generated HTML by using shorter
  2070. // classes like click1 and click2
  2071. $gridEditClass = 'grid_edit click2';
  2072. break;
  2073. case 'click':
  2074. $gridEditClass = 'grid_edit click1';
  2075. break;
  2076. default: // 'disabled'
  2077. $gridEditClass = '';
  2078. break;
  2079. }
  2080. }
  2081. // prepare to get the column order, if available
  2082. [$colOrder, $colVisib] = $this->getColumnParams($analyzedSqlResults);
  2083. // Correction University of Virginia 19991216 in the while below
  2084. // Previous code assumed that all tables have keys, specifically that
  2085. // the phpMyAdmin GUI should support row delete/edit only for such
  2086. // tables.
  2087. // Although always using keys is arguably the prescribed way of
  2088. // defining a relational table, it is not required. This will in
  2089. // particular be violated by the novice.
  2090. // We want to encourage phpMyAdmin usage by such novices. So the code
  2091. // below has been changed to conditionally work as before when the
  2092. // table being displayed has one or more keys; but to display
  2093. // delete/edit options correctly for tables without keys.
  2094. $whereClauseMap = $this->properties['whereClauseMap'];
  2095. while ($row = $dbi->fetchRow($dtResult)) {
  2096. // add repeating headers
  2097. if (
  2098. ($rowNumber != 0) && ($_SESSION['tmpval']['repeat_cells'] != 0)
  2099. && ! $rowNumber % $_SESSION['tmpval']['repeat_cells']
  2100. ) {
  2101. $tableBodyHtml .= $this->getRepeatingHeaders($displayParams);
  2102. }
  2103. $trClass = [];
  2104. if ($GLOBALS['cfg']['BrowsePointerEnable'] != true) {
  2105. $trClass[] = 'nopointer';
  2106. }
  2107. if ($GLOBALS['cfg']['BrowseMarkerEnable'] != true) {
  2108. $trClass[] = 'nomarker';
  2109. }
  2110. // pointer code part
  2111. $classes = (empty($trClass) ? ' ' : 'class="' . implode(' ', $trClass) . '"');
  2112. $tableBodyHtml .= '<tr ' . $classes . ' >';
  2113. // 1. Prepares the row
  2114. // In print view these variable needs to be initialized
  2115. $deleteUrl = null;
  2116. $deleteString = null;
  2117. $editString = null;
  2118. $jsConf = null;
  2119. $copyUrl = null;
  2120. $copyString = null;
  2121. $editUrl = null;
  2122. // 1.2 Defines the URLs for the modify/delete link(s)
  2123. if (
  2124. ($displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE)
  2125. || ($displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE)
  2126. ) {
  2127. $expressions = [];
  2128. if (
  2129. isset($analyzedSqlResults['statement'])
  2130. && $analyzedSqlResults['statement'] instanceof SelectStatement
  2131. ) {
  2132. $expressions = $analyzedSqlResults['statement']->expr;
  2133. }
  2134. // Results from a "SELECT" statement -> builds the
  2135. // WHERE clause to use in links (a unique key if possible)
  2136. /**
  2137. * @todo $where_clause could be empty, for example a table
  2138. * with only one field and it's a BLOB; in this case,
  2139. * avoid to display the delete and edit links
  2140. */
  2141. [$whereClause, $clauseIsUnique, $conditionArray] = Util::getUniqueCondition(
  2142. $dtResult,
  2143. $this->properties['fields_cnt'],
  2144. $this->properties['fields_meta'],
  2145. $row,
  2146. false,
  2147. $this->properties['table'],
  2148. $expressions
  2149. );
  2150. $whereClauseMap[$rowNumber][$this->properties['table']] = $whereClause;
  2151. $this->properties['whereClauseMap'] = $whereClauseMap;
  2152. // 1.2.1 Modify link(s) - update row case
  2153. if ($displayParts['edit_lnk'] === self::UPDATE_ROW) {
  2154. [
  2155. $editUrl,
  2156. $copyUrl,
  2157. $editString,
  2158. $copyString,
  2159. ] = $this->getModifiedLinks($whereClause, $clauseIsUnique, $urlSqlQuery);
  2160. }
  2161. // 1.2.2 Delete/Kill link(s)
  2162. [$deleteUrl, $deleteString, $jsConf] = $this->getDeleteAndKillLinks(
  2163. $whereClause,
  2164. $clauseIsUnique,
  2165. $urlSqlQuery,
  2166. $displayParts['del_lnk'],
  2167. $row
  2168. );
  2169. // 1.3 Displays the links at left if required
  2170. if (
  2171. ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_LEFT)
  2172. || ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_BOTH)
  2173. ) {
  2174. $tableBodyHtml .= $this->template->render('display/results/checkbox_and_links', [
  2175. 'position' => self::POSITION_LEFT,
  2176. 'has_checkbox' => ! empty($deleteUrl) && $displayParts['del_lnk'] !== self::KILL_PROCESS,
  2177. 'edit' => ['url' => $editUrl, 'string' => $editString, 'clause_is_unique' => $clauseIsUnique],
  2178. 'copy' => ['url' => $copyUrl, 'string' => $copyString],
  2179. 'delete' => ['url' => $deleteUrl, 'string' => $deleteString],
  2180. 'row_number' => $rowNumber,
  2181. 'where_clause' => $whereClause,
  2182. 'condition' => json_encode($conditionArray),
  2183. 'is_ajax' => ResponseRenderer::getInstance()->isAjax(),
  2184. 'js_conf' => $jsConf ?? '',
  2185. ]);
  2186. } elseif ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_NONE) {
  2187. $tableBodyHtml .= $this->template->render('display/results/checkbox_and_links', [
  2188. 'position' => self::POSITION_NONE,
  2189. 'has_checkbox' => ! empty($deleteUrl) && $displayParts['del_lnk'] !== self::KILL_PROCESS,
  2190. 'edit' => ['url' => $editUrl, 'string' => $editString, 'clause_is_unique' => $clauseIsUnique],
  2191. 'copy' => ['url' => $copyUrl, 'string' => $copyString],
  2192. 'delete' => ['url' => $deleteUrl, 'string' => $deleteString],
  2193. 'row_number' => $rowNumber,
  2194. 'where_clause' => $whereClause,
  2195. 'condition' => json_encode($conditionArray),
  2196. 'is_ajax' => ResponseRenderer::getInstance()->isAjax(),
  2197. 'js_conf' => $jsConf ?? '',
  2198. ]);
  2199. }
  2200. }
  2201. // 2. Displays the rows' values
  2202. if ($this->properties['mime_map'] === null) {
  2203. $this->setMimeMap();
  2204. }
  2205. $tableBodyHtml .= $this->getRowValues(
  2206. $dtResult,
  2207. $row,
  2208. $rowNumber,
  2209. $colOrder,
  2210. $map,
  2211. $gridEditClass,
  2212. $colVisib,
  2213. $urlSqlQuery,
  2214. $analyzedSqlResults
  2215. );
  2216. // 3. Displays the modify/delete links on the right if required
  2217. if (
  2218. ($displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE)
  2219. || ($displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE)
  2220. ) {
  2221. if (
  2222. ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_RIGHT)
  2223. || ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_BOTH)
  2224. ) {
  2225. $tableBodyHtml .= $this->template->render('display/results/checkbox_and_links', [
  2226. 'position' => self::POSITION_RIGHT,
  2227. 'has_checkbox' => ! empty($deleteUrl) && $displayParts['del_lnk'] !== self::KILL_PROCESS,
  2228. 'edit' => [
  2229. 'url' => $editUrl,
  2230. 'string' => $editString,
  2231. 'clause_is_unique' => $clauseIsUnique ?? true,
  2232. ],
  2233. 'copy' => ['url' => $copyUrl, 'string' => $copyString],
  2234. 'delete' => ['url' => $deleteUrl, 'string' => $deleteString],
  2235. 'row_number' => $rowNumber,
  2236. 'where_clause' => $whereClause ?? '',
  2237. 'condition' => json_encode($conditionArray ?? []),
  2238. 'is_ajax' => ResponseRenderer::getInstance()->isAjax(),
  2239. 'js_conf' => $jsConf ?? '',
  2240. ]);
  2241. }
  2242. }
  2243. $tableBodyHtml .= '</tr>';
  2244. $tableBodyHtml .= "\n";
  2245. $rowNumber++;
  2246. }
  2247. return $tableBodyHtml;
  2248. }
  2249. /**
  2250. * Sets the MIME details of the columns in the results set
  2251. */
  2252. private function setMimeMap(): void
  2253. {
  2254. /** @var FieldMetadata[] $fieldsMeta */
  2255. $fieldsMeta = $this->properties['fields_meta'];
  2256. $mediaTypeMap = [];
  2257. $added = [];
  2258. for ($currentColumn = 0; $currentColumn < $this->properties['fields_cnt']; ++$currentColumn) {
  2259. $meta = $fieldsMeta[$currentColumn];
  2260. $orgFullTableName = $this->properties['db'] . '.' . $meta->orgtable;
  2261. if (
  2262. ! $GLOBALS['cfgRelation']['commwork']
  2263. || ! $GLOBALS['cfgRelation']['mimework']
  2264. || ! $GLOBALS['cfg']['BrowseMIME']
  2265. || $_SESSION['tmpval']['hide_transformation']
  2266. || ! empty($added[$orgFullTableName])
  2267. ) {
  2268. continue;
  2269. }
  2270. $mediaTypeMap = array_merge(
  2271. $mediaTypeMap,
  2272. $this->transformations->getMime($this->properties['db'], $meta->orgtable, false, true) ?? []
  2273. );
  2274. $added[$orgFullTableName] = true;
  2275. }
  2276. // special browser transformation for some SHOW statements
  2277. if ($this->properties['is_show'] && ! $_SESSION['tmpval']['hide_transformation']) {
  2278. preg_match(
  2279. '@^SHOW[[:space:]]+(VARIABLES|(FULL[[:space:]]+)?'
  2280. . 'PROCESSLIST|STATUS|TABLE|GRANTS|CREATE|LOGS|DATABASES|FIELDS'
  2281. . ')@i',
  2282. $this->properties['sql_query'],
  2283. $which
  2284. );
  2285. if (isset($which[1])) {
  2286. $str = ' ' . strtoupper($which[1]);
  2287. $isShowProcessList = strpos($str, 'PROCESSLIST') > 0;
  2288. if ($isShowProcessList) {
  2289. $mediaTypeMap['..Info'] = [
  2290. 'mimetype' => 'Text_Plain',
  2291. 'transformation' => 'output/Text_Plain_Sql.php',
  2292. ];
  2293. }
  2294. $isShowCreateTable = preg_match('@CREATE[[:space:]]+TABLE@i', $this->properties['sql_query']);
  2295. if ($isShowCreateTable) {
  2296. $mediaTypeMap['..Create Table'] = [
  2297. 'mimetype' => 'Text_Plain',
  2298. 'transformation' => 'output/Text_Plain_Sql.php',
  2299. ];
  2300. }
  2301. }
  2302. }
  2303. $this->properties['mime_map'] = $mediaTypeMap;
  2304. }
  2305. /**
  2306. * Get the values for one data row
  2307. *
  2308. * @see getTableBody()
  2309. *
  2310. * @param int $dtResult the link id associated to the query
  2311. * which results have to be displayed
  2312. * @param array $row current row data
  2313. * @param int $rowNumber the index of current row
  2314. * @param array|false $colOrder the column order false when
  2315. * a property not found false
  2316. * when a property not found
  2317. * @param array $map the list of relations
  2318. * @param string $gridEditClass the class for all editable
  2319. * columns
  2320. * @param bool|array|string $colVisib column is visible(false);
  2321. * column isn't visible(string
  2322. * array)
  2323. * @param string $urlSqlQuery the analyzed sql query
  2324. * @param array $analyzedSqlResults analyzed sql results
  2325. *
  2326. * @return string html content
  2327. *
  2328. * @access private
  2329. */
  2330. private function getRowValues(
  2331. &$dtResult,
  2332. array $row,
  2333. $rowNumber,
  2334. $colOrder,
  2335. array $map,
  2336. $gridEditClass,
  2337. $colVisib,
  2338. $urlSqlQuery,
  2339. array $analyzedSqlResults
  2340. ) {
  2341. $rowValuesHtml = '';
  2342. // Following variable are needed for use in isset/empty or
  2343. // use with array indexes/safe use in foreach
  2344. $sqlQuery = $this->properties['sql_query'];
  2345. /** @var FieldMetadata[] $fieldsMeta */
  2346. $fieldsMeta = $this->properties['fields_meta'];
  2347. $highlightColumns = $this->properties['highlight_columns'];
  2348. $mediaTypeMap = $this->properties['mime_map'];
  2349. $rowInfo = $this->getRowInfoForSpecialLinks($row, $colOrder);
  2350. $whereClauseMap = $this->properties['whereClauseMap'];
  2351. $columnCount = $this->properties['fields_cnt'];
  2352. // Load SpecialSchemaLinks for all rows
  2353. $specialSchemaLinks = SpecialSchemaLinks::get();
  2354. for ($currentColumn = 0; $currentColumn < $columnCount; ++$currentColumn) {
  2355. // assign $i with appropriate column order
  2356. $i = is_array($colOrder) ? $colOrder[$currentColumn] : $currentColumn;
  2357. $meta = $fieldsMeta[$i];
  2358. $orgFullColName = $this->properties['db'] . '.' . $meta->orgtable . '.' . $meta->orgname;
  2359. $notNullClass = $meta->isNotNull() ? 'not_null' : '';
  2360. $relationClass = isset($map[$meta->name]) ? 'relation' : '';
  2361. $hideClass = is_array($colVisib) && isset($colVisib[$currentColumn]) && ! $colVisib[$currentColumn]
  2362. ? 'hide'
  2363. : '';
  2364. $gridEdit = $meta->orgtable != '' ? $gridEditClass : '';
  2365. // handle datetime-related class, for grid editing
  2366. $fieldTypeClass = $this->getClassForDateTimeRelatedFields($meta);
  2367. $isFieldTruncated = false;
  2368. // combine all the classes applicable to this column's value
  2369. $class = $this->getClassesForColumn($gridEdit, $notNullClass, $relationClass, $hideClass, $fieldTypeClass);
  2370. // See if this column should get highlight because it's used in the
  2371. // where-query.
  2372. $conditionField = isset($highlightColumns)
  2373. && (isset($highlightColumns[$meta->name])
  2374. || isset($highlightColumns[Util::backquote($meta->name)]));
  2375. // Wrap MIME-transformations. [MIME]
  2376. $transformationPlugin = null;
  2377. $transformOptions = [];
  2378. if ($GLOBALS['cfgRelation']['mimework'] && $GLOBALS['cfg']['BrowseMIME']) {
  2379. if (
  2380. isset($mediaTypeMap[$orgFullColName]['mimetype'])
  2381. && ! empty($mediaTypeMap[$orgFullColName]['transformation'])
  2382. ) {
  2383. $file = $mediaTypeMap[$orgFullColName]['transformation'];
  2384. $includeFile = 'libraries/classes/Plugins/Transformations/' . $file;
  2385. if (@file_exists(ROOT_PATH . $includeFile)) {
  2386. $className = $this->transformations->getClassName($includeFile);
  2387. if (class_exists($className)) {
  2388. $plugin = new $className();
  2389. if ($plugin instanceof TransformationsPlugin) {
  2390. $transformationPlugin = $plugin;
  2391. $transformOptions = $this->transformations->getOptions(
  2392. $mediaTypeMap[$orgFullColName]['transformation_options'] ?? ''
  2393. );
  2394. $meta->internalMediaType = str_replace(
  2395. '_',
  2396. '/',
  2397. $mediaTypeMap[$orgFullColName]['mimetype']
  2398. );
  2399. }
  2400. }
  2401. }
  2402. }
  2403. }
  2404. // Check whether the field needs to display with syntax highlighting
  2405. $dbLower = mb_strtolower($this->properties['db']);
  2406. $tblLower = mb_strtolower($meta->orgtable);
  2407. $nameLower = mb_strtolower($meta->orgname);
  2408. if (
  2409. ! empty($this->transformationInfo[$dbLower][$tblLower][$nameLower])
  2410. && isset($row[$i])
  2411. && (trim($row[$i]) != '')
  2412. && ! $_SESSION['tmpval']['hide_transformation']
  2413. ) {
  2414. /** @psalm-suppress UnresolvableInclude */
  2415. include_once ROOT_PATH . $this->transformationInfo[$dbLower][$tblLower][$nameLower][0];
  2416. $plugin = new $this->transformationInfo[$dbLower][$tblLower][$nameLower][1]();
  2417. if ($plugin instanceof TransformationsPlugin) {
  2418. $transformationPlugin = $plugin;
  2419. $transformOptions = $this->transformations->getOptions(
  2420. $mediaTypeMap[$orgFullColName]['transformation_options'] ?? ''
  2421. );
  2422. $orgTable = mb_strtolower($meta->orgtable);
  2423. $orgName = mb_strtolower($meta->orgname);
  2424. $meta->internalMediaType = str_replace(
  2425. '_',
  2426. '/',
  2427. $this->transformationInfo[$dbLower][$orgTable][$orgName][2]
  2428. );
  2429. }
  2430. }
  2431. // Check for the predefined fields need to show as link in schemas
  2432. if (! empty($specialSchemaLinks[$dbLower][$tblLower][$nameLower])) {
  2433. $linkingUrl = $this->getSpecialLinkUrl(
  2434. $specialSchemaLinks[$dbLower][$tblLower][$nameLower],
  2435. $row[$i],
  2436. $rowInfo
  2437. );
  2438. $transformationPlugin = new Text_Plain_Link();
  2439. $transformOptions = [
  2440. 0 => $linkingUrl,
  2441. 2 => true,
  2442. ];
  2443. $meta->internalMediaType = str_replace('_', '/', 'Text/Plain');
  2444. }
  2445. $expressions = [];
  2446. if (
  2447. isset($analyzedSqlResults['statement'])
  2448. && $analyzedSqlResults['statement'] instanceof SelectStatement
  2449. ) {
  2450. $expressions = $analyzedSqlResults['statement']->expr;
  2451. }
  2452. /**
  2453. * The result set can have columns from more than one table,
  2454. * this is why we have to check for the unique conditions
  2455. * related to this table; however getUniqueCondition() is
  2456. * costly and does not need to be called if we already know
  2457. * the conditions for the current table.
  2458. */
  2459. if (! isset($whereClauseMap[$rowNumber][$meta->orgtable])) {
  2460. $uniqueConditions = Util::getUniqueCondition(
  2461. $dtResult,
  2462. $this->properties['fields_cnt'],
  2463. $this->properties['fields_meta'],
  2464. $row,
  2465. false,
  2466. $meta->orgtable,
  2467. $expressions
  2468. );
  2469. $whereClauseMap[$rowNumber][$meta->orgtable] = $uniqueConditions[0];
  2470. }
  2471. $urlParams = [
  2472. 'db' => $this->properties['db'],
  2473. 'table' => $meta->orgtable,
  2474. 'where_clause_sign' => Core::signSqlQuery($whereClauseMap[$rowNumber][$meta->orgtable]),
  2475. 'where_clause' => $whereClauseMap[$rowNumber][$meta->orgtable],
  2476. 'transform_key' => $meta->orgname,
  2477. ];
  2478. if (! empty($sqlQuery)) {
  2479. $urlParams['sql_query'] = $urlSqlQuery;
  2480. }
  2481. $transformOptions['wrapper_link'] = Url::getCommon($urlParams);
  2482. $transformOptions['wrapper_params'] = $urlParams;
  2483. $displayParams = $this->properties['display_params'] ?? [];
  2484. // in some situations (issue 11406), numeric returns 1
  2485. // even for a string type
  2486. // for decimal numeric is returning 1
  2487. // have to improve logic
  2488. // Nullable text fields and text fields have the blob flag (issue 16896)
  2489. $isNumericAndNotBlob = $meta->isNumeric && ! $meta->isBlob;
  2490. if (
  2491. ($isNumericAndNotBlob && $meta->isNotType(FieldMetadata::TYPE_STRING))
  2492. || $meta->isType(FieldMetadata::TYPE_REAL)
  2493. ) {
  2494. // n u m e r i c
  2495. $displayParams['data'][$rowNumber][$i] = $this->getDataCellForNumericColumns(
  2496. $row[$i] === null ? null : (string) $row[$i],
  2497. $class,
  2498. $conditionField,
  2499. $meta,
  2500. $map,
  2501. $isFieldTruncated,
  2502. $analyzedSqlResults,
  2503. $transformationPlugin,
  2504. $transformOptions
  2505. );
  2506. } elseif ($meta->isMappedTypeGeometry) {
  2507. // g e o m e t r y
  2508. // Remove 'grid_edit' from $class as we do not allow to
  2509. // inline-edit geometry data.
  2510. $class = str_replace('grid_edit', '', $class);
  2511. $displayParams['data'][$rowNumber][$i] = $this->getDataCellForGeometryColumns(
  2512. $row[$i] === null ? null : (string) $row[$i],
  2513. $class,
  2514. $meta,
  2515. $map,
  2516. $urlParams,
  2517. $conditionField,
  2518. $transformationPlugin,
  2519. $transformOptions,
  2520. $analyzedSqlResults
  2521. );
  2522. } else {
  2523. // n o t n u m e r i c
  2524. $displayParams['data'][$rowNumber][$i] = $this->getDataCellForNonNumericColumns(
  2525. $row[$i] === null ? null : (string) $row[$i],
  2526. $class,
  2527. $meta,
  2528. $map,
  2529. $urlParams,
  2530. $conditionField,
  2531. $transformationPlugin,
  2532. $transformOptions,
  2533. $isFieldTruncated,
  2534. $analyzedSqlResults
  2535. );
  2536. }
  2537. // output stored cell
  2538. $rowValuesHtml .= $displayParams['data'][$rowNumber][$i];
  2539. if (isset($displayParams['rowdata'][$i][$rowNumber])) {
  2540. $displayParams['rowdata'][$i][$rowNumber] .= $displayParams['data'][$rowNumber][$i];
  2541. } else {
  2542. $displayParams['rowdata'][$i][$rowNumber] = $displayParams['data'][$rowNumber][$i];
  2543. }
  2544. $this->properties['display_params'] = $displayParams;
  2545. }
  2546. return $rowValuesHtml;
  2547. }
  2548. /**
  2549. * Get link for display special schema links
  2550. *
  2551. * @param array<string,array<int,array<string,string>>|string> $linkRelations
  2552. * @param string $columnValue column value
  2553. * @param array $rowInfo information about row
  2554. * @phpstan-param array{
  2555. * 'link_param': string,
  2556. * 'link_dependancy_params'?: array<
  2557. * int,
  2558. * array{'param_info': string, 'column_name': string}
  2559. * >,
  2560. * 'default_page': string
  2561. * } $linkRelations
  2562. *
  2563. * @return string generated link
  2564. */
  2565. private function getSpecialLinkUrl(
  2566. array $linkRelations,
  2567. $columnValue,
  2568. array $rowInfo
  2569. ) {
  2570. $linkingUrlParams = [];
  2571. $linkingUrlParams[$linkRelations['link_param']] = $columnValue;
  2572. $divider = strpos($linkRelations['default_page'], '?') ? '&' : '?';
  2573. if (empty($linkRelations['link_dependancy_params'])) {
  2574. return $linkRelations['default_page']
  2575. . Url::getCommonRaw($linkingUrlParams, $divider);
  2576. }
  2577. foreach ($linkRelations['link_dependancy_params'] as $new_param) {
  2578. $columnName = mb_strtolower($new_param['column_name']);
  2579. // If there is a value for this column name in the rowInfo provided
  2580. if (isset($rowInfo[$columnName])) {
  2581. $urlParameterName = $new_param['param_info'];
  2582. $linkingUrlParams[$urlParameterName] = $rowInfo[$columnName];
  2583. }
  2584. // Special case 1 - when executing routines, according
  2585. // to the type of the routine, url param changes
  2586. if (empty($rowInfo['routine_type'])) {
  2587. continue;
  2588. }
  2589. }
  2590. return $linkRelations['default_page']
  2591. . Url::getCommonRaw($linkingUrlParams, $divider);
  2592. }
  2593. /**
  2594. * Prepare row information for display special links
  2595. *
  2596. * @param array $row current row data
  2597. * @param array|bool $colOrder the column order
  2598. *
  2599. * @return array associative array with column nama -> value
  2600. */
  2601. private function getRowInfoForSpecialLinks(array $row, $colOrder)
  2602. {
  2603. $rowInfo = [];
  2604. /** @var FieldMetadata[] $fieldsMeta */
  2605. $fieldsMeta = $this->properties['fields_meta'];
  2606. for ($n = 0; $n < $this->properties['fields_cnt']; ++$n) {
  2607. $m = is_array($colOrder) ? $colOrder[$n] : $n;
  2608. $rowInfo[mb_strtolower($fieldsMeta[$m]->orgname)] = $row[$m];
  2609. }
  2610. return $rowInfo;
  2611. }
  2612. /**
  2613. * Get url sql query without conditions to shorten URLs
  2614. *
  2615. * @see getTableBody()
  2616. *
  2617. * @param array $analyzedSqlResults analyzed sql results
  2618. *
  2619. * @return string analyzed sql query
  2620. *
  2621. * @access private
  2622. */
  2623. private function getUrlSqlQuery(array $analyzedSqlResults)
  2624. {
  2625. if (($analyzedSqlResults['querytype'] !== 'SELECT') || (mb_strlen($this->properties['sql_query']) < 200)) {
  2626. return $this->properties['sql_query'];
  2627. }
  2628. $query = 'SELECT ' . Query::getClause(
  2629. $analyzedSqlResults['statement'],
  2630. $analyzedSqlResults['parser']->list,
  2631. 'SELECT'
  2632. );
  2633. $fromClause = Query::getClause($analyzedSqlResults['statement'], $analyzedSqlResults['parser']->list, 'FROM');
  2634. if (! empty($fromClause)) {
  2635. $query .= ' FROM ' . $fromClause;
  2636. }
  2637. return $query;
  2638. }
  2639. /**
  2640. * Get column order and column visibility
  2641. *
  2642. * @see getTableBody()
  2643. *
  2644. * @param array $analyzedSqlResults analyzed sql results
  2645. *
  2646. * @return array 2 element array - $col_order, $col_visib
  2647. *
  2648. * @access private
  2649. */
  2650. private function getColumnParams(array $analyzedSqlResults)
  2651. {
  2652. if ($this->isSelect($analyzedSqlResults)) {
  2653. $pmatable = new Table($this->properties['table'], $this->properties['db']);
  2654. $colOrder = $pmatable->getUiProp(Table::PROP_COLUMN_ORDER);
  2655. /* Validate the value */
  2656. if ($colOrder !== false) {
  2657. $fieldsCount = $this->properties['fields_cnt'];
  2658. foreach ($colOrder as $value) {
  2659. if ($value < $fieldsCount) {
  2660. continue;
  2661. }
  2662. $pmatable->removeUiProp(Table::PROP_COLUMN_ORDER);
  2663. $fieldsCount = false;
  2664. }
  2665. }
  2666. $colVisib = $pmatable->getUiProp(Table::PROP_COLUMN_VISIB);
  2667. } else {
  2668. $colOrder = false;
  2669. $colVisib = false;
  2670. }
  2671. return [
  2672. $colOrder,
  2673. $colVisib,
  2674. ];
  2675. }
  2676. /**
  2677. * Get HTML for repeating headers
  2678. *
  2679. * @see getTableBody()
  2680. *
  2681. * @param array $displayParams holds various display info
  2682. *
  2683. * @return string html content
  2684. *
  2685. * @access private
  2686. */
  2687. private function getRepeatingHeaders(
  2688. array $displayParams
  2689. ) {
  2690. $headerHtml = '<tr>' . "\n";
  2691. if ($displayParams['emptypre'] > 0) {
  2692. $headerHtml .= ' <th colspan="'
  2693. . $displayParams['emptypre'] . '">'
  2694. . "\n" . ' &nbsp;</th>' . "\n";
  2695. } elseif ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_NONE) {
  2696. $headerHtml .= ' <th></th>' . "\n";
  2697. }
  2698. foreach ($displayParams['desc'] as $val) {
  2699. $headerHtml .= $val;
  2700. }
  2701. if ($displayParams['emptyafter'] > 0) {
  2702. $headerHtml .= ' <th colspan="' . $displayParams['emptyafter']
  2703. . '">'
  2704. . "\n" . ' &nbsp;</th>' . "\n";
  2705. }
  2706. $headerHtml .= '</tr>' . "\n";
  2707. return $headerHtml;
  2708. }
  2709. /**
  2710. * Get modified links
  2711. *
  2712. * @see getTableBody()
  2713. *
  2714. * @param string $whereClause the where clause of the sql
  2715. * @param bool $clauseIsUnique the unique condition of clause
  2716. * @param string $urlSqlQuery the analyzed sql query
  2717. *
  2718. * @return array<int,string> 5 element array - $edit_url, $copy_url,
  2719. * $edit_str, $copy_str
  2720. *
  2721. * @access private
  2722. */
  2723. private function getModifiedLinks(
  2724. $whereClause,
  2725. $clauseIsUnique,
  2726. $urlSqlQuery
  2727. ) {
  2728. $urlParams = [
  2729. 'db' => $this->properties['db'],
  2730. 'table' => $this->properties['table'],
  2731. 'where_clause' => $whereClause,
  2732. 'clause_is_unique' => $clauseIsUnique,
  2733. 'sql_query' => $urlSqlQuery,
  2734. 'goto' => Url::getFromRoute('/sql'),
  2735. ];
  2736. $editUrl = Url::getFromRoute(
  2737. '/table/change',
  2738. $urlParams + ['default_action' => 'update']
  2739. );
  2740. $copyUrl = Url::getFromRoute(
  2741. '/table/change',
  2742. $urlParams + ['default_action' => 'insert']
  2743. );
  2744. $editStr = $this->getActionLinkContent(
  2745. 'b_edit',
  2746. __('Edit')
  2747. );
  2748. $copyStr = $this->getActionLinkContent(
  2749. 'b_insrow',
  2750. __('Copy')
  2751. );
  2752. return [
  2753. $editUrl,
  2754. $copyUrl,
  2755. $editStr,
  2756. $copyStr,
  2757. ];
  2758. }
  2759. /**
  2760. * Get delete and kill links
  2761. *
  2762. * @see getTableBody()
  2763. *
  2764. * @param string $whereClause the where clause of the sql
  2765. * @param bool $clauseIsUnique the unique condition of clause
  2766. * @param string $urlSqlQuery the analyzed sql query
  2767. * @param string $deleteLink the delete link of current row
  2768. * @param array $row the current row
  2769. *
  2770. * @return array 3 element array
  2771. * $del_url, $del_str, $js_conf
  2772. *
  2773. * @access private
  2774. */
  2775. private function getDeleteAndKillLinks(
  2776. $whereClause,
  2777. $clauseIsUnique,
  2778. $urlSqlQuery,
  2779. $deleteLink,
  2780. array $row
  2781. ) {
  2782. global $dbi;
  2783. $goto = $this->properties['goto'];
  2784. if ($deleteLink === self::DELETE_ROW) { // delete row case
  2785. $urlParams = [
  2786. 'db' => $this->properties['db'],
  2787. 'table' => $this->properties['table'],
  2788. 'sql_query' => $urlSqlQuery,
  2789. 'message_to_show' => __('The row has been deleted.'),
  2790. 'goto' => empty($goto) ? Url::getFromRoute('/table/sql') : $goto,
  2791. ];
  2792. $linkGoto = Url::getFromRoute('/sql', $urlParams);
  2793. $deleteQuery = 'DELETE FROM '
  2794. . Util::backquote($this->properties['table'])
  2795. . ' WHERE ' . $whereClause .
  2796. ($clauseIsUnique ? '' : ' LIMIT 1');
  2797. $urlParams = [
  2798. 'db' => $this->properties['db'],
  2799. 'table' => $this->properties['table'],
  2800. 'sql_query' => $deleteQuery,
  2801. 'message_to_show' => __('The row has been deleted.'),
  2802. 'goto' => $linkGoto,
  2803. ];
  2804. $deleteUrl = Url::getFromRoute('/sql', $urlParams);
  2805. $jsConf = 'DELETE FROM ' . Sanitize::jsFormat($this->properties['table'])
  2806. . ' WHERE ' . Sanitize::jsFormat($whereClause, false)
  2807. . ($clauseIsUnique ? '' : ' LIMIT 1');
  2808. $deleteString = $this->getActionLinkContent('b_drop', __('Delete'));
  2809. } elseif ($deleteLink === self::KILL_PROCESS) { // kill process case
  2810. $urlParams = [
  2811. 'db' => $this->properties['db'],
  2812. 'table' => $this->properties['table'],
  2813. 'sql_query' => $urlSqlQuery,
  2814. 'goto' => Url::getFromRoute('/'),
  2815. ];
  2816. $linkGoto = Url::getFromRoute('/sql', $urlParams);
  2817. $kill = $dbi->getKillQuery((int) $row[0]);
  2818. $urlParams = [
  2819. 'db' => 'mysql',
  2820. 'sql_query' => $kill,
  2821. 'goto' => $linkGoto,
  2822. ];
  2823. $deleteUrl = Url::getFromRoute('/sql', $urlParams);
  2824. $jsConf = $kill;
  2825. $deleteString = Generator::getIcon(
  2826. 'b_drop',
  2827. __('Kill')
  2828. );
  2829. } else {
  2830. $deleteUrl = $deleteString = $jsConf = null;
  2831. }
  2832. return [
  2833. $deleteUrl,
  2834. $deleteString,
  2835. $jsConf,
  2836. ];
  2837. }
  2838. /**
  2839. * Get content inside the table row action links (Edit/Copy/Delete)
  2840. *
  2841. * @see getModifiedLinks(), getDeleteAndKillLinks()
  2842. *
  2843. * @param string $icon The name of the file to get
  2844. * @param string $displayText The text displaying after the image icon
  2845. *
  2846. * @return string
  2847. *
  2848. * @access private
  2849. */
  2850. private function getActionLinkContent($icon, $displayText)
  2851. {
  2852. $linkContent = '';
  2853. if (
  2854. isset($GLOBALS['cfg']['RowActionType'])
  2855. && $GLOBALS['cfg']['RowActionType'] === self::ACTION_LINK_CONTENT_ICONS
  2856. ) {
  2857. $linkContent .= '<span class="text-nowrap">'
  2858. . Generator::getImage($icon, $displayText)
  2859. . '</span>';
  2860. } elseif (
  2861. isset($GLOBALS['cfg']['RowActionType'])
  2862. && $GLOBALS['cfg']['RowActionType'] === self::ACTION_LINK_CONTENT_TEXT
  2863. ) {
  2864. $linkContent .= '<span class="text-nowrap">' . $displayText . '</span>';
  2865. } else {
  2866. $linkContent .= Generator::getIcon($icon, $displayText);
  2867. }
  2868. return $linkContent;
  2869. }
  2870. /**
  2871. * Get the combined classes for a column
  2872. *
  2873. * @see getTableBody()
  2874. *
  2875. * @param string $gridEditClass the class for all editable columns
  2876. * @param string $notNullClass the class for not null columns
  2877. * @param string $relationClass the class for relations in a column
  2878. * @param string $hideClass the class for visibility of a column
  2879. * @param string $fieldTypeClass the class related to type of the field
  2880. *
  2881. * @return string the combined classes
  2882. *
  2883. * @access private
  2884. */
  2885. private function getClassesForColumn(
  2886. string $gridEditClass,
  2887. string $notNullClass,
  2888. string $relationClass,
  2889. string $hideClass,
  2890. string $fieldTypeClass
  2891. ) {
  2892. return 'data ' . $gridEditClass . ' ' . $notNullClass . ' '
  2893. . $relationClass . ' ' . $hideClass . ' ' . $fieldTypeClass;
  2894. }
  2895. /**
  2896. * Get class for datetime related fields
  2897. *
  2898. * @see getTableBody()
  2899. *
  2900. * @param FieldMetadata $meta the type of the column field
  2901. *
  2902. * @return string the class for the column
  2903. *
  2904. * @access private
  2905. */
  2906. private function getClassForDateTimeRelatedFields(FieldMetadata $meta): string
  2907. {
  2908. $fieldTypeClass = '';
  2909. if ($meta->isMappedTypeTimestamp || $meta->isType(FieldMetadata::TYPE_DATETIME)) {
  2910. $fieldTypeClass = 'datetimefield';
  2911. } elseif ($meta->isType(FieldMetadata::TYPE_DATE)) {
  2912. $fieldTypeClass = 'datefield';
  2913. } elseif ($meta->isType(FieldMetadata::TYPE_TIME)) {
  2914. $fieldTypeClass = 'timefield';
  2915. } elseif ($meta->isType(FieldMetadata::TYPE_STRING)) {
  2916. $fieldTypeClass = 'text';
  2917. }
  2918. return $fieldTypeClass;
  2919. }
  2920. /**
  2921. * Prepare data cell for numeric type fields
  2922. *
  2923. * @see getTableBody()
  2924. *
  2925. * @param string|null $column the column's value
  2926. * @param string $class the html class for column
  2927. * @param bool $conditionField the column should highlighted or not
  2928. * @param FieldMetadata $meta the meta-information about this field
  2929. * @param array $map the list of relations
  2930. * @param bool $isFieldTruncated the condition for blob data replacements
  2931. * @param array $analyzedSqlResults the analyzed query
  2932. * @param array $transformOptions the transformation parameters
  2933. *
  2934. * @return string the prepared cell, html content
  2935. */
  2936. private function getDataCellForNumericColumns(
  2937. ?string $column,
  2938. $class,
  2939. $conditionField,
  2940. FieldMetadata $meta,
  2941. array $map,
  2942. $isFieldTruncated,
  2943. array $analyzedSqlResults,
  2944. ?TransformationsPlugin $transformationPlugin,
  2945. array $transformOptions
  2946. ) {
  2947. if (! isset($column)) {
  2948. $cell = $this->buildNullDisplay('text-end ' . $class, $conditionField, $meta, '');
  2949. } elseif ($column != '') {
  2950. $nowrap = ' text-nowrap';
  2951. $whereComparison = ' = ' . $column;
  2952. $cell = $this->getRowData(
  2953. 'text-end ' . $class,
  2954. $conditionField,
  2955. $analyzedSqlResults,
  2956. $meta,
  2957. $map,
  2958. $column,
  2959. $column,
  2960. $transformationPlugin,
  2961. $nowrap,
  2962. $whereComparison,
  2963. $transformOptions,
  2964. $isFieldTruncated,
  2965. ''
  2966. );
  2967. } else {
  2968. $cell = $this->buildEmptyDisplay('text-end ' . $class, $conditionField, $meta, '');
  2969. }
  2970. return $cell;
  2971. }
  2972. /**
  2973. * Get data cell for geometry type fields
  2974. *
  2975. * @see getTableBody()
  2976. *
  2977. * @param string|null $column the relevant column in data row
  2978. * @param string $class the html class for column
  2979. * @param FieldMetadata $meta the meta-information about this field
  2980. * @param array $map the list of relations
  2981. * @param array $urlParams the parameters for generate url
  2982. * @param bool $conditionField the column should highlighted or not
  2983. * @param array $transformOptions the transformation parameters
  2984. * @param array $analyzedSqlResults the analyzed query
  2985. *
  2986. * @return string the prepared data cell, html content
  2987. */
  2988. private function getDataCellForGeometryColumns(
  2989. ?string $column,
  2990. $class,
  2991. FieldMetadata $meta,
  2992. array $map,
  2993. array $urlParams,
  2994. $conditionField,
  2995. ?TransformationsPlugin $transformationPlugin,
  2996. $transformOptions,
  2997. array $analyzedSqlResults
  2998. ) {
  2999. if (! isset($column)) {
  3000. return $this->buildNullDisplay($class, $conditionField, $meta);
  3001. }
  3002. if ($column == '') {
  3003. return $this->buildEmptyDisplay($class, $conditionField, $meta);
  3004. }
  3005. // Display as [GEOMETRY - (size)]
  3006. if ($_SESSION['tmpval']['geoOption'] === self::GEOMETRY_DISP_GEOM) {
  3007. $geometryText = $this->handleNonPrintableContents(
  3008. 'GEOMETRY',
  3009. $column,
  3010. $transformationPlugin,
  3011. $transformOptions,
  3012. $meta,
  3013. $urlParams
  3014. );
  3015. return $this->buildValueDisplay($class, $conditionField, $geometryText);
  3016. }
  3017. if ($_SESSION['tmpval']['geoOption'] === self::GEOMETRY_DISP_WKT) {
  3018. // Prepare in Well Known Text(WKT) format.
  3019. $whereComparison = ' = ' . $column;
  3020. // Convert to WKT format
  3021. $wktval = Gis::convertToWellKnownText($column);
  3022. [
  3023. $isFieldTruncated,
  3024. $displayedColumn,
  3025. // skip 3rd param
  3026. ] = $this->getPartialText($wktval);
  3027. return $this->getRowData(
  3028. $class,
  3029. $conditionField,
  3030. $analyzedSqlResults,
  3031. $meta,
  3032. $map,
  3033. $wktval,
  3034. $displayedColumn,
  3035. $transformationPlugin,
  3036. '',
  3037. $whereComparison,
  3038. $transformOptions,
  3039. $isFieldTruncated,
  3040. ''
  3041. );
  3042. }
  3043. // Prepare in Well Known Binary (WKB) format.
  3044. if ($_SESSION['tmpval']['display_binary']) {
  3045. $whereComparison = ' = ' . $column;
  3046. $wkbval = substr(bin2hex($column), 8);
  3047. [
  3048. $isFieldTruncated,
  3049. $displayedColumn,
  3050. // skip 3rd param
  3051. ] = $this->getPartialText($wkbval);
  3052. return $this->getRowData(
  3053. $class,
  3054. $conditionField,
  3055. $analyzedSqlResults,
  3056. $meta,
  3057. $map,
  3058. $wkbval,
  3059. $displayedColumn,
  3060. $transformationPlugin,
  3061. '',
  3062. $whereComparison,
  3063. $transformOptions,
  3064. $isFieldTruncated,
  3065. ''
  3066. );
  3067. }
  3068. $wkbval = $this->handleNonPrintableContents(
  3069. 'BINARY',
  3070. $column,
  3071. $transformationPlugin,
  3072. $transformOptions,
  3073. $meta,
  3074. $urlParams
  3075. );
  3076. return $this->buildValueDisplay($class, $conditionField, $wkbval);
  3077. }
  3078. /**
  3079. * Get data cell for non numeric type fields
  3080. *
  3081. * @see getTableBody()
  3082. *
  3083. * @param string|null $column the relevant column in data row
  3084. * @param string $class the html class for column
  3085. * @param FieldMetadata $meta the meta-information about the field
  3086. * @param array $map the list of relations
  3087. * @param array $urlParams the parameters for generate url
  3088. * @param bool $conditionField the column should highlighted or not
  3089. * @param array $transformOptions the transformation parameters
  3090. * @param bool $isFieldTruncated is data truncated due to LimitChars
  3091. * @param array $analyzedSqlResults the analyzed query
  3092. *
  3093. * @return string the prepared data cell, html content
  3094. */
  3095. private function getDataCellForNonNumericColumns(
  3096. ?string $column,
  3097. $class,
  3098. FieldMetadata $meta,
  3099. array $map,
  3100. array $urlParams,
  3101. $conditionField,
  3102. ?TransformationsPlugin $transformationPlugin,
  3103. $transformOptions,
  3104. $isFieldTruncated,
  3105. array $analyzedSqlResults
  3106. ) {
  3107. global $dbi;
  3108. $originalLength = 0;
  3109. $isAnalyse = $this->properties['is_analyse'];
  3110. $bIsText = isset($transformationPlugin) && ! str_contains($transformationPlugin->getMIMEType(), 'Text');
  3111. // disable inline grid editing
  3112. // if binary fields are protected
  3113. // or transformation plugin is of non text type
  3114. // such as image
  3115. $isTypeBlob = $meta->isType(FieldMetadata::TYPE_BLOB);
  3116. $cfgProtectBinary = $GLOBALS['cfg']['ProtectBinary'];
  3117. if (
  3118. ($meta->isBinary()
  3119. && (
  3120. $cfgProtectBinary === 'all'
  3121. || ($cfgProtectBinary === 'noblob' && ! $isTypeBlob)
  3122. || ($cfgProtectBinary === 'blob' && $isTypeBlob)
  3123. )
  3124. ) || $bIsText
  3125. ) {
  3126. $class = str_replace('grid_edit', '', $class);
  3127. }
  3128. if (! isset($column)) {
  3129. return $this->buildNullDisplay($class, $conditionField, $meta);
  3130. }
  3131. if ($column == '') {
  3132. return $this->buildEmptyDisplay($class, $conditionField, $meta);
  3133. }
  3134. // Cut all fields to $GLOBALS['cfg']['LimitChars']
  3135. // (unless it's a link-type transformation or binary)
  3136. $originalDataForWhereClause = $column;
  3137. $displayedColumn = $column;
  3138. if (
  3139. ! (isset($transformationPlugin)
  3140. && str_contains($transformationPlugin->getName(), 'Link'))
  3141. && ! $meta->isBinary()
  3142. ) {
  3143. [
  3144. $isFieldTruncated,
  3145. $column,
  3146. $originalLength,
  3147. ] = $this->getPartialText($column);
  3148. }
  3149. $formatted = false;
  3150. if ($meta->isMappedTypeBit) {
  3151. $displayedColumn = Util::printableBitValue((int) $displayedColumn, (int) $meta->length);
  3152. // some results of PROCEDURE ANALYSE() are reported as
  3153. // being BINARY but they are quite readable,
  3154. // so don't treat them as BINARY
  3155. } elseif ($meta->isBinary() && ! (isset($isAnalyse) && $isAnalyse)) {
  3156. // we show the BINARY or BLOB message and field's size
  3157. // (or maybe use a transformation)
  3158. $binaryOrBlob = 'BLOB';
  3159. if ($meta->isType(FieldMetadata::TYPE_STRING)) {
  3160. $binaryOrBlob = 'BINARY';
  3161. }
  3162. $displayedColumn = $this->handleNonPrintableContents(
  3163. $binaryOrBlob,
  3164. $displayedColumn,
  3165. $transformationPlugin,
  3166. $transformOptions,
  3167. $meta,
  3168. $urlParams,
  3169. $isFieldTruncated
  3170. );
  3171. $class = $this->addClass(
  3172. $class,
  3173. $conditionField,
  3174. $meta,
  3175. '',
  3176. $isFieldTruncated,
  3177. isset($transformationPlugin)
  3178. );
  3179. $result = strip_tags($column);
  3180. // disable inline grid editing
  3181. // if binary or blob data is not shown
  3182. if (stripos($result, $binaryOrBlob) !== false) {
  3183. $class = str_replace('grid_edit', '', $class);
  3184. }
  3185. $formatted = true;
  3186. }
  3187. if ($formatted) {
  3188. return $this->buildValueDisplay($class, $conditionField, $displayedColumn);
  3189. }
  3190. // transform functions may enable no-wrapping:
  3191. $boolNoWrap = isset($transformationPlugin)
  3192. && $transformationPlugin->applyTransformationNoWrap($transformOptions);
  3193. // do not wrap if date field type or if no-wrapping enabled by transform functions
  3194. // otherwise, preserve whitespaces and wrap
  3195. $nowrap = $meta->isDateTimeType() || $boolNoWrap ? 'text-nowrap' : 'pre_wrap';
  3196. $whereComparison = ' = \''
  3197. . $dbi->escapeString($originalDataForWhereClause)
  3198. . '\'';
  3199. return $this->getRowData(
  3200. $class,
  3201. $conditionField,
  3202. $analyzedSqlResults,
  3203. $meta,
  3204. $map,
  3205. $column,
  3206. $displayedColumn,
  3207. $transformationPlugin,
  3208. $nowrap,
  3209. $whereComparison,
  3210. $transformOptions,
  3211. $isFieldTruncated,
  3212. $originalLength
  3213. );
  3214. }
  3215. /**
  3216. * Checks the posted options for viewing query results
  3217. * and sets appropriate values in the session.
  3218. *
  3219. * @todo make maximum remembered queries configurable
  3220. * @todo move/split into SQL class!?
  3221. * @todo currently this is called twice unnecessary
  3222. * @todo ignore LIMIT and ORDER in query!?
  3223. * @access public
  3224. */
  3225. public function setConfigParamsForDisplayTable(): void
  3226. {
  3227. $sqlMd5 = md5($this->properties['server'] . $this->properties['db'] . $this->properties['sql_query']);
  3228. $query = [];
  3229. if (isset($_SESSION['tmpval']['query'][$sqlMd5])) {
  3230. $query = $_SESSION['tmpval']['query'][$sqlMd5];
  3231. }
  3232. $query['sql'] = $this->properties['sql_query'];
  3233. if (empty($query['repeat_cells'])) {
  3234. $query['repeat_cells'] = $GLOBALS['cfg']['RepeatCells'];
  3235. }
  3236. // The value can also be from _GET as described on issue #16146 when sorting results
  3237. $sessionMaxRows = $_GET['session_max_rows'] ?? $_POST['session_max_rows'] ?? '';
  3238. if (isset($sessionMaxRows) && is_numeric($sessionMaxRows)) {
  3239. $query['max_rows'] = (int) $sessionMaxRows;
  3240. unset($_GET['session_max_rows'], $_POST['session_max_rows']);
  3241. } elseif ($sessionMaxRows === self::ALL_ROWS) {
  3242. $query['max_rows'] = self::ALL_ROWS;
  3243. unset($_GET['session_max_rows'], $_POST['session_max_rows']);
  3244. } elseif (empty($query['max_rows'])) {
  3245. $query['max_rows'] = intval($GLOBALS['cfg']['MaxRows']);
  3246. }
  3247. if (isset($_REQUEST['pos']) && is_numeric($_REQUEST['pos'])) {
  3248. $query['pos'] = (int) $_REQUEST['pos'];
  3249. unset($_REQUEST['pos']);
  3250. } elseif (empty($query['pos'])) {
  3251. $query['pos'] = 0;
  3252. }
  3253. if (
  3254. isset($_REQUEST['pftext']) && in_array(
  3255. $_REQUEST['pftext'],
  3256. [self::DISPLAY_PARTIAL_TEXT, self::DISPLAY_FULL_TEXT]
  3257. )
  3258. ) {
  3259. $query['pftext'] = $_REQUEST['pftext'];
  3260. unset($_REQUEST['pftext']);
  3261. } elseif (empty($query['pftext'])) {
  3262. $query['pftext'] = self::DISPLAY_PARTIAL_TEXT;
  3263. }
  3264. if (
  3265. isset($_REQUEST['relational_display']) && in_array(
  3266. $_REQUEST['relational_display'],
  3267. [self::RELATIONAL_KEY, self::RELATIONAL_DISPLAY_COLUMN]
  3268. )
  3269. ) {
  3270. $query['relational_display'] = $_REQUEST['relational_display'];
  3271. unset($_REQUEST['relational_display']);
  3272. } elseif (empty($query['relational_display'])) {
  3273. // The current session value has priority over a
  3274. // change via Settings; this change will be apparent
  3275. // starting from the next session
  3276. $query['relational_display'] = $GLOBALS['cfg']['RelationalDisplay'];
  3277. }
  3278. if (
  3279. isset($_REQUEST['geoOption']) && in_array(
  3280. $_REQUEST['geoOption'],
  3281. [self::GEOMETRY_DISP_WKT, self::GEOMETRY_DISP_WKB, self::GEOMETRY_DISP_GEOM]
  3282. )
  3283. ) {
  3284. $query['geoOption'] = $_REQUEST['geoOption'];
  3285. unset($_REQUEST['geoOption']);
  3286. } elseif (empty($query['geoOption'])) {
  3287. $query['geoOption'] = self::GEOMETRY_DISP_GEOM;
  3288. }
  3289. if (isset($_REQUEST['display_binary'])) {
  3290. $query['display_binary'] = true;
  3291. unset($_REQUEST['display_binary']);
  3292. } elseif (isset($_REQUEST['display_options_form'])) {
  3293. // we know that the checkbox was unchecked
  3294. unset($query['display_binary']);
  3295. } elseif (! isset($_REQUEST['full_text_button'])) {
  3296. // selected by default because some operations like OPTIMIZE TABLE
  3297. // and all queries involving functions return "binary" contents,
  3298. // according to low-level field flags
  3299. $query['display_binary'] = true;
  3300. }
  3301. if (isset($_REQUEST['display_blob'])) {
  3302. $query['display_blob'] = true;
  3303. unset($_REQUEST['display_blob']);
  3304. } elseif (isset($_REQUEST['display_options_form'])) {
  3305. // we know that the checkbox was unchecked
  3306. unset($query['display_blob']);
  3307. }
  3308. if (isset($_REQUEST['hide_transformation'])) {
  3309. $query['hide_transformation'] = true;
  3310. unset($_REQUEST['hide_transformation']);
  3311. } elseif (isset($_REQUEST['display_options_form'])) {
  3312. // we know that the checkbox was unchecked
  3313. unset($query['hide_transformation']);
  3314. }
  3315. // move current query to the last position, to be removed last
  3316. // so only least executed query will be removed if maximum remembered
  3317. // queries limit is reached
  3318. unset($_SESSION['tmpval']['query'][$sqlMd5]);
  3319. $_SESSION['tmpval']['query'][$sqlMd5] = $query;
  3320. // do not exceed a maximum number of queries to remember
  3321. if (count($_SESSION['tmpval']['query']) > 10) {
  3322. array_shift($_SESSION['tmpval']['query']);
  3323. //echo 'deleting one element ...';
  3324. }
  3325. // populate query configuration
  3326. $_SESSION['tmpval']['pftext'] = $query['pftext'];
  3327. $_SESSION['tmpval']['relational_display'] = $query['relational_display'];
  3328. $_SESSION['tmpval']['geoOption'] = $query['geoOption'];
  3329. $_SESSION['tmpval']['display_binary'] = isset($query['display_binary']);
  3330. $_SESSION['tmpval']['display_blob'] = isset($query['display_blob']);
  3331. $_SESSION['tmpval']['hide_transformation'] = isset($query['hide_transformation']);
  3332. $_SESSION['tmpval']['pos'] = $query['pos'];
  3333. $_SESSION['tmpval']['max_rows'] = $query['max_rows'];
  3334. $_SESSION['tmpval']['repeat_cells'] = $query['repeat_cells'];
  3335. }
  3336. /**
  3337. * Prepare a table of results returned by a SQL query.
  3338. *
  3339. * @param int $dtResult the link id associated to the query
  3340. * which results have to be displayed
  3341. * @param array $displayParts the parts to display
  3342. * @param array $analyzedSqlResults analyzed sql results
  3343. * @param bool $isLimitedDisplay With limited operations or not
  3344. *
  3345. * @return string Generated HTML content for resulted table
  3346. *
  3347. * @access public
  3348. */
  3349. public function getTable(
  3350. &$dtResult,
  3351. array &$displayParts,
  3352. array $analyzedSqlResults,
  3353. $isLimitedDisplay = false
  3354. ) {
  3355. // The statement this table is built for.
  3356. if (isset($analyzedSqlResults['statement'])) {
  3357. /** @var SelectStatement $statement */
  3358. $statement = $analyzedSqlResults['statement'];
  3359. } else {
  3360. $statement = null;
  3361. }
  3362. // Following variable are needed for use in isset/empty or
  3363. // use with array indexes/safe use in foreach
  3364. /** @var FieldMetadata[] $fieldsMeta */
  3365. $fieldsMeta = $this->properties['fields_meta'];
  3366. $showTable = $this->properties['showtable'];
  3367. $printView = $this->properties['printview'];
  3368. /**
  3369. * @todo move this to a central place
  3370. * @todo for other future table types
  3371. */
  3372. $isInnodb = (isset($showTable['Type'])
  3373. && $showTable['Type'] === self::TABLE_TYPE_INNO_DB);
  3374. if ($isInnodb && Sql::isJustBrowsing($analyzedSqlResults, true)) {
  3375. $preCount = '~';
  3376. $afterCount = Generator::showHint(
  3377. Sanitize::sanitizeMessage(
  3378. __('May be approximate. See [doc@faq3-11]FAQ 3.11[/doc].')
  3379. )
  3380. );
  3381. } else {
  3382. $preCount = '';
  3383. $afterCount = '';
  3384. }
  3385. // 1. ----- Prepares the work -----
  3386. // 1.1 Gets the information about which functionalities should be
  3387. // displayed
  3388. [
  3389. $displayParts,
  3390. $total,
  3391. ] = $this->setDisplayPartsAndTotal($displayParts);
  3392. // 1.2 Defines offsets for the next and previous pages
  3393. $posNext = 0;
  3394. $posPrev = 0;
  3395. if ($displayParts['nav_bar'] == '1') {
  3396. [$posNext, $posPrev] = $this->getOffsets();
  3397. }
  3398. // 1.3 Extract sorting expressions.
  3399. // we need $sort_expression and $sort_expression_nodirection
  3400. // even if there are many table references
  3401. $sortExpression = [];
  3402. $sortExpressionNoDirection = [];
  3403. $sortDirection = [];
  3404. if ($statement !== null && ! empty($statement->order)) {
  3405. foreach ($statement->order as $o) {
  3406. $sortExpression[] = $o->expr->expr . ' ' . $o->type;
  3407. $sortExpressionNoDirection[] = $o->expr->expr;
  3408. $sortDirection[] = $o->type;
  3409. }
  3410. } else {
  3411. $sortExpression[] = '';
  3412. $sortExpressionNoDirection[] = '';
  3413. $sortDirection[] = '';
  3414. }
  3415. $numberOfColumns = count($sortExpressionNoDirection);
  3416. // 1.4 Prepares display of first and last value of the sorted column
  3417. $sortedColumnMessage = '';
  3418. for ($i = 0; $i < $numberOfColumns; $i++) {
  3419. $sortedColumnMessage .= $this->getSortedColumnMessage($dtResult, $sortExpressionNoDirection[$i]);
  3420. }
  3421. // 2. ----- Prepare to display the top of the page -----
  3422. // 2.1 Prepares a messages with position information
  3423. $sqlQueryMessage = '';
  3424. if (($displayParts['nav_bar'] == '1') && $posNext !== null) {
  3425. $message = $this->setMessageInformation(
  3426. $sortedColumnMessage,
  3427. $analyzedSqlResults,
  3428. $total,
  3429. $posNext,
  3430. $preCount,
  3431. $afterCount
  3432. );
  3433. $sqlQueryMessage = Generator::getMessage($message, $this->properties['sql_query'], 'success');
  3434. } elseif ((! isset($printView) || ($printView != '1')) && ! $isLimitedDisplay) {
  3435. $sqlQueryMessage = Generator::getMessage(
  3436. __('Your SQL query has been executed successfully.'),
  3437. $this->properties['sql_query'],
  3438. 'success'
  3439. );
  3440. }
  3441. // 2.3 Prepare the navigation bars
  3442. if (strlen($this->properties['table']) === 0) {
  3443. if ($analyzedSqlResults['querytype'] === 'SELECT') {
  3444. // table does not always contain a real table name,
  3445. // for example in MySQL 5.0.x, the query SHOW STATUS
  3446. // returns STATUS as a table name
  3447. $this->properties['table'] = $fieldsMeta[0]->table;
  3448. } else {
  3449. $this->properties['table'] = '';
  3450. }
  3451. }
  3452. // can the result be sorted?
  3453. if ($displayParts['sort_lnk'] == '1' && isset($analyzedSqlResults['statement'])) {
  3454. // At this point, $sort_expression is an array
  3455. [$unsortedSqlQuery, $sortByKeyData] = $this->getUnsortedSqlAndSortByKeyDropDown(
  3456. $analyzedSqlResults,
  3457. $sortExpression
  3458. );
  3459. } else {
  3460. $unsortedSqlQuery = '';
  3461. $sortByKeyData = [];
  3462. }
  3463. $navigation = [];
  3464. if ($displayParts['nav_bar'] == '1' && $statement !== null && empty($statement->limit)) {
  3465. $navigation = $this->getTableNavigation($posNext, $posPrev, $isInnodb, $sortByKeyData);
  3466. }
  3467. // 2b ----- Get field references from Database -----
  3468. // (see the 'relation' configuration variable)
  3469. // initialize map
  3470. $map = [];
  3471. if (strlen($this->properties['table']) > 0) {
  3472. // This method set the values for $map array
  3473. $this->setParamForLinkForeignKeyRelatedTables($map);
  3474. // Coming from 'Distinct values' action of structure page
  3475. // We manipulate relations mechanism to show a link to related rows.
  3476. if ($this->properties['is_browse_distinct']) {
  3477. $map[$fieldsMeta[1]->name] = [
  3478. $this->properties['table'],
  3479. $fieldsMeta[1]->name,
  3480. '',
  3481. $this->properties['db'],
  3482. ];
  3483. }
  3484. }
  3485. // end 2b
  3486. // 3. ----- Prepare the results table -----
  3487. $headers = $this->getTableHeaders(
  3488. $displayParts,
  3489. $analyzedSqlResults,
  3490. $unsortedSqlQuery,
  3491. $sortExpression,
  3492. $sortExpressionNoDirection,
  3493. $sortDirection,
  3494. $isLimitedDisplay
  3495. );
  3496. $body = $this->getTableBody($dtResult, $displayParts, $map, $analyzedSqlResults, $isLimitedDisplay);
  3497. $this->properties['display_params'] = null;
  3498. // 4. ----- Prepares the link for multi-fields edit and delete
  3499. $bulkLinks = $this->getBulkLinks($dtResult, $analyzedSqlResults, $displayParts['del_lnk']);
  3500. // 5. ----- Prepare "Query results operations"
  3501. $operations = [];
  3502. if ((! isset($printView) || ($printView != '1')) && ! $isLimitedDisplay) {
  3503. $operations = $this->getResultsOperations($displayParts, $analyzedSqlResults);
  3504. }
  3505. return $this->template->render('display/results/table', [
  3506. 'sql_query_message' => $sqlQueryMessage,
  3507. 'navigation' => $navigation,
  3508. 'headers' => $headers,
  3509. 'body' => $body,
  3510. 'bulk_links' => $bulkLinks,
  3511. 'operations' => $operations,
  3512. 'db' => $this->properties['db'],
  3513. 'table' => $this->properties['table'],
  3514. 'unique_id' => $this->properties['unique_id'],
  3515. 'sql_query' => $this->properties['sql_query'],
  3516. 'goto' => $this->properties['goto'],
  3517. 'unlim_num_rows' => $this->properties['unlim_num_rows'],
  3518. 'displaywork' => $GLOBALS['cfgRelation']['displaywork'],
  3519. 'relwork' => $GLOBALS['cfgRelation']['relwork'],
  3520. 'save_cells_at_once' => $GLOBALS['cfg']['SaveCellsAtOnce'],
  3521. 'default_sliders_state' => $GLOBALS['cfg']['InitialSlidersState'],
  3522. 'text_dir' => $this->properties['text_dir'],
  3523. ]);
  3524. }
  3525. /**
  3526. * Get offsets for next page and previous page
  3527. *
  3528. * @see getTable()
  3529. *
  3530. * @return int[] array with two elements - $pos_next, $pos_prev
  3531. *
  3532. * @access private
  3533. */
  3534. private function getOffsets()
  3535. {
  3536. if ($_SESSION['tmpval']['max_rows'] === self::ALL_ROWS) {
  3537. $posNext = 0;
  3538. $posPrev = 0;
  3539. } else {
  3540. $posNext = $_SESSION['tmpval']['pos'] + $_SESSION['tmpval']['max_rows'];
  3541. $posPrev = $_SESSION['tmpval']['pos'] - $_SESSION['tmpval']['max_rows'];
  3542. if ($posPrev < 0) {
  3543. $posPrev = 0;
  3544. }
  3545. }
  3546. return [
  3547. $posNext,
  3548. $posPrev,
  3549. ];
  3550. }
  3551. /**
  3552. * Prepare sorted column message
  3553. *
  3554. * @see getTable()
  3555. *
  3556. * @param int $dtResult the link id associated to the
  3557. * query which results have to
  3558. * be displayed
  3559. * @param string $sortExpressionNoDirection sort expression without direction
  3560. *
  3561. * @return string|null html content, null if not found sorted column
  3562. *
  3563. * @access private
  3564. */
  3565. private function getSortedColumnMessage(
  3566. &$dtResult,
  3567. $sortExpressionNoDirection
  3568. ) {
  3569. global $dbi;
  3570. /** @var FieldMetadata[] $fieldsMeta */
  3571. $fieldsMeta = $this->properties['fields_meta']; // To use array indexes
  3572. if (empty($sortExpressionNoDirection)) {
  3573. return null;
  3574. }
  3575. if (! str_contains($sortExpressionNoDirection, '.')) {
  3576. $sortTable = $this->properties['table'];
  3577. $sortColumn = $sortExpressionNoDirection;
  3578. } else {
  3579. [$sortTable, $sortColumn] = explode('.', $sortExpressionNoDirection);
  3580. }
  3581. $sortTable = Util::unQuote($sortTable);
  3582. $sortColumn = Util::unQuote($sortColumn);
  3583. // find the sorted column index in row result
  3584. // (this might be a multi-table query)
  3585. $sortedColumnIndex = false;
  3586. foreach ($fieldsMeta as $key => $meta) {
  3587. if (($meta->table == $sortTable) && ($meta->name == $sortColumn)) {
  3588. $sortedColumnIndex = $key;
  3589. break;
  3590. }
  3591. }
  3592. if ($sortedColumnIndex === false) {
  3593. return null;
  3594. }
  3595. // fetch first row of the result set
  3596. $row = $dbi->fetchRow($dtResult);
  3597. // check for non printable sorted row data
  3598. $meta = $fieldsMeta[$sortedColumnIndex];
  3599. $isBlobOrGeometryOrBinary = $meta->isType(FieldMetadata::TYPE_BLOB)
  3600. || $meta->isMappedTypeGeometry || $meta->isBinary;
  3601. if ($isBlobOrGeometryOrBinary) {
  3602. $columnForFirstRow = $this->handleNonPrintableContents(
  3603. $meta->getMappedType(),
  3604. $row[$sortedColumnIndex],
  3605. null,
  3606. [],
  3607. $meta
  3608. );
  3609. } else {
  3610. $columnForFirstRow = $row !== null ? $row[$sortedColumnIndex] : '';
  3611. }
  3612. $columnForFirstRow = mb_strtoupper(
  3613. mb_substr(
  3614. (string) $columnForFirstRow,
  3615. 0,
  3616. (int) $GLOBALS['cfg']['LimitChars']
  3617. ) . '...'
  3618. );
  3619. // fetch last row of the result set
  3620. $dbi->dataSeek($dtResult, $this->properties['num_rows'] > 0 ? $this->properties['num_rows'] - 1 : 0);
  3621. $row = $dbi->fetchRow($dtResult);
  3622. // check for non printable sorted row data
  3623. $meta = $fieldsMeta[$sortedColumnIndex];
  3624. if ($isBlobOrGeometryOrBinary) {
  3625. $columnForLastRow = $this->handleNonPrintableContents(
  3626. $meta->getMappedType(),
  3627. $row[$sortedColumnIndex],
  3628. null,
  3629. [],
  3630. $meta
  3631. );
  3632. } else {
  3633. $columnForLastRow = $row !== null ? $row[$sortedColumnIndex] : '';
  3634. }
  3635. $columnForLastRow = mb_strtoupper(
  3636. mb_substr(
  3637. (string) $columnForLastRow,
  3638. 0,
  3639. (int) $GLOBALS['cfg']['LimitChars']
  3640. ) . '...'
  3641. );
  3642. // reset to first row for the loop in getTableBody()
  3643. $dbi->dataSeek($dtResult, 0);
  3644. // we could also use here $sort_expression_nodirection
  3645. return ' [' . htmlspecialchars($sortColumn)
  3646. . ': <strong>' . htmlspecialchars($columnForFirstRow) . ' - '
  3647. . htmlspecialchars($columnForLastRow) . '</strong>]';
  3648. }
  3649. /**
  3650. * Set the content that needs to be shown in message
  3651. *
  3652. * @see getTable()
  3653. *
  3654. * @param string $sortedColumnMessage the message for sorted column
  3655. * @param array $analyzedSqlResults the analyzed query
  3656. * @param int $total the total number of rows returned by
  3657. * the SQL query without any
  3658. * programmatically appended LIMIT clause
  3659. * @param int $posNext the offset for next page
  3660. * @param string $preCount the string renders before row count
  3661. * @param string $afterCount the string renders after row count
  3662. *
  3663. * @return Message an object of Message
  3664. *
  3665. * @access private
  3666. */
  3667. private function setMessageInformation(
  3668. $sortedColumnMessage,
  3669. array $analyzedSqlResults,
  3670. $total,
  3671. $posNext,
  3672. $preCount,
  3673. $afterCount
  3674. ) {
  3675. $unlimNumRows = $this->properties['unlim_num_rows']; // To use in isset()
  3676. if (! empty($analyzedSqlResults['statement']->limit)) {
  3677. $firstShownRec = $analyzedSqlResults['statement']->limit->offset;
  3678. $rowCount = $analyzedSqlResults['statement']->limit->rowCount;
  3679. if ($rowCount < $total) {
  3680. $lastShownRec = $firstShownRec + $rowCount - 1;
  3681. } else {
  3682. $lastShownRec = $firstShownRec + $total - 1;
  3683. }
  3684. } elseif (($_SESSION['tmpval']['max_rows'] === self::ALL_ROWS) || ($posNext > $total)) {
  3685. $firstShownRec = $_SESSION['tmpval']['pos'];
  3686. $lastShownRec = $total - 1;
  3687. } else {
  3688. $firstShownRec = $_SESSION['tmpval']['pos'];
  3689. $lastShownRec = $posNext - 1;
  3690. }
  3691. $table = new Table($this->properties['table'], $this->properties['db']);
  3692. if ($table->isView() && ($total == $GLOBALS['cfg']['MaxExactCountViews'])) {
  3693. $message = Message::notice(
  3694. __(
  3695. 'This view has at least this number of rows. Please refer to %sdocumentation%s.'
  3696. )
  3697. );
  3698. $message->addParam('[doc@cfg_MaxExactCount]');
  3699. $message->addParam('[/doc]');
  3700. $messageViewWarning = Generator::showHint($message->getMessage());
  3701. } else {
  3702. $messageViewWarning = false;
  3703. }
  3704. $message = Message::success(__('Showing rows %1s - %2s'));
  3705. $message->addParam($firstShownRec);
  3706. if ($messageViewWarning !== false) {
  3707. $message->addParamHtml('... ' . $messageViewWarning);
  3708. } else {
  3709. $message->addParam($lastShownRec);
  3710. }
  3711. $message->addText('(');
  3712. if ($messageViewWarning === false) {
  3713. if ($unlimNumRows != $total) {
  3714. $messageTotal = Message::notice(
  3715. $preCount . __('%1$d total, %2$d in query')
  3716. );
  3717. $messageTotal->addParam($total);
  3718. $messageTotal->addParam($unlimNumRows);
  3719. } else {
  3720. $messageTotal = Message::notice($preCount . __('%d total'));
  3721. $messageTotal->addParam($total);
  3722. }
  3723. if (! empty($afterCount)) {
  3724. $messageTotal->addHtml($afterCount);
  3725. }
  3726. $message->addMessage($messageTotal, '');
  3727. $message->addText(', ', '');
  3728. }
  3729. $messageQueryTime = Message::notice(__('Query took %01.4f seconds.') . ')');
  3730. $messageQueryTime->addParam($this->properties['querytime']);
  3731. $message->addMessage($messageQueryTime, '');
  3732. if ($sortedColumnMessage !== null) {
  3733. $message->addHtml($sortedColumnMessage, '');
  3734. }
  3735. return $message;
  3736. }
  3737. /**
  3738. * Set the value of $map array for linking foreign key related tables
  3739. *
  3740. * @see getTable()
  3741. *
  3742. * @param array $map the list of relations
  3743. *
  3744. * @access private
  3745. */
  3746. private function setParamForLinkForeignKeyRelatedTables(array &$map): void
  3747. {
  3748. // To be able to later display a link to the related table,
  3749. // we verify both types of relations: either those that are
  3750. // native foreign keys or those defined in the phpMyAdmin
  3751. // configuration storage. If no PMA storage, we won't be able
  3752. // to use the "column to display" notion (for example show
  3753. // the name related to a numeric id).
  3754. $existRel = $this->relation->getForeigners(
  3755. $this->properties['db'],
  3756. $this->properties['table'],
  3757. '',
  3758. self::POSITION_BOTH
  3759. );
  3760. if (empty($existRel)) {
  3761. return;
  3762. }
  3763. foreach ($existRel as $masterField => $rel) {
  3764. if ($masterField !== 'foreign_keys_data') {
  3765. $displayField = $this->relation->getDisplayField($rel['foreign_db'], $rel['foreign_table']);
  3766. $map[$masterField] = [
  3767. $rel['foreign_table'],
  3768. $rel['foreign_field'],
  3769. $displayField,
  3770. $rel['foreign_db'],
  3771. ];
  3772. } else {
  3773. foreach ($rel as $key => $oneKey) {
  3774. foreach ($oneKey['index_list'] as $index => $oneField) {
  3775. $displayField = $this->relation->getDisplayField(
  3776. $oneKey['ref_db_name'] ?? $GLOBALS['db'],
  3777. $oneKey['ref_table_name']
  3778. );
  3779. $map[$oneField] = [
  3780. $oneKey['ref_table_name'],
  3781. $oneKey['ref_index_list'][$index],
  3782. $displayField,
  3783. $oneKey['ref_db_name'] ?? $GLOBALS['db'],
  3784. ];
  3785. }
  3786. }
  3787. }
  3788. }
  3789. }
  3790. /**
  3791. * Prepare multi field edit/delete links
  3792. *
  3793. * @see getTable()
  3794. *
  3795. * @param int $dtResult the link id associated to the query which
  3796. * results have to be displayed
  3797. * @param array $analyzedSqlResults analyzed sql results
  3798. * @param string $deleteLink the display element - 'del_link'
  3799. *
  3800. * @return array
  3801. */
  3802. private function getBulkLinks(
  3803. &$dtResult,
  3804. array $analyzedSqlResults,
  3805. $deleteLink
  3806. ): array {
  3807. global $dbi;
  3808. if ($deleteLink !== self::DELETE_ROW) {
  3809. return [];
  3810. }
  3811. // fetch last row of the result set
  3812. $dbi->dataSeek($dtResult, $this->properties['num_rows'] > 0 ? $this->properties['num_rows'] - 1 : 0);
  3813. $row = $dbi->fetchRow($dtResult);
  3814. // @see DbiMysqi::fetchRow & DatabaseInterface::fetchRow
  3815. if (! is_array($row)) {
  3816. $row = [];
  3817. }
  3818. $expressions = [];
  3819. if (isset($analyzedSqlResults['statement']) && $analyzedSqlResults['statement'] instanceof SelectStatement) {
  3820. $expressions = $analyzedSqlResults['statement']->expr;
  3821. }
  3822. /**
  3823. * $clause_is_unique is needed by getTable() to generate the proper param
  3824. * in the multi-edit and multi-delete form
  3825. */
  3826. [, $clauseIsUnique] = Util::getUniqueCondition(
  3827. $dtResult,
  3828. $this->properties['fields_cnt'],
  3829. $this->properties['fields_meta'],
  3830. $row,
  3831. false,
  3832. false,
  3833. $expressions
  3834. );
  3835. // reset to first row for the loop in getTableBody()
  3836. $dbi->dataSeek($dtResult, 0);
  3837. return [
  3838. 'has_export_button' => $analyzedSqlResults['querytype'] === 'SELECT',
  3839. 'clause_is_unique' => $clauseIsUnique,
  3840. ];
  3841. }
  3842. /**
  3843. * Get operations that are available on results.
  3844. *
  3845. * @see getTable()
  3846. *
  3847. * @param array $displayParts the parts to display
  3848. * @param array $analyzedSqlResults analyzed sql results
  3849. *
  3850. * @psalm-return array{
  3851. * has_export_link: bool,
  3852. * has_geometry: bool,
  3853. * has_print_link: bool,
  3854. * has_procedure: bool,
  3855. * url_params: array{
  3856. * db: string,
  3857. * table: string,
  3858. * printview: "1",
  3859. * sql_query: string,
  3860. * single_table?: "true",
  3861. * raw_query?: "true",
  3862. * unlim_num_rows?: int|numeric-string
  3863. * }
  3864. * }
  3865. */
  3866. private function getResultsOperations(
  3867. array $displayParts,
  3868. array $analyzedSqlResults
  3869. ): array {
  3870. global $printview, $dbi;
  3871. $urlParams = [
  3872. 'db' => $this->properties['db'],
  3873. 'table' => $this->properties['table'],
  3874. 'printview' => '1',
  3875. 'sql_query' => $this->properties['sql_query'],
  3876. ];
  3877. $geometryFound = false;
  3878. // Export link
  3879. // (the single_table parameter is used in \PhpMyAdmin\Export->getDisplay()
  3880. // to hide the SQL and the structure export dialogs)
  3881. // If the parser found a PROCEDURE clause
  3882. // (most probably PROCEDURE ANALYSE()) it makes no sense to
  3883. // display the Export link).
  3884. if (
  3885. ($analyzedSqlResults['querytype'] === self::QUERY_TYPE_SELECT)
  3886. && ! isset($printview)
  3887. && empty($analyzedSqlResults['procedure'])
  3888. ) {
  3889. if (count($analyzedSqlResults['select_tables']) === 1) {
  3890. $urlParams['single_table'] = 'true';
  3891. }
  3892. // In case this query doesn't involve any tables,
  3893. // implies only raw query is to be exported
  3894. if (! $analyzedSqlResults['select_tables']) {
  3895. $urlParams['raw_query'] = 'true';
  3896. }
  3897. $urlParams['unlim_num_rows'] = $this->properties['unlim_num_rows'];
  3898. /**
  3899. * At this point we don't know the table name; this can happen
  3900. * for example with a query like
  3901. * SELECT bike_code FROM (SELECT bike_code FROM bikes) tmp
  3902. * As a workaround we set in the table parameter the name of the
  3903. * first table of this database, so that /table/export and
  3904. * the script it calls do not fail
  3905. */
  3906. if ($urlParams['table'] === '' && strlen($urlParams['db']) > 0) {
  3907. $urlParams['table'] = (string) $dbi->fetchValue('SHOW TABLES');
  3908. }
  3909. /** @var FieldMetadata[] $fieldsMeta */
  3910. $fieldsMeta = $this->properties['fields_meta'];
  3911. foreach ($fieldsMeta as $meta) {
  3912. if ($meta->isMappedTypeGeometry) {
  3913. $geometryFound = true;
  3914. break;
  3915. }
  3916. }
  3917. }
  3918. return [
  3919. 'has_procedure' => ! empty($analyzedSqlResults['procedure']),
  3920. 'has_geometry' => $geometryFound,
  3921. 'has_print_link' => $displayParts['pview_lnk'] == '1',
  3922. 'has_export_link' => $analyzedSqlResults['querytype'] === self::QUERY_TYPE_SELECT && ! isset($printview),
  3923. 'url_params' => $urlParams,
  3924. ];
  3925. }
  3926. /**
  3927. * Verifies what to do with non-printable contents (binary or BLOB)
  3928. * in Browse mode.
  3929. *
  3930. * @see getDataCellForGeometryColumns(), getDataCellForNonNumericColumns(), getSortedColumnMessage()
  3931. *
  3932. * @param string $category BLOB|BINARY|GEOMETRY
  3933. * @param string|null $content the binary content
  3934. * @param array $transformOptions transformation parameters
  3935. * @param FieldMetadata $meta the meta-information about the field
  3936. * @param array $urlParams parameters that should go to the download link
  3937. * @param bool $isTruncated the result is truncated or not
  3938. *
  3939. * @return mixed string or float
  3940. *
  3941. * @access private
  3942. */
  3943. private function handleNonPrintableContents(
  3944. $category,
  3945. ?string $content,
  3946. ?TransformationsPlugin $transformationPlugin,
  3947. $transformOptions,
  3948. FieldMetadata $meta,
  3949. array $urlParams = [],
  3950. &$isTruncated = null
  3951. ) {
  3952. $isTruncated = false;
  3953. $result = '[' . $category;
  3954. if ($content !== null) {
  3955. $size = strlen($content);
  3956. $displaySize = Util::formatByteDown($size, 3, 1);
  3957. if ($displaySize !== null) {
  3958. $result .= ' - ' . $displaySize[0] . ' ' . $displaySize[1];
  3959. }
  3960. } else {
  3961. $result .= ' - NULL';
  3962. $size = 0;
  3963. $content = '';
  3964. }
  3965. $result .= ']';
  3966. // if we want to use a text transformation on a BLOB column
  3967. if (isset($transformationPlugin)) {
  3968. $posMimeOctetstream = strpos(
  3969. $transformationPlugin->getMIMESubtype(),
  3970. 'Octetstream'
  3971. );
  3972. $posMimeText = strpos($transformationPlugin->getMIMEtype(), 'Text');
  3973. if ($posMimeOctetstream || $posMimeText !== false) {
  3974. // Applying Transformations on hex string of binary data
  3975. // seems more appropriate
  3976. $result = pack('H*', bin2hex($content));
  3977. }
  3978. }
  3979. if ($size <= 0) {
  3980. return $result;
  3981. }
  3982. if (isset($transformationPlugin)) {
  3983. return $transformationPlugin->applyTransformation($result, $transformOptions, $meta);
  3984. }
  3985. $result = Core::mimeDefaultFunction($result);
  3986. if (
  3987. ($_SESSION['tmpval']['display_binary']
  3988. && $meta->isType(FieldMetadata::TYPE_STRING))
  3989. || ($_SESSION['tmpval']['display_blob']
  3990. && $meta->isType(FieldMetadata::TYPE_BLOB))
  3991. ) {
  3992. // in this case, restart from the original $content
  3993. if (
  3994. mb_check_encoding($content, 'utf-8')
  3995. && ! preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F]/u', $content)
  3996. ) {
  3997. // show as text if it's valid utf-8
  3998. $result = htmlspecialchars($content);
  3999. } else {
  4000. $result = '0x' . bin2hex($content);
  4001. }
  4002. [
  4003. $isTruncated,
  4004. $result,
  4005. // skip 3rd param
  4006. ] = $this->getPartialText($result);
  4007. }
  4008. /* Create link to download */
  4009. if (count($urlParams) > 0 && (! empty($this->properties['db']) && ! empty($meta->orgtable))) {
  4010. $urlParams['where_clause_sign'] = Core::signSqlQuery($urlParams['where_clause']);
  4011. $result = '<a href="'
  4012. . Url::getFromRoute('/table/get-field', $urlParams)
  4013. . '" class="disableAjax">'
  4014. . $result . '</a>';
  4015. }
  4016. return $result;
  4017. }
  4018. /**
  4019. * Retrieves the associated foreign key info for a data cell
  4020. *
  4021. * @param array $map the list of relations
  4022. * @param FieldMetadata $meta the meta-information about the field
  4023. * @param string $whereComparison data for the where clause
  4024. *
  4025. * @return string|null formatted data
  4026. *
  4027. * @access private
  4028. */
  4029. private function getFromForeign(array $map, FieldMetadata $meta, $whereComparison)
  4030. {
  4031. global $dbi;
  4032. $dispsql = 'SELECT '
  4033. . Util::backquote($map[$meta->name][2])
  4034. . ' FROM '
  4035. . Util::backquote($map[$meta->name][3])
  4036. . '.'
  4037. . Util::backquote($map[$meta->name][0])
  4038. . ' WHERE '
  4039. . Util::backquote($map[$meta->name][1])
  4040. . $whereComparison;
  4041. $dispresult = $dbi->tryQuery($dispsql, DatabaseInterface::CONNECT_USER, DatabaseInterface::QUERY_STORE);
  4042. if ($dispresult && $dbi->numRows($dispresult) > 0) {
  4043. [$dispval] = $dbi->fetchRow($dispresult);
  4044. } else {
  4045. $dispval = __('Link not found!');
  4046. }
  4047. $dbi->freeResult($dispresult);
  4048. return $dispval;
  4049. }
  4050. /**
  4051. * Prepares the displayable content of a data cell in Browse mode,
  4052. * taking into account foreign key description field and transformations
  4053. *
  4054. * @see getDataCellForNumericColumns(), getDataCellForGeometryColumns(),
  4055. * getDataCellForNonNumericColumns(),
  4056. *
  4057. * @param string $class css classes for the td element
  4058. * @param bool $conditionField whether the column is a part of the where clause
  4059. * @param array $analyzedSqlResults the analyzed query
  4060. * @param FieldMetadata $meta the meta-information about the field
  4061. * @param array $map the list of relations
  4062. * @param string $data data
  4063. * @param string $displayedData data that will be displayed (maybe be chunked)
  4064. * @param string $nowrap 'nowrap' if the content should not be wrapped
  4065. * @param string $whereComparison data for the where clause
  4066. * @param array $transformOptions options for transformation
  4067. * @param bool $isFieldTruncated whether the field is truncated
  4068. * @param string $originalLength of a truncated column, or ''
  4069. *
  4070. * @return string formatted data
  4071. *
  4072. * @access private
  4073. */
  4074. private function getRowData(
  4075. $class,
  4076. $conditionField,
  4077. array $analyzedSqlResults,
  4078. FieldMetadata $meta,
  4079. array $map,
  4080. $data,
  4081. $displayedData,
  4082. ?TransformationsPlugin $transformationPlugin,
  4083. $nowrap,
  4084. $whereComparison,
  4085. array $transformOptions,
  4086. $isFieldTruncated,
  4087. $originalLength = ''
  4088. ) {
  4089. $relationalDisplay = $_SESSION['tmpval']['relational_display'];
  4090. $printView = $this->properties['printview'];
  4091. $value = '';
  4092. $tableDataCellClass = $this->addClass(
  4093. $class,
  4094. $conditionField,
  4095. $meta,
  4096. $nowrap,
  4097. $isFieldTruncated,
  4098. isset($transformationPlugin)
  4099. );
  4100. if (! empty($analyzedSqlResults['statement']->expr)) {
  4101. foreach ($analyzedSqlResults['statement']->expr as $expr) {
  4102. if (empty($expr->alias) || empty($expr->column)) {
  4103. continue;
  4104. }
  4105. if (strcasecmp($meta->name, $expr->alias) != 0) {
  4106. continue;
  4107. }
  4108. $meta->name = $expr->column;
  4109. }
  4110. }
  4111. if (isset($map[$meta->name])) {
  4112. // Field to display from the foreign table?
  4113. if (isset($map[$meta->name][2]) && strlen((string) $map[$meta->name][2]) > 0) {
  4114. $dispval = $this->getFromForeign($map, $meta, $whereComparison);
  4115. } else {
  4116. $dispval = '';
  4117. }
  4118. if (isset($printView) && ($printView == '1')) {
  4119. if (isset($transformationPlugin)) {
  4120. $value .= $transformationPlugin->applyTransformation($data, $transformOptions, $meta);
  4121. } else {
  4122. $value .= Core::mimeDefaultFunction($data);
  4123. }
  4124. $value .= ' <code>[-&gt;' . $dispval . ']</code>';
  4125. } else {
  4126. if ($relationalDisplay === self::RELATIONAL_KEY) {
  4127. // user chose "relational key" in the display options, so
  4128. // the title contains the display field
  4129. $title = ! empty($dispval)
  4130. ? htmlspecialchars($dispval)
  4131. : '';
  4132. } else {
  4133. $title = htmlspecialchars($data);
  4134. }
  4135. $sqlQuery = 'SELECT * FROM '
  4136. . Util::backquote($map[$meta->name][3]) . '.'
  4137. . Util::backquote($map[$meta->name][0])
  4138. . ' WHERE '
  4139. . Util::backquote($map[$meta->name][1])
  4140. . $whereComparison;
  4141. $urlParams = [
  4142. 'db' => $map[$meta->name][3],
  4143. 'table' => $map[$meta->name][0],
  4144. 'pos' => '0',
  4145. 'sql_signature' => Core::signSqlQuery($sqlQuery),
  4146. 'sql_query' => $sqlQuery,
  4147. ];
  4148. if (isset($transformationPlugin)) {
  4149. // always apply a transformation on the real data,
  4150. // not on the display field
  4151. $displayedData = $transformationPlugin->applyTransformation($data, $transformOptions, $meta);
  4152. } else {
  4153. if ($relationalDisplay === self::RELATIONAL_DISPLAY_COLUMN && ! empty($map[$meta->name][2])) {
  4154. // user chose "relational display field" in the
  4155. // display options, so show display field in the cell
  4156. $displayedData = $dispval === null ? '<em>NULL</em>' : Core::mimeDefaultFunction($dispval);
  4157. } else {
  4158. // otherwise display data in the cell
  4159. $displayedData = Core::mimeDefaultFunction($displayedData);
  4160. }
  4161. }
  4162. $tagParams = ['title' => $title];
  4163. if (str_contains($class, 'grid_edit')) {
  4164. $tagParams['class'] = 'ajax';
  4165. }
  4166. $value .= Generator::linkOrButton(
  4167. Url::getFromRoute('/sql', $urlParams),
  4168. $displayedData,
  4169. $tagParams
  4170. );
  4171. }
  4172. } elseif (isset($transformationPlugin)) {
  4173. $value .= $transformationPlugin->applyTransformation($data, $transformOptions, $meta);
  4174. } else {
  4175. $value .= Core::mimeDefaultFunction($data);
  4176. }
  4177. return $this->template->render('display/results/row_data', [
  4178. 'value' => $value,
  4179. 'td_class' => $tableDataCellClass,
  4180. 'decimals' => $meta->decimals ?? '-1',
  4181. 'type' => $meta->getMappedType(),
  4182. 'original_length' => $originalLength,
  4183. ]);
  4184. }
  4185. /**
  4186. * Truncates given string based on LimitChars configuration
  4187. * and Session pftext variable
  4188. * (string is truncated only if necessary)
  4189. *
  4190. * @see handleNonPrintableContents(), getDataCellForGeometryColumns(), getDataCellForNonNumericColumns
  4191. *
  4192. * @param string $str string to be truncated
  4193. *
  4194. * @return array
  4195. *
  4196. * @access private
  4197. */
  4198. private function getPartialText($str): array
  4199. {
  4200. $originalLength = mb_strlen($str);
  4201. if (
  4202. $originalLength > $GLOBALS['cfg']['LimitChars']
  4203. && $_SESSION['tmpval']['pftext'] === self::DISPLAY_PARTIAL_TEXT
  4204. ) {
  4205. $str = mb_substr($str, 0, (int) $GLOBALS['cfg']['LimitChars']) . '...';
  4206. $truncated = true;
  4207. } else {
  4208. $truncated = false;
  4209. }
  4210. return [
  4211. $truncated,
  4212. $str,
  4213. $originalLength,
  4214. ];
  4215. }
  4216. }