PageRenderTime 48ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/libraries/classes/Database/Routines.php

http://github.com/phpmyadmin/phpmyadmin
PHP | 1612 lines | 1172 code | 214 blank | 226 comment | 221 complexity | 2f19d28208e549717bbd0a8cd2d06270 MD5 | raw file
Possible License(s): GPL-2.0, MIT, LGPL-3.0

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin\Database;
  4. use PhpMyAdmin\Charsets;
  5. use PhpMyAdmin\Charsets\Charset;
  6. use PhpMyAdmin\DatabaseInterface;
  7. use PhpMyAdmin\Html\Generator;
  8. use PhpMyAdmin\Message;
  9. use PhpMyAdmin\ResponseRenderer;
  10. use PhpMyAdmin\SqlParser\Parser;
  11. use PhpMyAdmin\SqlParser\Statements\CreateStatement;
  12. use PhpMyAdmin\SqlParser\TokensList;
  13. use PhpMyAdmin\SqlParser\Utils\Routine;
  14. use PhpMyAdmin\Template;
  15. use PhpMyAdmin\Util;
  16. use function __;
  17. use function _ngettext;
  18. use function array_merge;
  19. use function count;
  20. use function explode;
  21. use function htmlentities;
  22. use function htmlspecialchars;
  23. use function implode;
  24. use function in_array;
  25. use function is_array;
  26. use function is_string;
  27. use function max;
  28. use function mb_strtolower;
  29. use function mb_strtoupper;
  30. use function preg_match;
  31. use function sprintf;
  32. use function str_contains;
  33. use function stripos;
  34. use function substr;
  35. use function trim;
  36. use const ENT_QUOTES;
  37. /**
  38. * Functions for routine management.
  39. */
  40. class Routines
  41. {
  42. /** @var array<int, string> */
  43. private $directions = ['IN', 'OUT', 'INOUT'];
  44. /** @var array<int, string> */
  45. private $sqlDataAccess = ['CONTAINS SQL', 'NO SQL', 'READS SQL DATA', 'MODIFIES SQL DATA'];
  46. /** @var array<int, string> */
  47. private $numericOptions = ['UNSIGNED', 'ZEROFILL', 'UNSIGNED ZEROFILL'];
  48. /** @var DatabaseInterface */
  49. private $dbi;
  50. /** @var Template */
  51. private $template;
  52. /** @var ResponseRenderer */
  53. private $response;
  54. /**
  55. * @param DatabaseInterface $dbi DatabaseInterface instance.
  56. * @param Template $template Template instance.
  57. * @param ResponseRenderer $response Response instance.
  58. */
  59. public function __construct(DatabaseInterface $dbi, Template $template, $response)
  60. {
  61. $this->dbi = $dbi;
  62. $this->template = $template;
  63. $this->response = $response;
  64. }
  65. /**
  66. * Handles editor requests for adding or editing an item
  67. */
  68. public function handleEditor(): void
  69. {
  70. global $db, $errors;
  71. $errors = $this->handleRequestCreateOrEdit($errors, $db);
  72. /**
  73. * Display a form used to add/edit a routine, if necessary
  74. */
  75. // FIXME: this must be simpler than that
  76. if (
  77. ! count($errors)
  78. && ( ! empty($_POST['editor_process_add'])
  79. || ! empty($_POST['editor_process_edit'])
  80. || (empty($_REQUEST['add_item']) && empty($_REQUEST['edit_item'])
  81. && empty($_POST['routine_addparameter'])
  82. && empty($_POST['routine_removeparameter'])
  83. && empty($_POST['routine_changetype'])))
  84. ) {
  85. return;
  86. }
  87. // Handle requests to add/remove parameters and changing routine type
  88. // This is necessary when JS is disabled
  89. $operation = '';
  90. if (! empty($_POST['routine_addparameter'])) {
  91. $operation = 'add';
  92. } elseif (! empty($_POST['routine_removeparameter'])) {
  93. $operation = 'remove';
  94. } elseif (! empty($_POST['routine_changetype'])) {
  95. $operation = 'change';
  96. }
  97. // Get the data for the form (if any)
  98. $routine = null;
  99. $mode = null;
  100. $title = null;
  101. if (! empty($_REQUEST['add_item'])) {
  102. $title = __('Add routine');
  103. $routine = $this->getDataFromRequest();
  104. $mode = 'add';
  105. } elseif (! empty($_REQUEST['edit_item'])) {
  106. $title = __('Edit routine');
  107. if (! $operation && ! empty($_GET['item_name']) && empty($_POST['editor_process_edit'])) {
  108. $routine = $this->getDataFromName($_GET['item_name'], $_GET['item_type']);
  109. if ($routine !== null) {
  110. $routine['item_original_name'] = $routine['item_name'];
  111. $routine['item_original_type'] = $routine['item_type'];
  112. }
  113. } else {
  114. $routine = $this->getDataFromRequest();
  115. }
  116. $mode = 'edit';
  117. }
  118. if ($routine !== null) {
  119. // Show form
  120. $editor = $this->getEditorForm($mode, $operation, $routine);
  121. if ($this->response->isAjax()) {
  122. $this->response->addJSON('message', $editor);
  123. $this->response->addJSON('title', $title);
  124. $this->response->addJSON('paramTemplate', $this->getParameterRow());
  125. $this->response->addJSON('type', $routine['item_type']);
  126. } else {
  127. echo "\n\n<h2>" . $title . "</h2>\n\n" . $editor;
  128. }
  129. exit;
  130. }
  131. $message = __('Error in processing request:') . ' ';
  132. $message .= sprintf(
  133. __(
  134. 'No routine with name %1$s found in database %2$s. '
  135. . 'You might be lacking the necessary privileges to edit this routine.'
  136. ),
  137. htmlspecialchars(
  138. Util::backquote($_REQUEST['item_name'])
  139. ),
  140. htmlspecialchars(Util::backquote($db))
  141. );
  142. $message = Message::error($message);
  143. if ($this->response->isAjax()) {
  144. $this->response->setRequestStatus(false);
  145. $this->response->addJSON('message', $message);
  146. exit;
  147. }
  148. echo $message->getDisplay();
  149. }
  150. /**
  151. * Handle request to create or edit a routine
  152. *
  153. * @param array $errors Errors
  154. * @param string $db DB name
  155. *
  156. * @return array
  157. */
  158. public function handleRequestCreateOrEdit(array $errors, $db)
  159. {
  160. global $message;
  161. if (empty($_POST['editor_process_add']) && empty($_POST['editor_process_edit'])) {
  162. return $errors;
  163. }
  164. $sql_query = '';
  165. $routine_query = $this->getQueryFromRequest();
  166. // set by getQueryFromRequest()
  167. if (! count($errors)) {
  168. // Execute the created query
  169. if (! empty($_POST['editor_process_edit'])) {
  170. $isProcOrFunc = in_array(
  171. $_POST['item_original_type'],
  172. [
  173. 'PROCEDURE',
  174. 'FUNCTION',
  175. ]
  176. );
  177. if (! $isProcOrFunc) {
  178. $errors[] = sprintf(
  179. __('Invalid routine type: "%s"'),
  180. htmlspecialchars($_POST['item_original_type'])
  181. );
  182. } else {
  183. // Backup the old routine, in case something goes wrong
  184. $create_routine = $this->dbi->getDefinition(
  185. $db,
  186. $_POST['item_original_type'],
  187. $_POST['item_original_name']
  188. );
  189. $privilegesBackup = $this->backupPrivileges();
  190. $drop_routine = 'DROP ' . $_POST['item_original_type'] . ' '
  191. . Util::backquote($_POST['item_original_name'])
  192. . ";\n";
  193. $result = $this->dbi->tryQuery($drop_routine);
  194. if (! $result) {
  195. $errors[] = sprintf(
  196. __('The following query has failed: "%s"'),
  197. htmlspecialchars($drop_routine)
  198. )
  199. . '<br>'
  200. . __('MySQL said: ') . $this->dbi->getError();
  201. } else {
  202. [$newErrors, $message] = $this->create($routine_query, $create_routine, $privilegesBackup);
  203. if (empty($newErrors)) {
  204. $sql_query = $drop_routine . $routine_query;
  205. } else {
  206. $errors = array_merge($errors, $newErrors);
  207. }
  208. unset($newErrors);
  209. }
  210. }
  211. } else {
  212. // 'Add a new routine' mode
  213. $result = $this->dbi->tryQuery($routine_query);
  214. if (! $result) {
  215. $errors[] = sprintf(
  216. __('The following query has failed: "%s"'),
  217. htmlspecialchars($routine_query)
  218. )
  219. . '<br><br>'
  220. . __('MySQL said: ') . $this->dbi->getError();
  221. } else {
  222. $message = Message::success(
  223. __('Routine %1$s has been created.')
  224. );
  225. $message->addParam(
  226. Util::backquote($_POST['item_name'])
  227. );
  228. $sql_query = $routine_query;
  229. }
  230. }
  231. }
  232. if (count($errors)) {
  233. $message = Message::error(
  234. __(
  235. 'One or more errors have occurred while processing your request:'
  236. )
  237. );
  238. $message->addHtml('<ul>');
  239. foreach ($errors as $string) {
  240. $message->addHtml('<li>' . $string . '</li>');
  241. }
  242. $message->addHtml('</ul>');
  243. }
  244. $output = Generator::getMessage($message, $sql_query);
  245. if (! $this->response->isAjax()) {
  246. return $errors;
  247. }
  248. if (! $message->isSuccess()) {
  249. $this->response->setRequestStatus(false);
  250. $this->response->addJSON('message', $output);
  251. exit;
  252. }
  253. $routines = $this->dbi->getRoutines($db, $_POST['item_type'], $_POST['item_name']);
  254. $routine = $routines[0];
  255. $this->response->addJSON(
  256. 'name',
  257. htmlspecialchars(
  258. mb_strtoupper($_POST['item_name'])
  259. )
  260. );
  261. $this->response->addJSON('new_row', $this->getRow($routine));
  262. $this->response->addJSON('insert', ! empty($routine));
  263. $this->response->addJSON('message', $output);
  264. $this->response->addJSON('tableType', 'routines');
  265. exit;
  266. }
  267. /**
  268. * Backup the privileges
  269. *
  270. * @return array
  271. */
  272. public function backupPrivileges()
  273. {
  274. if (! $GLOBALS['proc_priv'] || ! $GLOBALS['is_reload_priv']) {
  275. return [];
  276. }
  277. // Backup the Old Privileges before dropping
  278. // if $_POST['item_adjust_privileges'] set
  279. if (! isset($_POST['item_adjust_privileges']) || empty($_POST['item_adjust_privileges'])) {
  280. return [];
  281. }
  282. $privilegesBackupQuery = 'SELECT * FROM ' . Util::backquote('mysql')
  283. . '.' . Util::backquote('procs_priv')
  284. . ' where Routine_name = "' . $_POST['item_original_name']
  285. . '" AND Routine_type = "' . $_POST['item_original_type']
  286. . '";';
  287. return $this->dbi->fetchResult($privilegesBackupQuery, 0);
  288. }
  289. /**
  290. * Create the routine
  291. *
  292. * @param string $routine_query Query to create routine
  293. * @param string $create_routine Query to restore routine
  294. * @param array $privilegesBackup Privileges backup
  295. *
  296. * @return array
  297. */
  298. public function create(
  299. $routine_query,
  300. $create_routine,
  301. array $privilegesBackup
  302. ) {
  303. $result = $this->dbi->tryQuery($routine_query);
  304. if (! $result) {
  305. $errors = [];
  306. $errors[] = sprintf(
  307. __('The following query has failed: "%s"'),
  308. htmlspecialchars($routine_query)
  309. )
  310. . '<br>'
  311. . __('MySQL said: ') . $this->dbi->getError();
  312. // We dropped the old routine,
  313. // but were unable to create the new one
  314. // Try to restore the backup query
  315. $result = $this->dbi->tryQuery($create_routine);
  316. $errors = $this->checkResult($result, $create_routine, $errors);
  317. return [
  318. $errors,
  319. null,
  320. ];
  321. }
  322. // Default value
  323. $resultAdjust = false;
  324. if ($GLOBALS['proc_priv'] && $GLOBALS['is_reload_priv']) {
  325. // Insert all the previous privileges
  326. // but with the new name and the new type
  327. foreach ($privilegesBackup as $priv) {
  328. $adjustProcPrivilege = 'INSERT INTO '
  329. . Util::backquote('mysql') . '.'
  330. . Util::backquote('procs_priv')
  331. . ' VALUES("' . $priv[0] . '", "'
  332. . $priv[1] . '", "' . $priv[2] . '", "'
  333. . $_POST['item_name'] . '", "'
  334. . $_POST['item_type'] . '", "'
  335. . $priv[5] . '", "'
  336. . $priv[6] . '", "'
  337. . $priv[7] . '");';
  338. $resultAdjust = $this->dbi->query($adjustProcPrivilege);
  339. }
  340. }
  341. $message = $this->flushPrivileges($resultAdjust);
  342. return [
  343. [],
  344. $message,
  345. ];
  346. }
  347. /**
  348. * Flush privileges and get message
  349. *
  350. * @param bool $flushPrivileges Flush privileges
  351. *
  352. * @return Message
  353. */
  354. public function flushPrivileges($flushPrivileges)
  355. {
  356. if ($flushPrivileges) {
  357. // Flush the Privileges
  358. $flushPrivQuery = 'FLUSH PRIVILEGES;';
  359. $this->dbi->query($flushPrivQuery);
  360. $message = Message::success(
  361. __(
  362. 'Routine %1$s has been modified. Privileges have been adjusted.'
  363. )
  364. );
  365. } else {
  366. $message = Message::success(
  367. __('Routine %1$s has been modified.')
  368. );
  369. }
  370. $message->addParam(
  371. Util::backquote($_POST['item_name'])
  372. );
  373. return $message;
  374. }
  375. /**
  376. * This function will generate the values that are required to
  377. * complete the editor form. It is especially necessary to handle
  378. * the 'Add another parameter', 'Remove last parameter' and
  379. * 'Change routine type' functionalities when JS is disabled.
  380. *
  381. * @return array Data necessary to create the routine editor.
  382. */
  383. public function getDataFromRequest()
  384. {
  385. $retval = [];
  386. $indices = [
  387. 'item_name',
  388. 'item_original_name',
  389. 'item_returnlength',
  390. 'item_returnopts_num',
  391. 'item_returnopts_text',
  392. 'item_definition',
  393. 'item_comment',
  394. 'item_definer',
  395. ];
  396. foreach ($indices as $index) {
  397. $retval[$index] = $_POST[$index] ?? '';
  398. }
  399. $retval['item_type'] = 'PROCEDURE';
  400. $retval['item_type_toggle'] = 'FUNCTION';
  401. if (isset($_POST['item_type']) && $_POST['item_type'] === 'FUNCTION') {
  402. $retval['item_type'] = 'FUNCTION';
  403. $retval['item_type_toggle'] = 'PROCEDURE';
  404. }
  405. $retval['item_original_type'] = 'PROCEDURE';
  406. if (isset($_POST['item_original_type']) && $_POST['item_original_type'] === 'FUNCTION') {
  407. $retval['item_original_type'] = 'FUNCTION';
  408. }
  409. $retval['item_num_params'] = 0;
  410. $retval['item_param_dir'] = [];
  411. $retval['item_param_name'] = [];
  412. $retval['item_param_type'] = [];
  413. $retval['item_param_length'] = [];
  414. $retval['item_param_opts_num'] = [];
  415. $retval['item_param_opts_text'] = [];
  416. if (
  417. isset($_POST['item_param_name'], $_POST['item_param_type'])
  418. && isset($_POST['item_param_length'])
  419. && isset($_POST['item_param_opts_num'])
  420. && isset($_POST['item_param_opts_text'])
  421. && is_array($_POST['item_param_name'])
  422. && is_array($_POST['item_param_type'])
  423. && is_array($_POST['item_param_length'])
  424. && is_array($_POST['item_param_opts_num'])
  425. && is_array($_POST['item_param_opts_text'])
  426. ) {
  427. if ($_POST['item_type'] === 'PROCEDURE') {
  428. $retval['item_param_dir'] = $_POST['item_param_dir'];
  429. foreach ($retval['item_param_dir'] as $key => $value) {
  430. if (in_array($value, $this->directions, true)) {
  431. continue;
  432. }
  433. $retval['item_param_dir'][$key] = '';
  434. }
  435. }
  436. $retval['item_param_name'] = $_POST['item_param_name'];
  437. $retval['item_param_type'] = $_POST['item_param_type'];
  438. foreach ($retval['item_param_type'] as $key => $value) {
  439. if (in_array($value, Util::getSupportedDatatypes(), true)) {
  440. continue;
  441. }
  442. $retval['item_param_type'][$key] = '';
  443. }
  444. $retval['item_param_length'] = $_POST['item_param_length'];
  445. $retval['item_param_opts_num'] = $_POST['item_param_opts_num'];
  446. $retval['item_param_opts_text'] = $_POST['item_param_opts_text'];
  447. $retval['item_num_params'] = max(
  448. count($retval['item_param_name']),
  449. count($retval['item_param_type']),
  450. count($retval['item_param_length']),
  451. count($retval['item_param_opts_num']),
  452. count($retval['item_param_opts_text'])
  453. );
  454. }
  455. $retval['item_returntype'] = '';
  456. if (isset($_POST['item_returntype']) && in_array($_POST['item_returntype'], Util::getSupportedDatatypes())) {
  457. $retval['item_returntype'] = $_POST['item_returntype'];
  458. }
  459. $retval['item_isdeterministic'] = '';
  460. if (isset($_POST['item_isdeterministic']) && mb_strtolower($_POST['item_isdeterministic']) === 'on') {
  461. $retval['item_isdeterministic'] = " checked='checked'";
  462. }
  463. $retval['item_securitytype_definer'] = '';
  464. $retval['item_securitytype_invoker'] = '';
  465. if (isset($_POST['item_securitytype'])) {
  466. if ($_POST['item_securitytype'] === 'DEFINER') {
  467. $retval['item_securitytype_definer'] = " selected='selected'";
  468. } elseif ($_POST['item_securitytype'] === 'INVOKER') {
  469. $retval['item_securitytype_invoker'] = " selected='selected'";
  470. }
  471. }
  472. $retval['item_sqldataaccess'] = '';
  473. if (isset($_POST['item_sqldataaccess']) && in_array($_POST['item_sqldataaccess'], $this->sqlDataAccess, true)) {
  474. $retval['item_sqldataaccess'] = $_POST['item_sqldataaccess'];
  475. }
  476. return $retval;
  477. }
  478. /**
  479. * This function will generate the values that are required to complete
  480. * the "Edit routine" form given the name of a routine.
  481. *
  482. * @param string $name The name of the routine.
  483. * @param string $type Type of routine (ROUTINE|PROCEDURE)
  484. * @param bool $all Whether to return all data or just the info about parameters.
  485. *
  486. * @return array|null Data necessary to create the routine editor.
  487. */
  488. public function getDataFromName($name, $type, $all = true): ?array
  489. {
  490. global $db;
  491. $retval = [];
  492. // Build and execute the query
  493. $fields = 'SPECIFIC_NAME, ROUTINE_TYPE, DTD_IDENTIFIER, '
  494. . 'ROUTINE_DEFINITION, IS_DETERMINISTIC, SQL_DATA_ACCESS, '
  495. . 'ROUTINE_COMMENT, SECURITY_TYPE';
  496. $where = 'ROUTINE_SCHEMA ' . Util::getCollateForIS() . '='
  497. . "'" . $this->dbi->escapeString($db) . "' "
  498. . "AND SPECIFIC_NAME='" . $this->dbi->escapeString($name) . "'"
  499. . "AND ROUTINE_TYPE='" . $this->dbi->escapeString($type) . "'";
  500. $query = 'SELECT ' . $fields . ' FROM INFORMATION_SCHEMA.ROUTINES WHERE ' . $where . ';';
  501. $routine = $this->dbi->fetchSingleRow($query, 'ASSOC');
  502. if (! $routine) {
  503. return null;
  504. }
  505. // Get required data
  506. $retval['item_name'] = $routine['SPECIFIC_NAME'];
  507. $retval['item_type'] = $routine['ROUTINE_TYPE'];
  508. $definition = $this->dbi->getDefinition($db, $routine['ROUTINE_TYPE'], $routine['SPECIFIC_NAME']);
  509. if ($definition === null) {
  510. return null;
  511. }
  512. $parser = new Parser($definition);
  513. /**
  514. * @var CreateStatement $stmt
  515. */
  516. $stmt = $parser->statements[0];
  517. // Do not use $routine['ROUTINE_DEFINITION'] because of a MySQL escaping issue: #15370
  518. $body = TokensList::build($stmt->body);
  519. if (empty($body)) {
  520. // Fallback just in case the parser fails
  521. $body = (string) $routine['ROUTINE_DEFINITION'];
  522. }
  523. $params = Routine::getParameters($stmt);
  524. $retval['item_num_params'] = $params['num'];
  525. $retval['item_param_dir'] = $params['dir'];
  526. $retval['item_param_name'] = $params['name'];
  527. $retval['item_param_type'] = $params['type'];
  528. $retval['item_param_length'] = $params['length'];
  529. $retval['item_param_length_arr'] = $params['length_arr'];
  530. $retval['item_param_opts_num'] = $params['opts'];
  531. $retval['item_param_opts_text'] = $params['opts'];
  532. // Get extra data
  533. if (! $all) {
  534. return $retval;
  535. }
  536. if ($retval['item_type'] === 'FUNCTION') {
  537. $retval['item_type_toggle'] = 'PROCEDURE';
  538. } else {
  539. $retval['item_type_toggle'] = 'FUNCTION';
  540. }
  541. $retval['item_returntype'] = '';
  542. $retval['item_returnlength'] = '';
  543. $retval['item_returnopts_num'] = '';
  544. $retval['item_returnopts_text'] = '';
  545. if (! empty($routine['DTD_IDENTIFIER'])) {
  546. $options = [];
  547. foreach ($stmt->return->options->options as $opt) {
  548. $options[] = is_string($opt) ? $opt : $opt['value'];
  549. }
  550. $retval['item_returntype'] = $stmt->return->name;
  551. $retval['item_returnlength'] = implode(',', $stmt->return->parameters);
  552. $retval['item_returnopts_num'] = implode(' ', $options);
  553. $retval['item_returnopts_text'] = implode(' ', $options);
  554. }
  555. $retval['item_definer'] = $stmt->options->has('DEFINER');
  556. $retval['item_definition'] = $body;
  557. $retval['item_isdeterministic'] = '';
  558. if ($routine['IS_DETERMINISTIC'] === 'YES') {
  559. $retval['item_isdeterministic'] = " checked='checked'";
  560. }
  561. $retval['item_securitytype_definer'] = '';
  562. $retval['item_securitytype_invoker'] = '';
  563. if ($routine['SECURITY_TYPE'] === 'DEFINER') {
  564. $retval['item_securitytype_definer'] = " selected='selected'";
  565. } elseif ($routine['SECURITY_TYPE'] === 'INVOKER') {
  566. $retval['item_securitytype_invoker'] = " selected='selected'";
  567. }
  568. $retval['item_sqldataaccess'] = $routine['SQL_DATA_ACCESS'];
  569. $retval['item_comment'] = $routine['ROUTINE_COMMENT'];
  570. return $retval;
  571. }
  572. /**
  573. * Creates one row for the parameter table used in the routine editor.
  574. *
  575. * @param array $routine Data for the routine returned by
  576. * getDataFromRequest() or getDataFromName()
  577. * @param mixed $index Either a numeric index of the row being processed
  578. * or NULL to create a template row for AJAX request
  579. * @param string $class Class used to hide the direction column, if the
  580. * row is for a stored function.
  581. *
  582. * @return string HTML code of one row of parameter table for the editor.
  583. */
  584. public function getParameterRow(array $routine = [], $index = null, $class = '')
  585. {
  586. if ($index === null) {
  587. // template row for AJAX request
  588. $i = 0;
  589. $index = '%s';
  590. $drop_class = '';
  591. $routine = [
  592. 'item_param_dir' => [0 => ''],
  593. 'item_param_name' => [0 => ''],
  594. 'item_param_type' => [0 => ''],
  595. 'item_param_length' => [0 => ''],
  596. 'item_param_opts_num' => [0 => ''],
  597. 'item_param_opts_text' => [0 => ''],
  598. ];
  599. } elseif (! empty($routine)) {
  600. // regular row for routine editor
  601. $drop_class = ' hide';
  602. $i = $index;
  603. } else {
  604. // No input data. This shouldn't happen,
  605. // but better be safe than sorry.
  606. return '';
  607. }
  608. $allCharsets = Charsets::getCharsets($this->dbi, $GLOBALS['cfg']['Server']['DisableIS']);
  609. $charsets = [];
  610. /** @var Charset $charset */
  611. foreach ($allCharsets as $charset) {
  612. $charsets[] = [
  613. 'name' => $charset->getName(),
  614. 'description' => $charset->getDescription(),
  615. 'is_selected' => $charset->getName() === $routine['item_param_opts_text'][$i],
  616. ];
  617. }
  618. return $this->template->render('database/routines/parameter_row', [
  619. 'class' => $class,
  620. 'index' => $index,
  621. 'param_directions' => $this->directions,
  622. 'param_opts_num' => $this->numericOptions,
  623. 'item_param_dir' => $routine['item_param_dir'][$i] ?? '',
  624. 'item_param_name' => $routine['item_param_name'][$i] ?? '',
  625. 'item_param_length' => $routine['item_param_length'][$i] ?? '',
  626. 'item_param_opts_num' => $routine['item_param_opts_num'][$i] ?? '',
  627. 'supported_datatypes' => Util::getSupportedDatatypes(
  628. true,
  629. $routine['item_param_type'][$i]
  630. ),
  631. 'charsets' => $charsets,
  632. 'drop_class' => $drop_class,
  633. ]);
  634. }
  635. /**
  636. * Displays a form used to add/edit a routine
  637. *
  638. * @param string $mode If the editor will be used to edit a routine
  639. * or add a new one: 'edit' or 'add'.
  640. * @param string $operation If the editor was previously invoked with
  641. * JS turned off, this will hold the name of
  642. * the current operation
  643. * @param array $routine Data for the routine returned by
  644. * getDataFromRequest() or getDataFromName()
  645. *
  646. * @return string HTML code for the editor.
  647. */
  648. public function getEditorForm($mode, $operation, array $routine)
  649. {
  650. global $db, $errors;
  651. for ($i = 0; $i < $routine['item_num_params']; $i++) {
  652. $routine['item_param_name'][$i] = htmlentities($routine['item_param_name'][$i], ENT_QUOTES);
  653. $routine['item_param_length'][$i] = htmlentities($routine['item_param_length'][$i], ENT_QUOTES);
  654. }
  655. // Handle some logic first
  656. if ($operation === 'change') {
  657. if ($routine['item_type'] === 'PROCEDURE') {
  658. $routine['item_type'] = 'FUNCTION';
  659. $routine['item_type_toggle'] = 'PROCEDURE';
  660. } else {
  661. $routine['item_type'] = 'PROCEDURE';
  662. $routine['item_type_toggle'] = 'FUNCTION';
  663. }
  664. } elseif ($operation === 'add' || ($routine['item_num_params'] == 0 && $mode === 'add' && ! $errors)) {
  665. $routine['item_param_dir'][] = '';
  666. $routine['item_param_name'][] = '';
  667. $routine['item_param_type'][] = '';
  668. $routine['item_param_length'][] = '';
  669. $routine['item_param_opts_num'][] = '';
  670. $routine['item_param_opts_text'][] = '';
  671. $routine['item_num_params']++;
  672. } elseif ($operation === 'remove') {
  673. unset(
  674. $routine['item_param_dir'][$routine['item_num_params'] - 1],
  675. $routine['item_param_name'][$routine['item_num_params'] - 1],
  676. $routine['item_param_type'][$routine['item_num_params'] - 1],
  677. $routine['item_param_length'][$routine['item_num_params'] - 1],
  678. $routine['item_param_opts_num'][$routine['item_num_params'] - 1],
  679. $routine['item_param_opts_text'][$routine['item_num_params'] - 1]
  680. );
  681. $routine['item_num_params']--;
  682. }
  683. $parameterRows = '';
  684. for ($i = 0; $i < $routine['item_num_params']; $i++) {
  685. $parameterRows .= $this->getParameterRow($routine, $i, $routine['item_type'] === 'FUNCTION' ? ' hide' : '');
  686. }
  687. $charsets = Charsets::getCharsets($this->dbi, $GLOBALS['cfg']['Server']['DisableIS']);
  688. return $this->template->render('database/routines/editor_form', [
  689. 'db' => $db,
  690. 'routine' => $routine,
  691. 'is_edit_mode' => $mode === 'edit',
  692. 'is_ajax' => $this->response->isAjax(),
  693. 'parameter_rows' => $parameterRows,
  694. 'charsets' => $charsets,
  695. 'numeric_options' => $this->numericOptions,
  696. 'has_privileges' => $GLOBALS['proc_priv'] && $GLOBALS['is_reload_priv'],
  697. 'sql_data_access' => $this->sqlDataAccess,
  698. ]);
  699. }
  700. /**
  701. * Set the found errors and build the params
  702. *
  703. * @param string[] $itemParamName The parameter names
  704. * @param string[] $itemParamDir The direction parameter (see $this->directions)
  705. * @param array $itemParamType The parameter type
  706. * @param array $itemParamLength A length or not for the parameter
  707. * @param array $itemParamOpsText An optional charset for the parameter
  708. * @param array $itemParamOpsNum An optional parameter for a $itemParamType NUMBER
  709. * @param string $itemType The item type (PROCEDURE/FUNCTION)
  710. * @param bool $warnedAboutLength A boolean that will be switched if a the length warning is given
  711. */
  712. private function processParamsAndBuild(
  713. array $itemParamName,
  714. array $itemParamDir,
  715. array $itemParamType,
  716. array $itemParamLength,
  717. array $itemParamOpsText,
  718. array $itemParamOpsNum,
  719. string $itemType,
  720. bool &$warnedAboutLength
  721. ): string {
  722. global $errors, $dbi;
  723. $params = '';
  724. $warnedAboutDir = false;
  725. for ($i = 0, $nb = count($itemParamName); $i < $nb; $i++) {
  726. if (empty($itemParamName[$i]) || empty($itemParamType[$i])) {
  727. $errors[] = __('You must provide a name and a type for each routine parameter.');
  728. break;
  729. }
  730. if (
  731. $itemType === 'PROCEDURE'
  732. && ! empty($itemParamDir[$i])
  733. && in_array($itemParamDir[$i], $this->directions)
  734. ) {
  735. $params .= $itemParamDir[$i] . ' '
  736. . Util::backquote($itemParamName[$i])
  737. . ' ' . $itemParamType[$i];
  738. } elseif ($itemType === 'FUNCTION') {
  739. $params .= Util::backquote($itemParamName[$i])
  740. . ' ' . $itemParamType[$i];
  741. } elseif (! $warnedAboutDir) {
  742. $warnedAboutDir = true;
  743. $errors[] = sprintf(
  744. __('Invalid direction "%s" given for parameter.'),
  745. htmlspecialchars($itemParamDir[$i])
  746. );
  747. }
  748. if (
  749. $itemParamLength[$i] != ''
  750. && ! preg_match(
  751. '@^(DATE|TINYBLOB|TINYTEXT|BLOB|TEXT|MEDIUMBLOB|MEDIUMTEXT|LONGBLOB|LONGTEXT|SERIAL|BOOLEAN)$@i',
  752. $itemParamType[$i]
  753. )
  754. ) {
  755. $params .= '(' . $itemParamLength[$i] . ')';
  756. } elseif (
  757. $itemParamLength[$i] == ''
  758. && preg_match('@^(ENUM|SET|VARCHAR|VARBINARY)$@i', $itemParamType[$i])
  759. ) {
  760. if (! $warnedAboutLength) {
  761. $warnedAboutLength = true;
  762. $errors[] = __(
  763. 'You must provide length/values for routine parameters'
  764. . ' of type ENUM, SET, VARCHAR and VARBINARY.'
  765. );
  766. }
  767. }
  768. if (! empty($itemParamOpsText[$i])) {
  769. if ($dbi->types->getTypeClass($itemParamType[$i]) === 'CHAR') {
  770. if (! in_array($itemParamType[$i], ['VARBINARY', 'BINARY'])) {
  771. $params .= ' CHARSET '
  772. . mb_strtolower($itemParamOpsText[$i]);
  773. }
  774. }
  775. }
  776. if (! empty($itemParamOpsNum[$i])) {
  777. if ($dbi->types->getTypeClass($itemParamType[$i]) === 'NUMBER') {
  778. $params .= ' '
  779. . mb_strtoupper($itemParamOpsNum[$i]);
  780. }
  781. }
  782. if ($i == count($itemParamName) - 1) {
  783. continue;
  784. }
  785. $params .= ', ';
  786. }
  787. return $params;
  788. }
  789. /**
  790. * Set the found errors and build the query
  791. *
  792. * @param string $query The existing query
  793. * @param bool $warnedAboutLength If the length warning was given
  794. */
  795. private function processFunctionSpecificParameters(
  796. string $query,
  797. bool $warnedAboutLength
  798. ): string {
  799. global $errors, $dbi;
  800. $itemReturnType = $_POST['item_returntype'] ?? null;
  801. if (! empty($itemReturnType) && in_array($itemReturnType, Util::getSupportedDatatypes())) {
  802. $query .= 'RETURNS ' . $itemReturnType;
  803. } else {
  804. $errors[] = __('You must provide a valid return type for the routine.');
  805. }
  806. if (
  807. ! empty($_POST['item_returnlength'])
  808. && ! preg_match(
  809. '@^(DATE|DATETIME|TIME|TINYBLOB|TINYTEXT|BLOB|TEXT|'
  810. . 'MEDIUMBLOB|MEDIUMTEXT|LONGBLOB|LONGTEXT|SERIAL|BOOLEAN)$@i',
  811. $itemReturnType
  812. )
  813. ) {
  814. $query .= '(' . $_POST['item_returnlength'] . ')';
  815. } elseif (
  816. empty($_POST['item_returnlength'])
  817. && preg_match('@^(ENUM|SET|VARCHAR|VARBINARY)$@i', $itemReturnType)
  818. ) {
  819. if (! $warnedAboutLength) {
  820. $errors[] = __(
  821. 'You must provide length/values for routine parameters of type ENUM, SET, VARCHAR and VARBINARY.'
  822. );
  823. }
  824. }
  825. if (! empty($_POST['item_returnopts_text'])) {
  826. if ($dbi->types->getTypeClass($itemReturnType) === 'CHAR') {
  827. $query .= ' CHARSET '
  828. . mb_strtolower($_POST['item_returnopts_text']);
  829. }
  830. }
  831. if (! empty($_POST['item_returnopts_num'])) {
  832. if ($dbi->types->getTypeClass($itemReturnType) === 'NUMBER') {
  833. $query .= ' '
  834. . mb_strtoupper($_POST['item_returnopts_num']);
  835. }
  836. }
  837. return $query . ' ';
  838. }
  839. /**
  840. * Composes the query necessary to create a routine from an HTTP request.
  841. *
  842. * @return string The CREATE [ROUTINE | PROCEDURE] query.
  843. */
  844. public function getQueryFromRequest(): string
  845. {
  846. global $errors;
  847. $itemType = $_POST['item_type'] ?? '';
  848. $itemDefiner = $_POST['item_definer'] ?? '';
  849. $itemName = $_POST['item_name'] ?? '';
  850. $query = 'CREATE ';
  851. if (! empty($itemDefiner)) {
  852. if (str_contains($itemDefiner, '@')) {
  853. $arr = explode('@', $itemDefiner);
  854. $do_backquote = true;
  855. if (substr($arr[0], 0, 1) === '`' && substr($arr[0], -1) === '`') {
  856. $do_backquote = false;
  857. }
  858. $query .= 'DEFINER=' . Util::backquoteCompat($arr[0], 'NONE', $do_backquote);
  859. $do_backquote = true;
  860. if (substr($arr[1], 0, 1) === '`' && substr($arr[1], -1) === '`') {
  861. $do_backquote = false;
  862. }
  863. $query .= '@' . Util::backquoteCompat($arr[1], 'NONE', $do_backquote) . ' ';
  864. } else {
  865. $errors[] = __('The definer must be in the "username@hostname" format!');
  866. }
  867. }
  868. if ($itemType === 'FUNCTION' || $itemType === 'PROCEDURE') {
  869. $query .= $itemType . ' ';
  870. } else {
  871. $errors[] = sprintf(
  872. __('Invalid routine type: "%s"'),
  873. htmlspecialchars($itemType)
  874. );
  875. }
  876. if (! empty($itemName)) {
  877. $query .= Util::backquote($itemName);
  878. } else {
  879. $errors[] = __('You must provide a routine name!');
  880. }
  881. $warnedAboutLength = false;
  882. $itemParamName = $_POST['item_param_name'] ?? '';
  883. $itemParamType = $_POST['item_param_type'] ?? '';
  884. $itemParamLength = $_POST['item_param_length'] ?? '';
  885. $itemParamDir = (array) ($_POST['item_param_dir'] ?? []);
  886. $itemParamOpsText = (array) ($_POST['item_param_opts_text'] ?? []);
  887. $itemParamOpsNum = (array) ($_POST['item_param_opts_num'] ?? []);
  888. $params = '';
  889. if (
  890. ! empty($itemParamName)
  891. && ! empty($itemParamType)
  892. && ! empty($itemParamLength)
  893. && is_array($itemParamName)
  894. && is_array($itemParamType)
  895. && is_array($itemParamLength)
  896. ) {
  897. $params = $this->processParamsAndBuild(
  898. $itemParamName,
  899. $itemParamDir,
  900. $itemParamType,
  901. $itemParamLength,
  902. $itemParamOpsText,
  903. $itemParamOpsNum,
  904. $itemType,
  905. $warnedAboutLength// Will possibly be modified by the function
  906. );
  907. }
  908. $query .= '(' . $params . ') ';
  909. if ($itemType === 'FUNCTION') {
  910. $query = $this->processFunctionSpecificParameters($query, $warnedAboutLength);
  911. }
  912. if (! empty($_POST['item_comment'])) {
  913. $query .= "COMMENT '" . $this->dbi->escapeString($_POST['item_comment'])
  914. . "' ";
  915. }
  916. if (isset($_POST['item_isdeterministic'])) {
  917. $query .= 'DETERMINISTIC ';
  918. } else {
  919. $query .= 'NOT DETERMINISTIC ';
  920. }
  921. $itemSqlDataAccess = $_POST['item_sqldataaccess'] ?? '';
  922. if (! empty($itemSqlDataAccess) && in_array($itemSqlDataAccess, $this->sqlDataAccess)) {
  923. $query .= $itemSqlDataAccess . ' ';
  924. }
  925. $itemSecurityType = $_POST['item_securitytype'] ?? '';
  926. if (! empty($itemSecurityType)) {
  927. if ($itemSecurityType === 'DEFINER' || $itemSecurityType === 'INVOKER') {
  928. $query .= 'SQL SECURITY ' . $itemSecurityType . ' ';
  929. }
  930. }
  931. $itemDefinition = $_POST['item_definition'] ?? '';
  932. if (! empty($itemDefinition)) {
  933. $query .= $itemDefinition;
  934. } else {
  935. $errors[] = __('You must provide a routine definition.');
  936. }
  937. return $query;
  938. }
  939. /**
  940. * @see handleExecuteRoutine
  941. *
  942. * @param array $routine The routine params
  943. *
  944. * @return string[] The SQL queries / SQL query parts
  945. */
  946. private function getQueriesFromRoutineForm(array $routine): array
  947. {
  948. $queries = [];
  949. $end_query = [];
  950. $args = [];
  951. $all_functions = $this->dbi->types->getAllFunctions();
  952. for ($i = 0; $i < $routine['item_num_params']; $i++) {
  953. if (isset($_POST['params'][$routine['item_param_name'][$i]])) {
  954. $value = $_POST['params'][$routine['item_param_name'][$i]];
  955. if (is_array($value)) { // is SET type
  956. $value = implode(',', $value);
  957. }
  958. $value = $this->dbi->escapeString($value);
  959. if (
  960. ! empty($_POST['funcs'][$routine['item_param_name'][$i]])
  961. && in_array($_POST['funcs'][$routine['item_param_name'][$i]], $all_functions)
  962. ) {
  963. $queries[] = 'SET @p' . $i . '='
  964. . $_POST['funcs'][$routine['item_param_name'][$i]]
  965. . "('" . $value . "');\n";
  966. } else {
  967. $queries[] = 'SET @p' . $i . "='" . $value . "';\n";
  968. }
  969. $args[] = '@p' . $i;
  970. } else {
  971. $args[] = '@p' . $i;
  972. }
  973. if ($routine['item_type'] !== 'PROCEDURE') {
  974. continue;
  975. }
  976. if ($routine['item_param_dir'][$i] !== 'OUT' && $routine['item_param_dir'][$i] !== 'INOUT') {
  977. continue;
  978. }
  979. $end_query[] = '@p' . $i . ' AS '
  980. . Util::backquote($routine['item_param_name'][$i]);
  981. }
  982. if ($routine['item_type'] === 'PROCEDURE') {
  983. $queries[] = 'CALL ' . Util::backquote($routine['item_name'])
  984. . '(' . implode(', ', $args) . ");\n";
  985. if (count($end_query)) {
  986. $queries[] = 'SELECT ' . implode(', ', $end_query) . ";\n";
  987. }
  988. } else {
  989. $queries[] = 'SELECT ' . Util::backquote($routine['item_name'])
  990. . '(' . implode(', ', $args) . ') '
  991. . 'AS ' . Util::backquote($routine['item_name'])
  992. . ";\n";
  993. }
  994. return $queries;
  995. }
  996. private function handleExecuteRoutine(): void
  997. {
  998. global $db;
  999. // Build the queries
  1000. $routine = $this->getDataFromName($_POST['item_name'], $_POST['item_type'], false);
  1001. if ($routine === null) {
  1002. $message = __('Error in processing request:') . ' ';
  1003. $message .= sprintf(
  1004. __('No routine with name %1$s found in database %2$s.'),
  1005. htmlspecialchars(Util::backquote($_POST['item_name'])),
  1006. htmlspecialchars(Util::backquote($db))
  1007. );
  1008. $message = Message::error($message);
  1009. if ($this->response->isAjax()) {
  1010. $this->response->setRequestStatus(false);
  1011. $this->response->addJSON('message', $message);
  1012. exit;
  1013. }
  1014. echo $message->getDisplay();
  1015. unset($_POST);
  1016. //NOTE: Missing exit ?
  1017. }
  1018. $queries = is_array($routine) ? $this->getQueriesFromRoutineForm($routine) : [];
  1019. // Get all the queries as one SQL statement
  1020. $multiple_query = implode('', $queries);
  1021. $outcome = true;
  1022. $affected = 0;
  1023. // Execute query
  1024. if (! $this->dbi->tryMultiQuery($multiple_query)) {
  1025. $outcome = false;
  1026. }
  1027. // Generate output
  1028. $output = '';
  1029. $nbResultsetToDisplay = 0;
  1030. if ($outcome) {
  1031. // Pass the SQL queries through the "pretty printer"
  1032. $output = Generator::formatSql(implode("\n", $queries));
  1033. // Display results
  1034. $output .= '<div class="card my-3"><div class="card-header">';
  1035. $output .= sprintf(
  1036. __('Execution results of routine %s'),
  1037. Util::backquote(htmlspecialchars($routine['item_name']))
  1038. );
  1039. $output .= '</div><div class="card-body">';
  1040. do {
  1041. $result = $this->dbi->storeResult();
  1042. $num_rows = $this->dbi->numRows($result);
  1043. if (($result !== false) && ($num_rows > 0)) {
  1044. $output .= '<table class="table table-light table-striped w-auto"><tr>';
  1045. $fieldsMeta = $this->dbi->getFieldsMeta($result) ?? [];
  1046. foreach ($fieldsMeta as $field) {
  1047. $output .= '<th>';
  1048. $output .= htmlspecialchars($field->name);
  1049. $output .= '</th>';
  1050. }
  1051. $output .= '</tr>';
  1052. while ($row = $this->dbi->fetchAssoc($result)) {
  1053. $output .= '<tr>' . $this->browseRow($row) . '</tr>';
  1054. }
  1055. $output .= '</table>';
  1056. $nbResultsetToDisplay++;
  1057. $affected = $num_rows;
  1058. }
  1059. if (! $this->dbi->moreResults()) {
  1060. break;
  1061. }
  1062. $this->dbi->freeResult($result);
  1063. $outcome = $this->dbi->nextResult();
  1064. } while ($outcome);
  1065. }
  1066. if ($outcome) {
  1067. $output .= '</div></div>';
  1068. $message = __('Your SQL query has been executed successfully.');
  1069. if ($routine['item_type'] === 'PROCEDURE') {
  1070. $message .= '<br>';
  1071. // TODO : message need to be modified according to the
  1072. // output from the routine
  1073. $message .= sprintf(
  1074. _ngettext(
  1075. '%d row affected by the last statement inside the procedure.',
  1076. '%d rows affected by the last statement inside the procedure.',
  1077. (int) $affected
  1078. ),
  1079. $affected
  1080. );
  1081. }
  1082. $message = Message::success($message);
  1083. if ($nbResultsetToDisplay == 0) {
  1084. $notice = __('MySQL returned an empty result set (i.e. zero rows).');
  1085. $output .= Message::notice($notice)->getDisplay();
  1086. }
  1087. } else {
  1088. $output = '';
  1089. $message = Message::error(
  1090. sprintf(
  1091. __('The following query has failed: "%s"'),
  1092. htmlspecialchars($multiple_query)
  1093. )
  1094. . '<br><br>'
  1095. . __('MySQL said: ') . $this->dbi->getError()
  1096. );
  1097. }
  1098. // Print/send output
  1099. if ($this->response->isAjax()) {
  1100. $this->response->setRequestStatus($message->isSuccess());
  1101. $this->response->addJSON('message', $message->getDisplay() . $output);
  1102. $this->response->addJSON('dialog', false);
  1103. exit;
  1104. }
  1105. echo $message->getDisplay() , $output;
  1106. if ($message->isError()) {
  1107. // At least one query has failed, so shouldn't
  1108. // execute any more queries, so we quit.
  1109. exit;
  1110. }
  1111. unset($_POST);
  1112. // Now deliberately fall through to displaying the routines list
  1113. }
  1114. /**
  1115. * Handles requests for executing a routine
  1116. */
  1117. public function handleExecute(): void
  1118. {
  1119. global $db;
  1120. /**
  1121. * Handle all user requests other than the default of listing routines
  1122. */
  1123. if (! empty($_POST['execute_routine']) && ! empty($_POST['item_name'])) {
  1124. $this->handleExecuteRoutine();
  1125. } elseif (! empty($_GET['execute_dialog']) && ! empty($_GET['item_name'])) {
  1126. /**
  1127. * Display the execute form for a routine.
  1128. */
  1129. $routine = $this->getDataFromName($_GET['item_name'], $_GET['item_type'], true);
  1130. if ($routine !== null) {
  1131. $form = $this->getExecuteForm($routine);
  1132. if ($this->response->isAjax()) {
  1133. $title = __('Execute routine') . ' ' . Util::backquote(
  1134. htmlentities($_GET['item_name'], ENT_QUOTES)
  1135. );
  1136. $this->response->addJSON('message', $form);
  1137. $this->response->addJSON('title', $title);
  1138. $this->response->addJSON('dialog', true);
  1139. } else {
  1140. echo "\n\n<h2>" . __('Execute routine') . "</h2>\n\n";
  1141. echo $form;
  1142. }
  1143. exit;
  1144. }
  1145. if ($this->response->isAjax()) {
  1146. $message = __('Error in processing request:') . ' ';
  1147. $message .= sprintf(
  1148. __('No routine with name %1$s found in database %2$s.'),
  1149. htmlspecialchars(Util::backquote($_GET['item_name'])),
  1150. htmlspecialchars(Util::backquote($db))
  1151. );
  1152. $message = Message::error($message);
  1153. $this->response->setRequestStatus(false);
  1154. $this->response->addJSON('message', $message);
  1155. exit;
  1156. }
  1157. }
  1158. }
  1159. /**
  1160. * Browse row array
  1161. *
  1162. * @param array $row Columns
  1163. */
  1164. private function browseRow(array $row): ?string
  1165. {
  1166. $output = null;
  1167. foreach ($row as $value) {
  1168. if ($value === null) {
  1169. $value = '<i>NULL</i>';
  1170. } else {
  1171. $value = htmlspecialchars($value);
  1172. }
  1173. $output .= '<td>' . $value . '</td>';
  1174. }
  1175. return $output;
  1176. }
  1177. /**
  1178. * Creates the HTML code that shows the routine execution dialog.
  1179. *
  1180. * @param array $routine Data for the routine returned by
  1181. * getDataFromName()
  1182. *
  1183. * @return string HTML code for the routine execution dialog.
  1184. */
  1185. public function getExecuteForm(array $routine): string
  1186. {
  1187. global $db, $cfg;
  1188. // Escape special characters
  1189. $routine['item_name'] = htmlentities($routine['item_name'], ENT_QUOTES);
  1190. for ($i = 0; $i < $routine['item_num_params']; $i++) {
  1191. $routine['item_param_name'][$i] = htmlentities($routine['item_param_name'][$i], ENT_QUOTES);
  1192. }
  1193. $no_support_types = Util::unsupportedDatatypes();
  1194. $params = [];
  1195. $params['no_support_types'] = $no_support_types;
  1196. for ($i = 0; $i < $routine['item_num_params']; $i++) {
  1197. if ($routine['item_type'] === 'PROCEDURE' && $routine['item_param_dir'][$i] === 'OUT') {
  1198. continue;
  1199. }
  1200. if ($cfg['ShowFunctionFields']) {
  1201. if (
  1202. stripos($routine['item_param_type'][$i], 'enum') !== false
  1203. || stripos($routine['item_param_type'][$i], 'set') !== false
  1204. || in_array(
  1205. mb_strtolower($routine['item_param…

Large files files are truncated, but you can click here to view the full file