PageRenderTime 57ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/libraries/classes/Html/Generator.php

http://github.com/phpmyadmin/phpmyadmin
PHP | 1354 lines | 859 code | 166 blank | 329 comment | 146 complexity | 60d5ec316dc8977a785b63321357ff4f MD5 | raw file
Possible License(s): GPL-2.0, MIT, LGPL-3.0
  1. <?php
  2. /**
  3. * HTML Generator
  4. */
  5. declare(strict_types=1);
  6. namespace PhpMyAdmin\Html;
  7. use PhpMyAdmin\Core;
  8. use PhpMyAdmin\Message;
  9. use PhpMyAdmin\Profiling;
  10. use PhpMyAdmin\Providers\ServerVariables\ServerVariablesProvider;
  11. use PhpMyAdmin\Query\Compatibility;
  12. use PhpMyAdmin\ResponseRenderer;
  13. use PhpMyAdmin\Sanitize;
  14. use PhpMyAdmin\SqlParser\Lexer;
  15. use PhpMyAdmin\SqlParser\Parser;
  16. use PhpMyAdmin\SqlParser\Utils\Error as ParserError;
  17. use PhpMyAdmin\Url;
  18. use PhpMyAdmin\Util;
  19. use Throwable;
  20. use Twig\Error\LoaderError;
  21. use Twig\Error\RuntimeError;
  22. use Twig\Error\SyntaxError;
  23. use function __;
  24. use function _pgettext;
  25. use function addslashes;
  26. use function array_key_exists;
  27. use function ceil;
  28. use function count;
  29. use function explode;
  30. use function floor;
  31. use function htmlentities;
  32. use function htmlspecialchars;
  33. use function implode;
  34. use function in_array;
  35. use function ini_get;
  36. use function intval;
  37. use function is_array;
  38. use function mb_strlen;
  39. use function mb_strstr;
  40. use function mb_strtolower;
  41. use function mb_substr;
  42. use function nl2br;
  43. use function preg_match;
  44. use function preg_replace;
  45. use function sprintf;
  46. use function str_contains;
  47. use function str_replace;
  48. use function str_starts_with;
  49. use function strlen;
  50. use function strtoupper;
  51. use function substr;
  52. use function trim;
  53. use function urlencode;
  54. use const ENT_COMPAT;
  55. /**
  56. * HTML Generator
  57. */
  58. class Generator
  59. {
  60. /**
  61. * Displays a button to copy content to clipboard
  62. *
  63. * @param string $text Text to copy to clipboard
  64. *
  65. * @return string the html link
  66. *
  67. * @access public
  68. */
  69. public static function showCopyToClipboard(string $text): string
  70. {
  71. return ' <a href="#" class="copyQueryBtn" data-text="'
  72. . htmlspecialchars($text) . '">' . __('Copy') . '</a>';
  73. }
  74. /**
  75. * Get a link to variable documentation
  76. *
  77. * @param string $name The variable name
  78. * @param bool $useMariaDB Use only MariaDB documentation
  79. * @param string $text (optional) The text for the link
  80. *
  81. * @return string link or empty string
  82. */
  83. public static function linkToVarDocumentation(
  84. string $name,
  85. bool $useMariaDB = false,
  86. ?string $text = null
  87. ): string {
  88. $kbs = ServerVariablesProvider::getImplementation();
  89. $link = $useMariaDB ? $kbs->getDocLinkByNameMariaDb($name) :
  90. $kbs->getDocLinkByNameMysql($name);
  91. return MySQLDocumentation::show($name, false, $link, $text);
  92. }
  93. /**
  94. * Returns HTML code for a tooltip
  95. *
  96. * @param string $message the message for the tooltip
  97. *
  98. * @access public
  99. */
  100. public static function showHint(string $message): string
  101. {
  102. if ($GLOBALS['cfg']['ShowHint']) {
  103. $classClause = ' class="pma_hint"';
  104. } else {
  105. $classClause = '';
  106. }
  107. return '<span' . $classClause . '>'
  108. . self::getImage('b_help')
  109. . '<span class="hide">' . $message . '</span>'
  110. . '</span>';
  111. }
  112. /**
  113. * returns html code for db link to default db page
  114. *
  115. * @param string $database database
  116. *
  117. * @return string html link to default db page
  118. */
  119. public static function getDbLink($database = ''): string
  120. {
  121. if ((string) $database === '') {
  122. if ((string) $GLOBALS['db'] === '') {
  123. return '';
  124. }
  125. $database = $GLOBALS['db'];
  126. } else {
  127. $database = Util::unescapeMysqlWildcards($database);
  128. }
  129. $scriptName = Util::getScriptNameForOption($GLOBALS['cfg']['DefaultTabDatabase'], 'database');
  130. return '<a href="'
  131. . $scriptName
  132. . Url::getCommon(['db' => $database], ! str_contains($scriptName, '?') ? '?' : '&')
  133. . '" title="'
  134. . htmlspecialchars(
  135. sprintf(
  136. __('Jump to database ā€œ%sā€.'),
  137. $database
  138. )
  139. )
  140. . '">' . htmlspecialchars($database) . '</a>';
  141. }
  142. /**
  143. * Prepare a lightbulb hint explaining a known external bug
  144. * that affects a functionality
  145. *
  146. * @param string $functionality localized message explaining the func.
  147. * @param string $component 'mysql' (eventually, 'php')
  148. * @param string $minimumVersion of this component
  149. * @param string $bugReference bug reference for this component
  150. */
  151. public static function getExternalBug(
  152. $functionality,
  153. $component,
  154. $minimumVersion,
  155. $bugReference
  156. ): string {
  157. global $dbi;
  158. $return = '';
  159. if (($component === 'mysql') && ($dbi->getVersion() < $minimumVersion)) {
  160. $return .= self::showHint(
  161. sprintf(
  162. __('The %s functionality is affected by a known bug, see %s'),
  163. $functionality,
  164. Core::linkURL('https://bugs.mysql.com/') . $bugReference
  165. )
  166. );
  167. }
  168. return $return;
  169. }
  170. /**
  171. * Returns an HTML IMG tag for a particular icon from a theme,
  172. * which may be an actual file or an icon from a sprite.
  173. * This function takes into account the ActionLinksMode
  174. * configuration setting and wraps the image tag in a span tag.
  175. *
  176. * @param string $icon name of icon file
  177. * @param string $alternate alternate text
  178. * @param bool $forceText whether to force alternate text to be displayed
  179. * @param bool $menuIcon whether this icon is for the menu bar or not
  180. * @param string $controlParam which directive controls the display
  181. *
  182. * @return string an html snippet
  183. */
  184. public static function getIcon(
  185. $icon,
  186. $alternate = '',
  187. $forceText = false,
  188. $menuIcon = false,
  189. $controlParam = 'ActionLinksMode'
  190. ): string {
  191. $includeIcon = $includeText = false;
  192. if (Util::showIcons($controlParam)) {
  193. $includeIcon = true;
  194. }
  195. if ($forceText || Util::showText($controlParam)) {
  196. $includeText = true;
  197. }
  198. // Sometimes use a span (we rely on this in js/sql.js). But for menu bar
  199. // we don't need a span
  200. $button = $menuIcon ? '' : '<span class="text-nowrap">';
  201. if ($includeIcon) {
  202. $button .= self::getImage($icon, $alternate);
  203. }
  204. if ($includeIcon && $includeText) {
  205. $button .= '&nbsp;';
  206. }
  207. if ($includeText) {
  208. $button .= $alternate;
  209. }
  210. $button .= $menuIcon ? '' : '</span>';
  211. return $button;
  212. }
  213. /**
  214. * Returns information about SSL status for current connection
  215. */
  216. public static function getServerSSL(): string
  217. {
  218. $server = $GLOBALS['cfg']['Server'];
  219. $class = 'text-danger';
  220. if (! $server['ssl']) {
  221. $message = __('SSL is not being used');
  222. if (! empty($server['socket']) || in_array($server['host'], $GLOBALS['cfg']['MysqlSslWarningSafeHosts'])) {
  223. $class = '';
  224. }
  225. } elseif (! $server['ssl_verify']) {
  226. $message = __('SSL is used with disabled verification');
  227. } elseif (empty($server['ssl_ca'])) {
  228. $message = __('SSL is used without certification authority');
  229. } else {
  230. $class = '';
  231. $message = __('SSL is used');
  232. }
  233. return '<span class="' . $class . '">' . $message . '</span> ' . MySQLDocumentation::showDocumentation(
  234. 'setup',
  235. 'ssl'
  236. );
  237. }
  238. /**
  239. * Returns default function for a particular column.
  240. *
  241. * @param array $field Data about the column for which
  242. * to generate the dropdown
  243. * @param bool $insertMode Whether the operation is 'insert'
  244. *
  245. * @return string An HTML snippet of a dropdown list with function
  246. * names appropriate for the requested column.
  247. *
  248. * @global mixed $data data of currently edited row
  249. * (used to detect whether to choose defaults)
  250. * @global array $cfg PMA configuration
  251. */
  252. public static function getDefaultFunctionForField(array $field, $insertMode): string
  253. {
  254. global $cfg, $data, $dbi;
  255. $defaultFunction = '';
  256. // Can we get field class based values?
  257. $currentClass = $dbi->types->getTypeClass($field['True_Type']);
  258. if (! empty($currentClass) && isset($cfg['DefaultFunctions']['FUNC_' . $currentClass])) {
  259. $defaultFunction = $cfg['DefaultFunctions']['FUNC_' . $currentClass];
  260. // Change the configured default function to include the ST_ prefix with MySQL 5.6 and later.
  261. // It needs to match the function listed in the select html element.
  262. if (
  263. $currentClass === 'SPATIAL' &&
  264. $dbi->getVersion() >= 50600 &&
  265. strtoupper(substr($defaultFunction, 0, 3)) !== 'ST_'
  266. ) {
  267. $defaultFunction = 'ST_' . $defaultFunction;
  268. }
  269. }
  270. // what function defined as default?
  271. // for the first timestamp we don't set the default function
  272. // if there is a default value for the timestamp
  273. // (not including CURRENT_TIMESTAMP)
  274. // and the column does not have the
  275. // ON UPDATE DEFAULT TIMESTAMP attribute.
  276. if (
  277. ($field['True_Type'] === 'timestamp')
  278. && $field['first_timestamp']
  279. && empty($field['Default'])
  280. && empty($data)
  281. && $field['Extra'] !== 'on update CURRENT_TIMESTAMP'
  282. && $field['Null'] === 'NO'
  283. ) {
  284. $defaultFunction = $cfg['DefaultFunctions']['first_timestamp'];
  285. }
  286. // For primary keys of type char(36) or varchar(36) UUID if the default
  287. // function
  288. // Only applies to insert mode, as it would silently trash data on updates.
  289. if (
  290. $insertMode
  291. && $field['Key'] === 'PRI'
  292. && ($field['Type'] === 'char(36)' || $field['Type'] === 'varchar(36)')
  293. ) {
  294. $defaultFunction = $cfg['DefaultFunctions']['FUNC_UUID'];
  295. }
  296. return $defaultFunction;
  297. }
  298. /**
  299. * Creates a dropdown box with MySQL functions for a particular column.
  300. *
  301. * @param array $field Data about the column for which to generate the dropdown
  302. * @param bool $insertMode Whether the operation is 'insert'
  303. * @param array $foreignData Foreign data
  304. *
  305. * @return string An HTML snippet of a dropdown list with function names appropriate for the requested column.
  306. */
  307. public static function getFunctionsForField(array $field, $insertMode, array $foreignData): string
  308. {
  309. global $dbi;
  310. $defaultFunction = self::getDefaultFunctionForField($field, $insertMode);
  311. // Create the output
  312. $retval = '<option></option>' . "\n";
  313. // loop on the dropdown array and print all available options for that
  314. // field.
  315. $functions = $dbi->types->getAllFunctions();
  316. foreach ($functions as $function) {
  317. $retval .= '<option';
  318. if ($function === $defaultFunction && ! isset($foreignData['foreign_field'])) {
  319. $retval .= ' selected="selected"';
  320. }
  321. $retval .= '>' . $function . '</option>' . "\n";
  322. }
  323. $retval .= '<option value="PHP_PASSWORD_HASH" title="';
  324. $retval .= htmlentities(__('The PHP function password_hash() with default options.'), ENT_COMPAT);
  325. $retval .= '">' . __('password_hash() PHP function') . '</option>' . "\n";
  326. return $retval;
  327. }
  328. /**
  329. * Renders a single link for the top of the navigation panel
  330. *
  331. * @param string $link The url for the link
  332. * @param bool $showText Whether to show the text or to
  333. * only use it for title attributes
  334. * @param string $text The text to display and use for title attributes
  335. * @param bool $showIcon Whether to show the icon
  336. * @param string $icon The filename of the icon to show
  337. * @param string $linkId Value to use for the ID attribute
  338. * @param bool $disableAjax Whether to disable ajax page loading for this link
  339. * @param string $linkTarget The name of the target frame for the link
  340. * @param array $classes HTML classes to apply
  341. *
  342. * @return string HTML code for one link
  343. */
  344. public static function getNavigationLink(
  345. $link,
  346. $showText,
  347. $text,
  348. $showIcon,
  349. $icon,
  350. $linkId = '',
  351. $disableAjax = false,
  352. $linkTarget = '',
  353. array $classes = []
  354. ): string {
  355. $retval = '<a href="' . $link . '"';
  356. if (! empty($linkId)) {
  357. $retval .= ' id="' . $linkId . '"';
  358. }
  359. if (! empty($linkTarget)) {
  360. $retval .= ' target="' . $linkTarget . '"';
  361. }
  362. if ($disableAjax) {
  363. $classes[] = 'disableAjax';
  364. }
  365. if (! empty($classes)) {
  366. $retval .= ' class="' . implode(' ', $classes) . '"';
  367. }
  368. $retval .= ' title="' . $text . '">';
  369. if ($showIcon) {
  370. $retval .= self::getImage($icon, $text);
  371. }
  372. if ($showText) {
  373. $retval .= $text;
  374. }
  375. $retval .= '</a>';
  376. if ($showText) {
  377. $retval .= '<br>';
  378. }
  379. return $retval;
  380. }
  381. /**
  382. * @return array<string, int|string>
  383. * @psalm-return array{pos: int, unlim_num_rows: int, rows: int, sql_query: string}
  384. */
  385. public static function getStartAndNumberOfRowsFieldsetData(string $sqlQuery): array
  386. {
  387. if (isset($_REQUEST['session_max_rows'])) {
  388. $rows = (int) $_REQUEST['session_max_rows'];
  389. } elseif (isset($_SESSION['tmpval']['max_rows']) && $_SESSION['tmpval']['max_rows'] !== 'all') {
  390. $rows = (int) $_SESSION['tmpval']['max_rows'];
  391. } else {
  392. $rows = (int) $GLOBALS['cfg']['MaxRows'];
  393. $_SESSION['tmpval']['max_rows'] = $rows;
  394. }
  395. $numberOfLine = (int) $_REQUEST['unlim_num_rows'];
  396. if (isset($_REQUEST['pos'])) {
  397. $pos = (int) $_REQUEST['pos'];
  398. } elseif (isset($_SESSION['tmpval']['pos'])) {
  399. $pos = (int) $_SESSION['tmpval']['pos'];
  400. } else {
  401. $pos = ((int) ceil($numberOfLine / $rows) - 1) * $rows;
  402. $_SESSION['tmpval']['pos'] = $pos;
  403. }
  404. return ['pos' => $pos, 'unlim_num_rows' => $numberOfLine, 'rows' => $rows, 'sql_query' => $sqlQuery];
  405. }
  406. /**
  407. * Execute an EXPLAIN query and formats results similar to MySQL command line
  408. * utility.
  409. *
  410. * @param string $sqlQuery EXPLAIN query
  411. *
  412. * @return string query results
  413. */
  414. private static function generateRowQueryOutput($sqlQuery): string
  415. {
  416. global $dbi;
  417. $ret = '';
  418. $result = $dbi->query($sqlQuery);
  419. if ($result) {
  420. $devider = '+';
  421. $columnNames = '|';
  422. $fieldsMeta = $dbi->getFieldsMeta($result);
  423. foreach ($fieldsMeta as $meta) {
  424. $devider .= '---+';
  425. $columnNames .= ' ' . $meta->name . ' |';
  426. }
  427. $devider .= "\n";
  428. $ret .= $devider . $columnNames . "\n" . $devider;
  429. while ($row = $dbi->fetchRow($result)) {
  430. $values = '|';
  431. foreach ($row as $value) {
  432. if ($value === null) {
  433. $value = 'NULL';
  434. }
  435. $values .= ' ' . $value . ' |';
  436. }
  437. $ret .= $values . "\n";
  438. }
  439. $ret .= $devider;
  440. }
  441. return $ret;
  442. }
  443. /**
  444. * Prepare the message and the query
  445. * usually the message is the result of the query executed
  446. *
  447. * @param Message|string $message the message to display
  448. * @param string $sqlQuery the query to display
  449. * @param string $type the type (level) of the message
  450. *
  451. * @throws Throwable
  452. * @throws LoaderError
  453. * @throws RuntimeError
  454. * @throws SyntaxError
  455. *
  456. * @access public
  457. */
  458. public static function getMessage(
  459. $message,
  460. $sqlQuery = null,
  461. $type = 'notice'
  462. ): string {
  463. global $cfg, $dbi;
  464. $retval = '';
  465. if ($sqlQuery === null) {
  466. if (! empty($GLOBALS['display_query'])) {
  467. $sqlQuery = $GLOBALS['display_query'];
  468. } elseif (! empty($GLOBALS['unparsed_sql'])) {
  469. $sqlQuery = $GLOBALS['unparsed_sql'];
  470. } elseif (! empty($GLOBALS['sql_query'])) {
  471. $sqlQuery = $GLOBALS['sql_query'];
  472. } else {
  473. $sqlQuery = '';
  474. }
  475. }
  476. $renderSql = $cfg['ShowSQL'] == true && ! empty($sqlQuery) && $sqlQuery !== ';';
  477. if (isset($GLOBALS['using_bookmark_message'])) {
  478. $retval .= $GLOBALS['using_bookmark_message']->getDisplay();
  479. unset($GLOBALS['using_bookmark_message']);
  480. }
  481. if ($renderSql) {
  482. $retval .= '<div class="result_query">' . "\n";
  483. }
  484. if ($message instanceof Message) {
  485. if (isset($GLOBALS['special_message'])) {
  486. $message->addText($GLOBALS['special_message']);
  487. unset($GLOBALS['special_message']);
  488. }
  489. $retval .= $message->getDisplay();
  490. } else {
  491. $context = 'primary';
  492. if ($type === 'error') {
  493. $context = 'danger';
  494. } elseif ($type === 'success') {
  495. $context = 'success';
  496. }
  497. $retval .= '<div class="alert alert-' . $context . '" role="alert">';
  498. $retval .= Sanitize::sanitizeMessage($message);
  499. if (isset($GLOBALS['special_message'])) {
  500. $retval .= Sanitize::sanitizeMessage($GLOBALS['special_message']);
  501. unset($GLOBALS['special_message']);
  502. }
  503. $retval .= '</div>';
  504. }
  505. if ($renderSql) {
  506. $queryTooBig = false;
  507. $queryLength = mb_strlen($sqlQuery);
  508. if ($queryLength > $cfg['MaxCharactersInDisplayedSQL']) {
  509. // when the query is large (for example an INSERT of binary
  510. // data), the parser chokes; so avoid parsing the query
  511. $queryTooBig = true;
  512. $queryBase = mb_substr($sqlQuery, 0, $cfg['MaxCharactersInDisplayedSQL']) . '[...]';
  513. } else {
  514. $queryBase = $sqlQuery;
  515. }
  516. // Html format the query to be displayed
  517. // If we want to show some sql code it is easiest to create it here
  518. /* SQL-Parser-Analyzer */
  519. if (! empty($GLOBALS['show_as_php'])) {
  520. $newLine = '\\n"<br>' . "\n" . '&nbsp;&nbsp;&nbsp;&nbsp;. "';
  521. $queryBase = htmlspecialchars(addslashes($queryBase));
  522. $queryBase = preg_replace('/((\015\012)|(\015)|(\012))/', $newLine, $queryBase);
  523. $queryBase = '<code class="php"><pre>' . "\n"
  524. . '$sql = "' . $queryBase . '";' . "\n"
  525. . '</pre></code>';
  526. } elseif ($queryTooBig) {
  527. $queryBase = '<code class="sql"><pre>' . "\n" .
  528. htmlspecialchars($queryBase, ENT_COMPAT) .
  529. '</pre></code>';
  530. } else {
  531. $queryBase = self::formatSql($queryBase);
  532. }
  533. // Prepares links that may be displayed to edit/explain the query
  534. // (don't go to default pages, we must go to the page
  535. // where the query box is available)
  536. // Basic url query part
  537. $urlParams = [];
  538. if (! isset($GLOBALS['db'])) {
  539. $GLOBALS['db'] = '';
  540. }
  541. if (strlen($GLOBALS['db']) > 0) {
  542. $urlParams['db'] = $GLOBALS['db'];
  543. if (strlen($GLOBALS['table']) > 0) {
  544. $urlParams['table'] = $GLOBALS['table'];
  545. $editLink = Url::getFromRoute('/table/sql');
  546. } else {
  547. $editLink = Url::getFromRoute('/database/sql');
  548. }
  549. } else {
  550. $editLink = Url::getFromRoute('/server/sql');
  551. }
  552. // Want to have the query explained
  553. // but only explain a SELECT (that has not been explained)
  554. /* SQL-Parser-Analyzer */
  555. $explainLink = '';
  556. $isSelect = preg_match('@^SELECT[[:space:]]+@i', $sqlQuery);
  557. if (! empty($cfg['SQLQuery']['Explain']) && ! $queryTooBig) {
  558. $explainParams = $urlParams;
  559. if ($isSelect) {
  560. $explainParams['sql_query'] = 'EXPLAIN ' . $sqlQuery;
  561. $explainLink = ' [&nbsp;'
  562. . self::linkOrButton(
  563. Url::getFromRoute('/import', $explainParams),
  564. __('Explain SQL')
  565. ) . '&nbsp;]';
  566. } elseif (preg_match('@^EXPLAIN[[:space:]]+SELECT[[:space:]]+@i', $sqlQuery)) {
  567. $explainParams['sql_query'] = mb_substr($sqlQuery, 8);
  568. $explainLink = ' [&nbsp;'
  569. . self::linkOrButton(
  570. Url::getFromRoute('/import', $explainParams),
  571. __('Skip Explain SQL')
  572. ) . ']';
  573. $url = 'https://mariadb.org/explain_analyzer/analyze/'
  574. . '?client=phpMyAdmin&raw_explain='
  575. . urlencode(self::generateRowQueryOutput($sqlQuery));
  576. $explainLink .= ' ['
  577. . self::linkOrButton(
  578. htmlspecialchars('url.php?url=' . urlencode($url)),
  579. sprintf(__('Analyze Explain at %s'), 'mariadb.org'),
  580. [],
  581. '_blank',
  582. false
  583. ) . '&nbsp;]';
  584. }
  585. }
  586. $urlParams['sql_query'] = $sqlQuery;
  587. $urlParams['show_query'] = 1;
  588. // even if the query is big and was truncated, offer the chance
  589. // to edit it (unless it's enormous, see linkOrButton() )
  590. if (! empty($cfg['SQLQuery']['Edit']) && empty($GLOBALS['show_as_php'])) {
  591. $editLink .= Url::getCommon($urlParams, '&');
  592. $editLink = ' [&nbsp;'
  593. . self::linkOrButton($editLink, __('Edit'))
  594. . '&nbsp;]';
  595. } else {
  596. $editLink = '';
  597. }
  598. // Also we would like to get the SQL formed in some nice
  599. // php-code
  600. if (! empty($cfg['SQLQuery']['ShowAsPHP']) && ! $queryTooBig) {
  601. if (! empty($GLOBALS['show_as_php'])) {
  602. $phpLink = ' [&nbsp;'
  603. . self::linkOrButton(
  604. Url::getFromRoute('/import', $urlParams),
  605. __('Without PHP code')
  606. )
  607. . '&nbsp;]';
  608. $phpLink .= ' [&nbsp;'
  609. . self::linkOrButton(
  610. Url::getFromRoute('/import', $urlParams),
  611. __('Submit query')
  612. )
  613. . '&nbsp;]';
  614. } else {
  615. $phpParams = $urlParams;
  616. $phpParams['show_as_php'] = 1;
  617. $phpLink = ' [&nbsp;'
  618. . self::linkOrButton(
  619. Url::getFromRoute('/import', $phpParams),
  620. __('Create PHP code')
  621. )
  622. . '&nbsp;]';
  623. }
  624. } else {
  625. $phpLink = '';
  626. }
  627. // Refresh query
  628. if (
  629. ! empty($cfg['SQLQuery']['Refresh'])
  630. && ! isset($GLOBALS['show_as_php']) // 'Submit query' does the same
  631. && preg_match('@^(SELECT|SHOW)[[:space:]]+@i', $sqlQuery)
  632. ) {
  633. $refreshLink = Url::getFromRoute('/sql', $urlParams);
  634. $refreshLink = ' [&nbsp;'
  635. . self::linkOrButton($refreshLink, __('Refresh')) . '&nbsp;]';
  636. } else {
  637. $refreshLink = '';
  638. }
  639. $retval .= '<div class="sqlOuter">';
  640. $retval .= $queryBase;
  641. $retval .= '</div>';
  642. $retval .= '<div class="tools d-print-none">';
  643. $retval .= '<form action="' . Url::getFromRoute('/sql') . '" method="post">';
  644. $retval .= Url::getHiddenInputs($GLOBALS['db'], $GLOBALS['table']);
  645. $retval .= '<input type="hidden" name="sql_query" value="'
  646. . htmlspecialchars($sqlQuery) . '">';
  647. // avoid displaying a Profiling checkbox that could
  648. // be checked, which would re-execute an INSERT, for example
  649. if (! empty($refreshLink) && Profiling::isSupported($dbi)) {
  650. $retval .= '<input type="hidden" name="profiling_form" value="1">';
  651. $retval .= '<input type="checkbox" name="profiling" id="profilingCheckbox" class="autosubmit"';
  652. $retval .= isset($_SESSION['profiling']) ? ' checked' : '';
  653. $retval .= '> <label for="profilingCheckbox">' . __('Profiling') . '</label>';
  654. }
  655. $retval .= '</form>';
  656. /**
  657. * TODO: Should we have $cfg['SQLQuery']['InlineEdit']?
  658. */
  659. if (! empty($cfg['SQLQuery']['Edit']) && ! $queryTooBig && empty($GLOBALS['show_as_php'])) {
  660. $inlineEditLink = ' [&nbsp;'
  661. . self::linkOrButton(
  662. '#',
  663. _pgettext('Inline edit query', 'Edit inline'),
  664. ['class' => 'inline_edit_sql']
  665. )
  666. . '&nbsp;]';
  667. } else {
  668. $inlineEditLink = '';
  669. }
  670. $retval .= $inlineEditLink . $editLink . $explainLink . $phpLink
  671. . $refreshLink;
  672. $retval .= '</div>';
  673. $retval .= '</div>';
  674. }
  675. return $retval;
  676. }
  677. /**
  678. * Displays a link to the PHP documentation
  679. *
  680. * @param string $target anchor in documentation
  681. *
  682. * @return string the html link
  683. *
  684. * @access public
  685. */
  686. public static function showPHPDocumentation($target): string
  687. {
  688. return self::showDocumentationLink(Core::getPHPDocLink($target));
  689. }
  690. /**
  691. * Displays a link to the documentation as an icon
  692. *
  693. * @param string $link documentation link
  694. * @param string $target optional link target
  695. * @param bool $bbcode optional flag indicating whether to output bbcode
  696. *
  697. * @return string the html link
  698. *
  699. * @access public
  700. */
  701. public static function showDocumentationLink($link, $target = 'documentation', $bbcode = false): string
  702. {
  703. if ($bbcode) {
  704. return '[a@' . $link . '@' . $target . '][dochelpicon][/a]';
  705. }
  706. return '<a href="' . $link . '" target="' . $target . '">'
  707. . self::getImage('b_help', __('Documentation'))
  708. . '</a>';
  709. }
  710. /**
  711. * Displays a MySQL error message in the main panel when $exit is true.
  712. * Returns the error message otherwise.
  713. *
  714. * @param string|bool $serverMessage Server's error message.
  715. * @param string $sqlQuery The SQL query that failed.
  716. * @param bool $isModifyLink Whether to show a "modify" link or not.
  717. * @param string $backUrl URL for the "back" link (full path is
  718. * not required).
  719. * @param bool $exit Whether execution should be stopped or
  720. * the error message should be returned.
  721. *
  722. * @global string $table The current table.
  723. * @global string $db The current database.
  724. *
  725. * @access public
  726. */
  727. public static function mysqlDie(
  728. $serverMessage = '',
  729. $sqlQuery = '',
  730. $isModifyLink = true,
  731. $backUrl = '',
  732. $exit = true
  733. ): ?string {
  734. global $table, $db, $dbi;
  735. /**
  736. * Error message to be built.
  737. *
  738. * @var string $errorMessage
  739. */
  740. $errorMessage = '';
  741. // Checking for any server errors.
  742. if (empty($serverMessage)) {
  743. $serverMessage = (string) $dbi->getError();
  744. }
  745. // Finding the query that failed, if not specified.
  746. if (empty($sqlQuery) && ! empty($GLOBALS['sql_query'])) {
  747. $sqlQuery = $GLOBALS['sql_query'];
  748. }
  749. $sqlQuery = trim($sqlQuery);
  750. /**
  751. * The lexer used for analysis.
  752. *
  753. * @var Lexer $lexer
  754. */
  755. $lexer = new Lexer($sqlQuery);
  756. /**
  757. * The parser used for analysis.
  758. *
  759. * @var Parser $parser
  760. */
  761. $parser = new Parser($lexer->list);
  762. /**
  763. * The errors found by the lexer and the parser.
  764. *
  765. * @var array $errors
  766. */
  767. $errors = ParserError::get(
  768. [
  769. $lexer,
  770. $parser,
  771. ]
  772. );
  773. if (empty($sqlQuery)) {
  774. $formattedSql = '';
  775. } elseif (count($errors)) {
  776. $formattedSql = htmlspecialchars($sqlQuery);
  777. } else {
  778. $formattedSql = self::formatSql($sqlQuery, true);
  779. }
  780. $errorMessage .= '<div class="alert alert-danger" role="alert"><h1>' . __('Error') . '</h1>';
  781. // For security reasons, if the MySQL refuses the connection, the query
  782. // is hidden so no details are revealed.
  783. if (! empty($sqlQuery) && ! mb_strstr($sqlQuery, 'connect')) {
  784. // Static analysis errors.
  785. if (! empty($errors)) {
  786. $errorMessage .= '<p><strong>' . __('Static analysis:')
  787. . '</strong></p>';
  788. $errorMessage .= '<p>' . sprintf(
  789. __('%d errors were found during analysis.'),
  790. count($errors)
  791. ) . '</p>';
  792. $errorMessage .= '<p><ol>';
  793. $errorMessage .= implode(
  794. ParserError::format(
  795. $errors,
  796. '<li>%2$s (near "%4$s" at position %5$d)</li>'
  797. )
  798. );
  799. $errorMessage .= '</ol></p>';
  800. }
  801. // Display the SQL query and link to MySQL documentation.
  802. $errorMessage .= '<p><strong>' . __('SQL query:') . '</strong>' . self::showCopyToClipboard(
  803. $sqlQuery
  804. ) . "\n";
  805. $formattedSqlToLower = mb_strtolower($formattedSql);
  806. // TODO: Show documentation for all statement types.
  807. if (mb_strstr($formattedSqlToLower, 'select')) {
  808. // please show me help to the error on select
  809. $errorMessage .= MySQLDocumentation::show('SELECT');
  810. }
  811. if ($isModifyLink) {
  812. $urlParams = [
  813. 'sql_query' => $sqlQuery,
  814. 'show_query' => 1,
  815. ];
  816. if (strlen($table) > 0) {
  817. $urlParams['db'] = $db;
  818. $urlParams['table'] = $table;
  819. $doEditGoto = '<a href="' . Url::getFromRoute('/table/sql', $urlParams) . '">';
  820. } elseif (strlen($db) > 0) {
  821. $urlParams['db'] = $db;
  822. $doEditGoto = '<a href="' . Url::getFromRoute('/database/sql', $urlParams) . '">';
  823. } else {
  824. $doEditGoto = '<a href="' . Url::getFromRoute('/server/sql', $urlParams) . '">';
  825. }
  826. $errorMessage .= $doEditGoto
  827. . self::getIcon('b_edit', __('Edit'))
  828. . '</a>';
  829. }
  830. $errorMessage .= ' </p>' . "\n"
  831. . '<p>' . "\n"
  832. . $formattedSql . "\n"
  833. . '</p>' . "\n";
  834. }
  835. // Display server's error.
  836. if (! empty($serverMessage)) {
  837. $serverMessage = (string) preg_replace("@((\015\012)|(\015)|(\012)){3,}@", "\n\n", (string) $serverMessage);
  838. // Adds a link to MySQL documentation.
  839. $errorMessage .= '<p>' . "\n"
  840. . ' <strong>' . __('MySQL said: ') . '</strong>'
  841. . MySQLDocumentation::show('server-error-reference')
  842. . "\n"
  843. . '</p>' . "\n";
  844. // The error message will be displayed within a CODE segment.
  845. // To preserve original formatting, but allow word-wrapping,
  846. // a couple of replacements are done.
  847. // All non-single blanks and TAB-characters are replaced with their
  848. // HTML-counterpart
  849. $serverMessage = str_replace(
  850. [
  851. ' ',
  852. "\t",
  853. ],
  854. [
  855. '&nbsp;&nbsp;',
  856. '&nbsp;&nbsp;&nbsp;&nbsp;',
  857. ],
  858. $serverMessage
  859. );
  860. // Replace line breaks
  861. $serverMessage = nl2br($serverMessage);
  862. $errorMessage .= '<code>' . $serverMessage . '</code><br>';
  863. }
  864. $errorMessage .= '</div>';
  865. $_SESSION['Import_message']['message'] = $errorMessage;
  866. if (! $exit) {
  867. return $errorMessage;
  868. }
  869. /**
  870. * If this is an AJAX request, there is no "Back" link and
  871. * `Response()` is used to send the response.
  872. */
  873. $response = ResponseRenderer::getInstance();
  874. if ($response->isAjax()) {
  875. $response->setRequestStatus(false);
  876. $response->addJSON('message', $errorMessage);
  877. exit;
  878. }
  879. if (! empty($backUrl)) {
  880. if (mb_strstr($backUrl, '?')) {
  881. $backUrl .= '&amp;no_history=true';
  882. } else {
  883. $backUrl .= '?no_history=true';
  884. }
  885. $_SESSION['Import_message']['go_back_url'] = $backUrl;
  886. $errorMessage .= '<fieldset class="pma-fieldset tblFooters">'
  887. . '[ <a href="' . $backUrl . '">' . __('Back') . '</a> ]'
  888. . '</fieldset>' . "\n\n";
  889. }
  890. exit($errorMessage);
  891. }
  892. /**
  893. * Returns an HTML IMG tag for a particular image from a theme
  894. *
  895. * The image name should match CSS class defined in icons.css.php
  896. *
  897. * @param string $image The name of the file to get
  898. * @param string $alternate Used to set 'alt' and 'title' attributes
  899. * of the image
  900. * @param array $attributes An associative array of other attributes
  901. *
  902. * @return string an html IMG tag
  903. */
  904. public static function getImage($image, $alternate = '', array $attributes = []): string
  905. {
  906. $alternate = htmlspecialchars($alternate);
  907. if (isset($attributes['class'])) {
  908. $attributes['class'] = 'icon ic_' . $image . ' ' . $attributes['class'];
  909. } else {
  910. $attributes['class'] = 'icon ic_' . $image;
  911. }
  912. // set all other attributes
  913. $attributeString = '';
  914. foreach ($attributes as $key => $value) {
  915. if (in_array($key, ['alt', 'title'])) {
  916. continue;
  917. }
  918. $attributeString .= ' ' . $key . '="' . $value . '"';
  919. }
  920. // override the alt attribute
  921. $alt = $attributes['alt'] ?? $alternate;
  922. // override the title attribute
  923. $title = $attributes['title'] ?? $alternate;
  924. // generate the IMG tag
  925. $template = '<img src="themes/dot.gif" title="%s" alt="%s"%s>';
  926. return sprintf($template, $title, $alt, $attributeString);
  927. }
  928. /**
  929. * Displays a link, or a link with code to trigger POST request.
  930. *
  931. * POST is used in following cases:
  932. *
  933. * - URL is too long
  934. * - URL components are over Suhosin limits
  935. * - There is SQL query in the parameters
  936. *
  937. * @param string $url the URL
  938. * @param string $message the link message
  939. * @param mixed $tagParams string: js confirmation; array: additional tag
  940. * params (f.e. style="")
  941. * @param string $target target
  942. *
  943. * @return string the results to be echoed or saved in an array
  944. */
  945. public static function linkOrButton(
  946. $url,
  947. $message,
  948. $tagParams = [],
  949. $target = '',
  950. bool $respectUrlLengthLimit = true
  951. ): string {
  952. $urlLength = strlen($url);
  953. if (! is_array($tagParams)) {
  954. $tmp = $tagParams;
  955. $tagParams = [];
  956. if (! empty($tmp)) {
  957. $tagParams['onclick'] = 'return Functions.confirmLink(this, \''
  958. . Sanitize::escapeJsString($tmp) . '\')';
  959. }
  960. unset($tmp);
  961. }
  962. if (! empty($target)) {
  963. $tagParams['target'] = $target;
  964. if ($target === '_blank' && str_starts_with($url, 'url.php?')) {
  965. $tagParams['rel'] = 'noopener noreferrer';
  966. }
  967. }
  968. // Suhosin: Check that each query parameter is not above maximum
  969. $inSuhosinLimits = true;
  970. if ($urlLength <= $GLOBALS['cfg']['LinkLengthLimit']) {
  971. $suhosinGetMaxValueLength = ini_get('suhosin.get.max_value_length');
  972. if ($suhosinGetMaxValueLength) {
  973. $queryParts = Util::splitURLQuery($url);
  974. foreach ($queryParts as $queryPair) {
  975. if (! str_contains($queryPair, '=')) {
  976. continue;
  977. }
  978. [, $eachValue] = explode('=', $queryPair);
  979. if (strlen($eachValue) > $suhosinGetMaxValueLength) {
  980. $inSuhosinLimits = false;
  981. break;
  982. }
  983. }
  984. }
  985. }
  986. $tagParamsStrings = [];
  987. $isDataPostFormatSupported = ($urlLength > $GLOBALS['cfg']['LinkLengthLimit'])
  988. || ! $inSuhosinLimits
  989. // Has as sql_query without a signature, to be accepted it needs
  990. // to be sent using POST
  991. || (
  992. str_contains($url, 'sql_query=')
  993. && ! str_contains($url, 'sql_signature=')
  994. )
  995. || str_contains($url, 'view[as]=');
  996. if ($respectUrlLengthLimit && $isDataPostFormatSupported) {
  997. $parts = explode('?', $url, 2);
  998. /*
  999. * The data-post indicates that client should do POST
  1000. * this is handled in js/ajax.js
  1001. */
  1002. $tagParamsStrings[] = 'data-post="' . ($parts[1] ?? '') . '"';
  1003. $url = $parts[0];
  1004. if (array_key_exists('class', $tagParams) && str_contains($tagParams['class'], 'create_view')) {
  1005. $url .= '?' . explode('&', $parts[1], 2)[0];
  1006. }
  1007. }
  1008. foreach ($tagParams as $paramName => $paramValue) {
  1009. $tagParamsStrings[] = $paramName . '="' . htmlspecialchars($paramValue) . '"';
  1010. }
  1011. // no whitespace within an <a> else Safari will make it part of the link
  1012. return '<a href="' . $url . '" '
  1013. . implode(' ', $tagParamsStrings) . '>'
  1014. . $message . '</a>';
  1015. }
  1016. /**
  1017. * Prepare navigation for a list
  1018. *
  1019. * @param int $count number of elements in the list
  1020. * @param int $pos current position in the list
  1021. * @param array $urlParams url parameters
  1022. * @param string $script script name for form target
  1023. * @param string $frame target frame
  1024. * @param int $maxCount maximum number of elements to display from
  1025. * the list
  1026. * @param string $name the name for the request parameter
  1027. * @param string[] $classes additional classes for the container
  1028. *
  1029. * @return string the html content
  1030. *
  1031. * @access public
  1032. *
  1033. * @todo use $pos from $_url_params
  1034. */
  1035. public static function getListNavigator(
  1036. $count,
  1037. $pos,
  1038. array $urlParams,
  1039. $script,
  1040. $frame,
  1041. $maxCount,
  1042. $name = 'pos',
  1043. $classes = []
  1044. ): string {
  1045. // This is often coming from $cfg['MaxTableList'] and
  1046. // people sometimes set it to empty string
  1047. $maxCount = intval($maxCount);
  1048. if ($maxCount <= 0) {
  1049. $maxCount = 250;
  1050. }
  1051. $class = $frame === 'frame_navigation' ? ' class="ajax"' : '';
  1052. $listNavigatorHtml = '';
  1053. if ($maxCount < $count) {
  1054. $classes[] = 'pageselector';
  1055. $listNavigatorHtml .= '<div class="' . implode(' ', $classes) . '">';
  1056. if ($frame !== 'frame_navigation') {
  1057. $listNavigatorHtml .= __('Page number:');
  1058. }
  1059. // Move to the beginning or to the previous page
  1060. if ($pos > 0) {
  1061. $caption1 = '';
  1062. $caption2 = '';
  1063. if (Util::showIcons('TableNavigationLinksMode')) {
  1064. $caption1 .= '&lt;&lt; ';
  1065. $caption2 .= '&lt; ';
  1066. }
  1067. if (Util::showText('TableNavigationLinksMode')) {
  1068. $caption1 .= _pgettext('First page', 'Begin');
  1069. $caption2 .= _pgettext('Previous page', 'Previous');
  1070. }
  1071. $title1 = ' title="' . _pgettext('First page', 'Begin') . '"';
  1072. $title2 = ' title="' . _pgettext('Previous page', 'Previous') . '"';
  1073. $urlParams[$name] = 0;
  1074. $listNavigatorHtml .= '<a' . $class . $title1 . ' href="' . $script
  1075. . Url::getCommon($urlParams, '&') . '">' . $caption1
  1076. . '</a>';
  1077. $urlParams[$name] = $pos - $maxCount;
  1078. $listNavigatorHtml .= ' <a' . $class . $title2
  1079. . ' href="' . $script . Url::getCommon($urlParams, '&') . '">'
  1080. . $caption2 . '</a>';
  1081. }
  1082. $listNavigatorHtml .= '<form action="' . $script
  1083. . '" method="post">';
  1084. $listNavigatorHtml .= Url::getHiddenInputs($urlParams);
  1085. $listNavigatorHtml .= Util::pageselector(
  1086. $name,
  1087. $maxCount,
  1088. Util::getPageFromPosition($pos, $maxCount),
  1089. (int) ceil($count / $maxCount)
  1090. );
  1091. $listNavigatorHtml .= '</form>';
  1092. if ($pos + $maxCount < $count) {
  1093. $caption3 = '';
  1094. $caption4 = '';
  1095. if (Util::showText('TableNavigationLinksMode')) {
  1096. $caption3 .= _pgettext('Next page', 'Next');
  1097. $caption4 .= _pgettext('Last page', 'End');
  1098. }
  1099. if (Util::showIcons('TableNavigationLinksMode')) {
  1100. $caption3 .= ' &gt;';
  1101. $caption4 .= ' &gt;&gt;';
  1102. }
  1103. $title3 = ' title="' . _pgettext('Next page', 'Next') . '"';
  1104. $title4 = ' title="' . _pgettext('Last page', 'End') . '"';
  1105. $urlParams[$name] = $pos + $maxCount;
  1106. $listNavigatorHtml .= '<a' . $class . $title3 . ' href="' . $script
  1107. . Url::getCommon($urlParams, '&') . '" >' . $caption3
  1108. . '</a>';
  1109. $urlParams[$name] = floor($count / $maxCount) * $maxCount;
  1110. if ($urlParams[$name] == $count) {
  1111. $urlParams[$name] = $count - $maxCount;
  1112. }
  1113. $listNavigatorHtml .= ' <a' . $class . $title4
  1114. . ' href="' . $script . Url::getCommon($urlParams, '&') . '" >'
  1115. . $caption4 . '</a>';
  1116. }
  1117. $listNavigatorHtml .= '</div>' . "\n";
  1118. }
  1119. return $listNavigatorHtml;
  1120. }
  1121. /**
  1122. * format sql strings
  1123. *
  1124. * @param string $sqlQuery raw SQL string
  1125. * @param bool $truncate truncate the query if it is too long
  1126. *
  1127. * @return string the formatted sql
  1128. *
  1129. * @global array $cfg the configuration array
  1130. *
  1131. * @access public
  1132. */
  1133. public static function formatSql($sqlQuery, $truncate = false): string
  1134. {
  1135. global $cfg;
  1136. if ($truncate && mb_strlen($sqlQuery) > $cfg['MaxCharactersInDisplayedSQL']) {
  1137. $sqlQuery = mb_substr($sqlQuery, 0, $cfg['MaxCharactersInDisplayedSQL']) . '[...]';
  1138. }
  1139. return '<code class="sql"><pre>' . "\n"
  1140. . htmlspecialchars($sqlQuery, ENT_COMPAT) . "\n"
  1141. . '</pre></code>';
  1142. }
  1143. /**
  1144. * This function processes the datatypes supported by the DB,
  1145. * as specified in Types->getColumns() and returns an HTML snippet that
  1146. * creates a drop-down list.
  1147. *
  1148. * @param string $selected The value to mark as selected in HTML mode
  1149. */
  1150. public static function getSupportedDatatypes($selected): string
  1151. {
  1152. global $dbi;
  1153. // NOTE: the SELECT tag is not included in this snippet.
  1154. $retval = '';
  1155. foreach ($dbi->types->getColumns() as $key => $value) {
  1156. if (is_array($value)) {
  1157. $retval .= '<optgroup label="' . htmlspecialchars($key) . '">';
  1158. foreach ($value as $subvalue) {
  1159. if ($subvalue === '-') {
  1160. $retval .= '<option disabled="disabled">';
  1161. $retval .= $subvalue;
  1162. $retval .= '</option>';
  1163. continue;
  1164. }
  1165. $isLengthRestricted = Compatibility::isIntegersSupportLength($subvalue, '2', $dbi);
  1166. $retval .= sprintf(
  1167. '<option data-length-restricted="%b" %s title="%s">%s</option>',
  1168. $isLengthRestricted ? 0 : 1,
  1169. $selected === $subvalue ? 'selected="selected"' : '',
  1170. $dbi->types->getTypeDescription($subvalue),
  1171. $subvalue
  1172. );
  1173. }
  1174. $retval .= '</optgroup>';
  1175. continue;
  1176. }
  1177. $isLengthRestricted = Compatibility::isIntegersSupportLength($value, '2', $dbi);
  1178. $retval .= sprintf(
  1179. '<option data-length-restricted="%b" %s title="%s">%s</option>',
  1180. $isLengthRestricted ? 0 : 1,
  1181. $selected === $value ? 'selected="selected"' : '',
  1182. $dbi->types->getTypeDescription($value),
  1183. $value
  1184. );
  1185. }
  1186. return $retval;
  1187. }
  1188. }