Generator.php 47 KB

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