Generator.php 43 KB

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