3 declare(strict_types
=1);
5 namespace PhpMyAdmin\Database
;
7 use PhpMyAdmin\Charsets
;
9 use PhpMyAdmin\Current
;
10 use PhpMyAdmin\DatabaseInterface
;
11 use PhpMyAdmin\Html\Generator
;
12 use PhpMyAdmin\Message
;
13 use PhpMyAdmin\Query\Generator
as QueryGenerator
;
14 use PhpMyAdmin\SqlParser\Parser
;
15 use PhpMyAdmin\SqlParser\Statements\CreateStatement
;
16 use PhpMyAdmin\SqlParser\TokensList
;
17 use PhpMyAdmin\SqlParser\Utils\Routine
as RoutineUtils
;
18 use PhpMyAdmin\UserPrivileges
;
22 use function _ngettext
;
23 use function array_column
;
24 use function array_merge
;
25 use function array_multisort
;
28 use function htmlentities
;
29 use function htmlspecialchars
;
31 use function in_array
;
32 use function is_array
;
33 use function is_string
;
35 use function mb_strtolower
;
36 use function mb_strtoupper
;
37 use function preg_match
;
39 use function str_contains
;
40 use function str_ends_with
;
41 use function str_starts_with
;
48 * Functions for routine management.
52 /** @var array<int, string> */
53 public readonly
array $directions;
55 /** @var array<int, string> */
56 public readonly
array $sqlDataAccess;
58 /** @var array<int, string> */
59 public readonly
array $numericOptions;
61 public function __construct(private DatabaseInterface
$dbi)
63 $this->directions
= ['IN', 'OUT', 'INOUT'];
64 $this->sqlDataAccess
= ['CONTAINS SQL', 'NO SQL', 'READS SQL DATA', 'MODIFIES SQL DATA'];
65 $this->numericOptions
= ['UNSIGNED', 'ZEROFILL', 'UNSIGNED ZEROFILL'];
69 * Handle request to create or edit a routine
71 public function handleRequestCreateOrEdit(UserPrivileges
$userPrivileges, string $db): string
74 $routineQuery = $this->getQueryFromRequest();
76 // set by getQueryFromRequest()
77 if ($GLOBALS['errors'] === []) {
78 // Execute the created query
79 if (! empty($_POST['editor_process_edit'])) {
80 if (! in_array($_POST['item_original_type'], ['PROCEDURE', 'FUNCTION'], true)) {
81 $GLOBALS['errors'][] = sprintf(
82 __('Invalid routine type: "%s"'),
83 htmlspecialchars($_POST['item_original_type']),
86 // Backup the old routine, in case something goes wrong
87 if ($_POST['item_original_type'] === 'FUNCTION') {
88 $createRoutine = self
::getFunctionDefinition($this->dbi
, $db, $_POST['item_original_name']);
90 $createRoutine = self
::getProcedureDefinition($this->dbi
, $db, $_POST['item_original_name']);
93 $privilegesBackup = $this->backupPrivileges($userPrivileges);
95 $dropRoutine = 'DROP ' . $_POST['item_original_type'] . ' '
96 . Util
::backquote($_POST['item_original_name'])
98 $result = $this->dbi
->tryQuery($dropRoutine);
100 $GLOBALS['errors'][] = sprintf(
101 __('The following query has failed: "%s"'),
102 htmlspecialchars($dropRoutine),
105 . __('MySQL said: ') . $this->dbi
->getError();
107 [$newErrors, $GLOBALS['message']] = $this->create(
113 if (empty($newErrors)) {
114 $sqlQuery = $dropRoutine . $routineQuery;
116 $GLOBALS['errors'] = array_merge($GLOBALS['errors'], $newErrors);
123 // 'Add a new routine' mode
124 $result = $this->dbi
->tryQuery($routineQuery);
126 $GLOBALS['errors'][] = sprintf(
127 __('The following query has failed: "%s"'),
128 htmlspecialchars($routineQuery),
131 . __('MySQL said: ') . $this->dbi
->getError();
133 $GLOBALS['message'] = Message
::success(
134 __('Routine %1$s has been created.'),
136 $GLOBALS['message']->addParam(
137 Util
::backquote($_POST['item_name']),
139 $sqlQuery = $routineQuery;
144 if ($GLOBALS['errors'] !== []) {
145 $GLOBALS['message'] = Message
::error(
147 'One or more errors have occurred while processing your request:',
150 $GLOBALS['message']->addHtml('<ul>');
151 foreach ($GLOBALS['errors'] as $string) {
152 $GLOBALS['message']->addHtml('<li>' . $string . '</li>');
155 $GLOBALS['message']->addHtml('</ul>');
158 return Generator
::getMessage($GLOBALS['message'], $sqlQuery);
162 * Backup the privileges
166 public function backupPrivileges(UserPrivileges
$userPrivileges): array
168 if (! $userPrivileges->routines ||
! $userPrivileges->isReload
) {
172 // Backup the Old Privileges before dropping
173 // if $_POST['item_adjust_privileges'] set
174 if (empty($_POST['item_adjust_privileges'])) {
178 $privilegesBackupQuery = 'SELECT * FROM ' . Util
::backquote('mysql')
179 . '.' . Util
::backquote('procs_priv')
180 . ' WHERE Routine_name = ' . $this->dbi
->quoteString($_POST['item_original_name'])
181 . ' AND Routine_type = ' . $this->dbi
->quoteString($_POST['item_original_type']);
183 return $this->dbi
->fetchResult($privilegesBackupQuery, 0);
189 * @param string $routineQuery Query to create routine
190 * @param string $createRoutine Query to restore routine
191 * @param string[][] $privilegesBackup Privileges backup
193 * @return array{string[], Message|null}
195 public function create(
196 UserPrivileges
$userPrivileges,
197 string $routineQuery,
198 string $createRoutine,
199 array $privilegesBackup,
201 $result = $this->dbi
->tryQuery($routineQuery);
205 __('The following query has failed: "%s"'),
206 htmlspecialchars($routineQuery),
209 . __('MySQL said: ') . $this->dbi
->getError();
210 // We dropped the old routine,
211 // but were unable to create the new one
212 // Try to restore the backup query
213 $result = $this->dbi
->tryQuery($createRoutine);
215 // OMG, this is really bad! We dropped the query,
216 // failed to create a new one
217 // and now even the backup query does not execute!
218 // This should not happen, but we better handle
219 // this just in case.
220 $errors[] = __('Sorry, we failed to restore the dropped routine.') . '<br>'
221 . __('The backed up query was:')
222 . '"' . htmlspecialchars($createRoutine) . '"<br>'
223 . __('MySQL said: ') . $this->dbi
->getError();
226 return [$errors, null];
230 $resultAdjust = false;
232 if ($userPrivileges->routines
&& $userPrivileges->isReload
) {
233 // Insert all the previous privileges
234 // but with the new name and the new type
235 foreach ($privilegesBackup as $priv) {
236 $adjustProcPrivilege = 'INSERT INTO '
237 . Util
::backquote('mysql') . '.'
238 . Util
::backquote('procs_priv')
239 . ' VALUES(' . $this->dbi
->quoteString($priv[0]) . ', '
240 . $this->dbi
->quoteString($priv[1]) . ', ' . $this->dbi
->quoteString($priv[2]) . ', '
241 . $this->dbi
->quoteString($_POST['item_name']) . ', '
242 . $this->dbi
->quoteString($_POST['item_type']) . ', '
243 . $this->dbi
->quoteString($priv[5]) . ', '
244 . $this->dbi
->quoteString($priv[6]) . ', '
245 . $this->dbi
->quoteString($priv[7]) . ');';
246 $this->dbi
->query($adjustProcPrivilege);
247 $resultAdjust = true;
251 $message = $this->flushPrivileges($resultAdjust);
253 return [[], $message];
257 * Flush privileges and get message
259 * @param bool $flushPrivileges Flush privileges
261 public function flushPrivileges(bool $flushPrivileges): Message
263 if ($flushPrivileges) {
264 // Flush the Privileges
265 $flushPrivQuery = 'FLUSH PRIVILEGES;';
266 $this->dbi
->query($flushPrivQuery);
268 $message = Message
::success(
270 'Routine %1$s has been modified. Privileges have been adjusted.',
274 $message = Message
::success(
275 __('Routine %1$s has been modified.'),
280 Util
::backquote($_POST['item_name']),
287 * This function will generate the values that are required to
288 * complete the editor form. It is especially necessary to handle
289 * the 'Add another parameter', 'Remove last parameter' and
290 * 'Change routine type' functionalities when JS is disabled.
292 * @return mixed[] Data necessary to create the routine editor.
294 public function getDataFromRequest(): array
299 'item_original_name',
301 'item_returnopts_num',
302 'item_returnopts_text',
307 foreach ($indices as $index) {
308 $retval[$index] = $_POST[$index] ??
'';
311 $retval['item_type'] = 'PROCEDURE';
312 $retval['item_type_toggle'] = 'FUNCTION';
313 if (isset($_POST['item_type']) && $_POST['item_type'] === 'FUNCTION') {
314 $retval['item_type'] = 'FUNCTION';
315 $retval['item_type_toggle'] = 'PROCEDURE';
318 $retval['item_original_type'] = 'PROCEDURE';
319 if (isset($_POST['item_original_type']) && $_POST['item_original_type'] === 'FUNCTION') {
320 $retval['item_original_type'] = 'FUNCTION';
323 $retval['item_num_params'] = 0;
324 $retval['item_param_dir'] = [];
325 $retval['item_param_name'] = [];
326 $retval['item_param_type'] = [];
327 $retval['item_param_length'] = [];
328 $retval['item_param_opts_num'] = [];
329 $retval['item_param_opts_text'] = [];
332 $_POST['item_param_name'],
333 $_POST['item_param_type'],
334 $_POST['item_param_length'],
335 $_POST['item_param_opts_num'],
336 $_POST['item_param_opts_text'],
338 && is_array($_POST['item_param_name'])
339 && is_array($_POST['item_param_type'])
340 && is_array($_POST['item_param_length'])
341 && is_array($_POST['item_param_opts_num'])
342 && is_array($_POST['item_param_opts_text'])
344 if ($_POST['item_type'] === 'PROCEDURE') {
345 $retval['item_param_dir'] = $_POST['item_param_dir'];
346 foreach ($retval['item_param_dir'] as $key => $value) {
347 if (in_array($value, $this->directions
, true)) {
351 $retval['item_param_dir'][$key] = '';
355 $retval['item_param_name'] = $_POST['item_param_name'];
356 $retval['item_param_type'] = $_POST['item_param_type'];
357 foreach ($retval['item_param_type'] as $key => $value) {
358 if (in_array($value, Util
::getSupportedDatatypes(), true)) {
362 $retval['item_param_type'][$key] = '';
365 $retval['item_param_length'] = $_POST['item_param_length'];
366 $retval['item_param_opts_num'] = $_POST['item_param_opts_num'];
367 $retval['item_param_opts_text'] = $_POST['item_param_opts_text'];
368 $retval['item_num_params'] = max(
369 count($retval['item_param_name']),
370 count($retval['item_param_type']),
371 count($retval['item_param_length']),
372 count($retval['item_param_opts_num']),
373 count($retval['item_param_opts_text']),
377 $retval['item_returntype'] = '';
379 isset($_POST['item_returntype'])
380 && in_array($_POST['item_returntype'], Util
::getSupportedDatatypes(), true)
382 $retval['item_returntype'] = $_POST['item_returntype'];
385 $retval['item_isdeterministic'] = '';
386 if (isset($_POST['item_isdeterministic']) && mb_strtolower($_POST['item_isdeterministic']) === 'on') {
387 $retval['item_isdeterministic'] = " checked='checked'";
390 $retval['item_securitytype_definer'] = '';
391 $retval['item_securitytype_invoker'] = '';
392 if (isset($_POST['item_securitytype'])) {
393 if ($_POST['item_securitytype'] === 'DEFINER') {
394 $retval['item_securitytype_definer'] = " selected='selected'";
395 } elseif ($_POST['item_securitytype'] === 'INVOKER') {
396 $retval['item_securitytype_invoker'] = " selected='selected'";
400 $retval['item_sqldataaccess'] = '';
401 if (isset($_POST['item_sqldataaccess']) && in_array($_POST['item_sqldataaccess'], $this->sqlDataAccess
, true)) {
402 $retval['item_sqldataaccess'] = $_POST['item_sqldataaccess'];
409 * This function will generate the values that are required to complete
410 * the "Edit routine" form given the name of a routine.
412 * @param string $name The name of the routine.
413 * @param string $type Type of routine (ROUTINE|PROCEDURE)
414 * @param bool $all Whether to return all data or just the info about parameters.
416 * @return mixed[]|null Data necessary to create the routine editor.
418 public function getDataFromName(string $name, string $type, bool $all = true): array|
null
422 // Build and execute the query
423 $fields = 'SPECIFIC_NAME, ROUTINE_TYPE, DTD_IDENTIFIER, '
424 . 'ROUTINE_DEFINITION, IS_DETERMINISTIC, SQL_DATA_ACCESS, '
425 . 'ROUTINE_COMMENT, SECURITY_TYPE';
426 $where = 'ROUTINE_SCHEMA ' . Util
::getCollateForIS() . '=' . $this->dbi
->quoteString(Current
::$database)
427 . ' AND SPECIFIC_NAME=' . $this->dbi
->quoteString($name)
428 . ' AND ROUTINE_TYPE=' . $this->dbi
->quoteString($type);
429 $query = 'SELECT ' . $fields . ' FROM INFORMATION_SCHEMA.ROUTINES WHERE ' . $where . ';';
431 $routine = $this->dbi
->fetchSingleRow($query);
433 if ($routine === null ||
$routine === []) {
438 $retval['item_name'] = $routine['SPECIFIC_NAME'];
439 $retval['item_type'] = $routine['ROUTINE_TYPE'];
441 if ($routine['ROUTINE_TYPE'] === 'FUNCTION') {
442 $definition = self
::getFunctionDefinition($this->dbi
, Current
::$database, $routine['SPECIFIC_NAME']);
444 $definition = self
::getProcedureDefinition($this->dbi
, Current
::$database, $routine['SPECIFIC_NAME']);
447 if ($definition === null) {
451 $parser = new Parser('DELIMITER $$' . "\n" . $definition);
453 /** @var CreateStatement $stmt */
454 $stmt = $parser->statements
[0];
456 // Do not use $routine['ROUTINE_DEFINITION'] because of a MySQL escaping issue: #15370
457 $body = TokensList
::buildFromArray($stmt->body
);
459 // Fallback just in case the parser fails
460 $body = (string) $routine['ROUTINE_DEFINITION'];
463 $params = RoutineUtils
::getParameters($stmt);
464 $retval['item_num_params'] = $params['num'];
465 $retval['item_param_dir'] = $params['dir'];
466 $retval['item_param_name'] = $params['name'];
467 $retval['item_param_type'] = $params['type'];
468 $retval['item_param_length'] = $params['length'];
469 $retval['item_param_length_arr'] = $params['length_arr'];
470 $retval['item_param_opts_num'] = $params['opts'];
471 $retval['item_param_opts_text'] = $params['opts'];
478 if ($retval['item_type'] === 'FUNCTION') {
479 $retval['item_type_toggle'] = 'PROCEDURE';
481 $retval['item_type_toggle'] = 'FUNCTION';
484 $retval['item_returntype'] = '';
485 $retval['item_returnlength'] = '';
486 $retval['item_returnopts_num'] = '';
487 $retval['item_returnopts_text'] = '';
489 if (! empty($routine['DTD_IDENTIFIER'])) {
491 foreach ($stmt->return->options
->options
as $opt) {
492 $options[] = is_string($opt) ?
$opt : $opt['value'];
495 $retval['item_returntype'] = $stmt->return->name
;
496 $retval['item_returnlength'] = implode(',', $stmt->return->parameters
);
497 $retval['item_returnopts_num'] = implode(' ', $options);
498 $retval['item_returnopts_text'] = implode(' ', $options);
501 $retval['item_definer'] = $stmt->options
->has('DEFINER');
502 $retval['item_definition'] = $body;
503 $retval['item_isdeterministic'] = '';
504 if ($routine['IS_DETERMINISTIC'] === 'YES') {
505 $retval['item_isdeterministic'] = " checked='checked'";
508 $retval['item_securitytype_definer'] = '';
509 $retval['item_securitytype_invoker'] = '';
510 if ($routine['SECURITY_TYPE'] === 'DEFINER') {
511 $retval['item_securitytype_definer'] = " selected='selected'";
512 } elseif ($routine['SECURITY_TYPE'] === 'INVOKER') {
513 $retval['item_securitytype_invoker'] = " selected='selected'";
516 $retval['item_sqldataaccess'] = $routine['SQL_DATA_ACCESS'];
517 $retval['item_comment'] = $routine['ROUTINE_COMMENT'];
523 * Creates one row for the parameter table used in the routine editor.
525 * @param mixed[] $routine Data for the routine returned by
526 * getDataFromRequest() or getDataFromName()
527 * @param mixed $index Either a numeric index of the row being processed
528 * or NULL to create a template row for AJAX request
529 * @param string $class Class used to hide the direction column, if the
530 * row is for a stored function.
534 public function getParameterRow(array $routine = [], mixed $index = null, string $class = ''): array
536 if ($index === null) {
537 // template row for AJAX request
542 'item_param_dir' => [''],
543 'item_param_name' => [''],
544 'item_param_type' => [''],
545 'item_param_length' => [''],
546 'item_param_opts_num' => [''],
547 'item_param_opts_text' => [''],
549 } elseif ($routine !== []) {
550 // regular row for routine editor
551 $dropClass = ' hide';
554 // No input data. This shouldn't happen,
555 // but better be safe than sorry.
559 $allCharsets = Charsets
::getCharsets($this->dbi
, Config
::getInstance()->selectedServer
['DisableIS']);
561 foreach ($allCharsets as $charset) {
563 'name' => $charset->getName(),
564 'description' => $charset->getDescription(),
565 'is_selected' => $charset->getName() === $routine['item_param_opts_text'][$i],
572 'param_directions' => $this->directions
,
573 'param_opts_num' => $this->numericOptions
,
574 'item_param_dir' => $routine['item_param_dir'][$i] ??
'',
575 'item_param_name' => $routine['item_param_name'][$i] ??
'',
576 'item_param_length' => $routine['item_param_length'][$i] ??
'',
577 'item_param_opts_num' => $routine['item_param_opts_num'][$i] ??
'',
578 'supported_datatypes' => Generator
::getSupportedDatatypes(
579 $this->dbi
->types
->mapAliasToMysqlType($routine['item_param_type'][$i]),
581 'charsets' => $charsets,
582 'drop_class' => $dropClass,
587 * Set the found errors and build the params
589 * @param string[] $itemParamName The parameter names
590 * @param string[] $itemParamDir The direction parameter (see $this->directions)
591 * @param mixed[] $itemParamType The parameter type
592 * @param mixed[] $itemParamLength A length or not for the parameter
593 * @param mixed[] $itemParamOpsText An optional charset for the parameter
594 * @param mixed[] $itemParamOpsNum An optional parameter for a $itemParamType NUMBER
595 * @param string $itemType The item type (PROCEDURE/FUNCTION)
596 * @param bool $warnedAboutLength A boolean that will be switched if a the length warning is given
598 private function processParamsAndBuild(
599 array $itemParamName,
601 array $itemParamType,
602 array $itemParamLength,
603 array $itemParamOpsText,
604 array $itemParamOpsNum,
606 bool &$warnedAboutLength,
608 $GLOBALS['errors'] ??
= null;
611 $warnedAboutDir = false;
613 for ($i = 0, $nb = count($itemParamName); $i < $nb; $i++
) {
614 if (empty($itemParamName[$i]) ||
empty($itemParamType[$i])) {
615 $GLOBALS['errors'][] = __('You must provide a name and a type for each routine parameter.');
620 $itemType === 'PROCEDURE'
621 && ! empty($itemParamDir[$i])
622 && in_array($itemParamDir[$i], $this->directions
, true)
624 $params .= $itemParamDir[$i] . ' '
625 . Util
::backquote($itemParamName[$i])
626 . ' ' . $itemParamType[$i];
627 } elseif ($itemType === 'FUNCTION') {
628 $params .= Util
::backquote($itemParamName[$i])
629 . ' ' . $itemParamType[$i];
630 } elseif (! $warnedAboutDir) {
631 $warnedAboutDir = true;
632 $GLOBALS['errors'][] = sprintf(
633 __('Invalid direction "%s" given for parameter.'),
634 htmlspecialchars($itemParamDir[$i]),
639 $itemParamLength[$i] != ''
641 '@^(DATE|TINYBLOB|TINYTEXT|BLOB|TEXT|MEDIUMBLOB|MEDIUMTEXT|LONGBLOB|LONGTEXT|SERIAL|BOOLEAN)$@i',
645 $params .= '(' . $itemParamLength[$i] . ')';
647 $itemParamLength[$i] == ''
648 && preg_match('@^(ENUM|SET|VARCHAR|VARBINARY)$@i', $itemParamType[$i])
650 if (! $warnedAboutLength) {
651 $warnedAboutLength = true;
652 $GLOBALS['errors'][] = __(
653 'You must provide length/values for routine parameters'
654 . ' of type ENUM, SET, VARCHAR and VARBINARY.',
659 if (! empty($itemParamOpsText[$i])) {
660 if ($this->dbi
->types
->getTypeClass($itemParamType[$i]) === 'CHAR') {
661 if (! in_array($itemParamType[$i], ['VARBINARY', 'BINARY'], true)) {
662 $params .= ' CHARSET '
663 . mb_strtolower($itemParamOpsText[$i]);
668 if (! empty($itemParamOpsNum[$i])) {
669 if ($this->dbi
->types
->getTypeClass($itemParamType[$i]) === 'NUMBER') {
671 . mb_strtoupper($itemParamOpsNum[$i]);
675 if ($i === count($itemParamName) - 1) {
686 * Set the found errors and build the query
688 * @param string $query The existing query
689 * @param bool $warnedAboutLength If the length warning was given
691 private function processFunctionSpecificParameters(
693 bool $warnedAboutLength,
695 $GLOBALS['errors'] ??
= null;
697 $itemReturnType = $_POST['item_returntype'] ??
null;
699 if ($itemReturnType !== '' && in_array($itemReturnType, Util
::getSupportedDatatypes(), true)) {
700 $query .= 'RETURNS ' . $itemReturnType;
702 $GLOBALS['errors'][] = __('You must provide a valid return type for the routine.');
706 ! empty($_POST['item_returnlength'])
708 '@^(DATE|DATETIME|TIME|TINYBLOB|TINYTEXT|BLOB|TEXT|'
709 . 'MEDIUMBLOB|MEDIUMTEXT|LONGBLOB|LONGTEXT|SERIAL|BOOLEAN)$@i',
713 $query .= '(' . $_POST['item_returnlength'] . ')';
715 empty($_POST['item_returnlength'])
716 && preg_match('@^(ENUM|SET|VARCHAR|VARBINARY)$@i', $itemReturnType)
718 if (! $warnedAboutLength) {
719 $GLOBALS['errors'][] = __(
720 'You must provide length/values for routine parameters of type ENUM, SET, VARCHAR and VARBINARY.',
725 if (! empty($_POST['item_returnopts_text'])) {
726 if ($this->dbi
->types
->getTypeClass($itemReturnType) === 'CHAR') {
727 $query .= ' CHARSET '
728 . mb_strtolower($_POST['item_returnopts_text']);
732 if (! empty($_POST['item_returnopts_num'])) {
733 if ($this->dbi
->types
->getTypeClass($itemReturnType) === 'NUMBER') {
735 . mb_strtoupper($_POST['item_returnopts_num']);
743 * Composes the query necessary to create a routine from an HTTP request.
745 * @return string The CREATE [ROUTINE | PROCEDURE] query.
747 public function getQueryFromRequest(): string
749 $GLOBALS['errors'] ??
= null;
751 $itemType = $_POST['item_type'] ??
'';
752 $itemDefiner = $_POST['item_definer'] ??
'';
753 $itemName = $_POST['item_name'] ??
'';
756 if (! empty($itemDefiner)) {
757 if (str_contains($itemDefiner, '@')) {
758 $arr = explode('@', $itemDefiner);
761 if (str_starts_with($arr[0], '`') && str_ends_with($arr[0], '`')) {
762 $doBackquote = false;
765 $query .= 'DEFINER=' . Util
::backquoteCompat($arr[0], 'NONE', $doBackquote);
768 if (str_starts_with($arr[1], '`') && str_ends_with($arr[1], '`')) {
769 $doBackquote = false;
772 $query .= '@' . Util
::backquoteCompat($arr[1], 'NONE', $doBackquote) . ' ';
774 $GLOBALS['errors'][] = __('The definer must be in the "username@hostname" format!');
778 if ($itemType === 'FUNCTION' ||
$itemType === 'PROCEDURE') {
779 $query .= $itemType . ' ';
781 $GLOBALS['errors'][] = sprintf(
782 __('Invalid routine type: "%s"'),
783 htmlspecialchars($itemType),
787 if (! empty($itemName)) {
788 $query .= Util
::backquote($itemName);
790 $GLOBALS['errors'][] = __('You must provide a routine name!');
793 $warnedAboutLength = false;
795 $itemParamName = $_POST['item_param_name'] ??
'';
796 $itemParamType = $_POST['item_param_type'] ??
'';
797 $itemParamLength = $_POST['item_param_length'] ??
'';
798 $itemParamDir = (array) ($_POST['item_param_dir'] ??
[]);
799 $itemParamOpsText = (array) ($_POST['item_param_opts_text'] ??
[]);
800 $itemParamOpsNum = (array) ($_POST['item_param_opts_num'] ??
[]);
804 ! empty($itemParamName)
805 && ! empty($itemParamType)
806 && ! empty($itemParamLength)
807 && is_array($itemParamName)
808 && is_array($itemParamType)
809 && is_array($itemParamLength)
811 $params = $this->processParamsAndBuild(
819 $warnedAboutLength, // Will possibly be modified by the function
823 $query .= '(' . $params . ') ';
824 if ($itemType === 'FUNCTION') {
825 $query = $this->processFunctionSpecificParameters($query, $warnedAboutLength);
828 if (! empty($_POST['item_comment'])) {
829 $query .= 'COMMENT ' . $this->dbi
->quoteString($_POST['item_comment']) . ' ';
832 if (isset($_POST['item_isdeterministic'])) {
833 $query .= 'DETERMINISTIC ';
835 $query .= 'NOT DETERMINISTIC ';
838 $itemSqlDataAccess = $_POST['item_sqldataaccess'] ??
'';
839 if (in_array($itemSqlDataAccess, $this->sqlDataAccess
, true)) {
840 $query .= $itemSqlDataAccess . ' ';
843 $itemSecurityType = $_POST['item_securitytype'] ??
'';
844 if (! empty($itemSecurityType)) {
845 if ($itemSecurityType === 'DEFINER' ||
$itemSecurityType === 'INVOKER') {
846 $query .= 'SQL SECURITY ' . $itemSecurityType . ' ';
850 $itemDefinition = $_POST['item_definition'] ??
'';
851 if (! empty($itemDefinition)) {
852 $query .= $itemDefinition;
854 $GLOBALS['errors'][] = __('You must provide a routine definition.');
861 * @param mixed[] $routine The routine params
863 * @return string[] The SQL queries / SQL query parts
865 private function getQueriesFromRoutineForm(array $routine): array
870 $allFunctions = $this->dbi
->types
->getAllFunctions();
871 for ($i = 0; $i < $routine['item_num_params']; $i++
) {
872 if (isset($_POST['params'][$routine['item_param_name'][$i]])) {
873 $value = $_POST['params'][$routine['item_param_name'][$i]];
874 if (is_array($value)) { // is SET type
875 $value = implode(',', $value);
879 ! empty($_POST['funcs'][$routine['item_param_name'][$i]])
880 && in_array($_POST['funcs'][$routine['item_param_name'][$i]], $allFunctions, true)
882 $queries[] = sprintf(
885 $_POST['funcs'][$routine['item_param_name'][$i]],
886 $this->dbi
->quoteString($value),
889 $queries[] = 'SET @p' . $i . '=' . $this->dbi
->quoteString($value) . ';';
895 if ($routine['item_type'] !== 'PROCEDURE') {
899 if ($routine['item_param_dir'][$i] !== 'OUT' && $routine['item_param_dir'][$i] !== 'INOUT') {
903 $outParams[] = '@p' . $i . ' AS ' . Util
::backquote($routine['item_param_name'][$i]);
906 if ($routine['item_type'] === 'PROCEDURE') {
907 $queries[] = sprintf(
909 Util
::backquote($routine['item_name']),
910 implode(', ', $args),
912 if ($outParams !== []) {
913 $queries[] = 'SELECT ' . implode(', ', $outParams) . ';';
916 $queries[] = sprintf(
917 'SELECT %s(%s) AS %s;',
918 Util
::backquote($routine['item_name']),
919 implode(', ', $args),
920 Util
::backquote($routine['item_name']),
928 * @param mixed[] $routine
930 * @psalm-return array{string, Message}
932 public function handleExecuteRoutine(array $routine): array
934 $queries = $this->getQueriesFromRoutineForm($routine);
937 $resultHtmlTables = '';
938 $nbResultsetToDisplay = 0;
940 foreach ($queries as $query) {
941 $result = $this->dbi
->tryQuery($query);
944 while ($result !== false) {
945 if ($result->numRows() > 0) {
946 $resultHtmlTables .= '<table class="table table-striped w-auto"><tr>';
947 foreach ($result->getFieldNames() as $field) {
948 $resultHtmlTables .= '<th>';
949 $resultHtmlTables .= htmlspecialchars($field);
950 $resultHtmlTables .= '</th>';
953 $resultHtmlTables .= '</tr>';
955 foreach ($result as $row) {
956 $resultHtmlTables .= '<tr>' . $this->browseRow($row) . '</tr>';
959 $resultHtmlTables .= '</table>';
960 $nbResultsetToDisplay++
;
961 $affected = $result->numRows();
964 $result = $this->dbi
->nextResult();
967 // We must check for an error after fetching the results because
968 // either tryQuery might have produced an error or any of nextResult calls.
969 if ($this->dbi
->getError() !== '') {
970 $message = Message
::error(
972 __('The following query has failed: "%s"'),
973 htmlspecialchars($query),
976 . __('MySQL said: ') . $this->dbi
->getError(),
979 return ['', $message];
983 // Pass the SQL queries through the "pretty printer"
984 $output = Generator
::formatSql(implode("\n", $queries));
986 $output .= '<div class="card my-3"><div class="card-header">';
988 __('Execution results of routine %s'),
989 htmlspecialchars(Util
::backquote($routine['item_name'])),
991 $output .= '</div><div class="card-body">';
992 $output .= $resultHtmlTables;
993 $output .= '</div></div>';
995 $message = __('Your SQL query has been executed successfully.');
996 if ($routine['item_type'] === 'PROCEDURE') {
999 // TODO : message need to be modified according to the
1000 // output from the routine
1001 $message .= sprintf(
1003 '%d row affected by the last statement inside the procedure.',
1004 '%d rows affected by the last statement inside the procedure.',
1011 if ($nbResultsetToDisplay === 0) {
1012 $notice = __('MySQL returned an empty result set (i.e. zero rows).');
1013 $output .= Message
::notice($notice)->getDisplay();
1016 return [$output, Message
::success($message)];
1022 * @param (string|null)[] $row Columns
1024 private function browseRow(array $row): string
1027 foreach ($row as $value) {
1028 if ($value === null) {
1029 $value = '<i>NULL</i>';
1031 $value = htmlspecialchars($value);
1034 $output .= '<td>' . $value . '</td>';
1041 * Creates the HTML code that shows the routine execution dialog.
1043 * @param mixed[] $routine Data for the routine returned by getDataFromName()
1045 * @psalm-return array{mixed[], mixed[]}
1047 public function getExecuteForm(array $routine): array
1049 // Escape special characters
1050 $routine['item_name'] = htmlentities($routine['item_name'], ENT_QUOTES
);
1051 for ($i = 0; $i < $routine['item_num_params']; $i++
) {
1052 $routine['item_param_name'][$i] = htmlentities($routine['item_param_name'][$i], ENT_QUOTES
);
1055 $noSupportTypes = Util
::unsupportedDatatypes();
1058 $params['no_support_types'] = $noSupportTypes;
1060 for ($i = 0; $i < $routine['item_num_params']; $i++
) {
1061 if ($routine['item_type'] === 'PROCEDURE' && $routine['item_param_dir'][$i] === 'OUT') {
1065 if (Config
::getInstance()->settings
['ShowFunctionFields']) {
1067 stripos($routine['item_param_type'][$i], 'enum') !== false
1068 ||
stripos($routine['item_param_type'][$i], 'set') !== false
1070 mb_strtolower($routine['item_param_type'][$i]),
1075 $params[$i]['generator'] = null;
1077 $defaultFunction = Generator
::getDefaultFunctionForField(
1078 mb_strtolower($routine['item_param_type'][$i]),
1087 $params[$i]['generator'] = Generator
::getFunctionsForField($defaultFunction);
1091 if ($routine['item_param_type'][$i] === 'DATETIME' ||
$routine['item_param_type'][$i] === 'TIMESTAMP') {
1092 $params[$i]['class'] = 'datetimefield';
1093 } elseif ($routine['item_param_type'][$i] === 'DATE') {
1094 $params[$i]['class'] = 'datefield';
1097 if (in_array($routine['item_param_type'][$i], ['ENUM', 'SET'], true)) {
1098 if ($routine['item_param_type'][$i] === 'ENUM') {
1099 $params[$i]['input_type'] = 'radio';
1101 $params[$i]['input_type'] = 'checkbox';
1104 foreach ($routine['item_param_length_arr'][$i] as $value) {
1105 $value = htmlentities(Util
::unQuote($value), ENT_QUOTES
);
1106 $params[$i]['htmlentities'][] = $value;
1108 } elseif (in_array(mb_strtolower($routine['item_param_type'][$i]), $noSupportTypes, true)) {
1109 $params[$i]['input_type'] = null;
1111 $params[$i]['input_type'] = 'text';
1115 return [$routine, $params];
1119 * Creates the contents for a row in the list of routines
1121 * @param string $rowClass Additional class
1125 public function getRow(Routine
$routine, string $rowClass = ''): array
1128 'DROP %s IF EXISTS %s',
1130 Util
::backquote($routine->name
),
1133 // this is for our purpose to decide whether to
1134 // show the edit link or not, so we need the DEFINER for the routine
1135 $where = 'ROUTINE_SCHEMA ' . Util
::getCollateForIS() . '=' . $this->dbi
->quoteString(Current
::$database)
1136 . ' AND SPECIFIC_NAME=' . $this->dbi
->quoteString($routine->name
)
1137 . ' AND ROUTINE_TYPE=' . $this->dbi
->quoteString($routine->type
);
1138 $query = 'SELECT `DEFINER` FROM INFORMATION_SCHEMA.ROUTINES WHERE ' . $where . ';';
1139 $routineDefiner = $this->dbi
->fetchValue($query);
1141 $currentUser = $this->dbi
->getCurrentUser();
1142 $currentUserIsRoutineDefiner = $currentUser === $routineDefiner;
1144 // Since editing a procedure involved dropping and recreating, check also for
1145 // CREATE ROUTINE privilege to avoid lost procedures.
1146 $hasCreateRoutine = Util
::currentUserHasPrivilege('CREATE ROUTINE', Current
::$database);
1147 $hasEditPrivilege = ($hasCreateRoutine && $currentUserIsRoutineDefiner)
1148 ||
$this->dbi
->isSuperUser();
1149 $hasExportPrivilege = ($hasCreateRoutine && $currentUserIsRoutineDefiner)
1150 ||
$this->dbi
->isSuperUser();
1151 $hasExecutePrivilege = Util
::currentUserHasPrivilege('EXECUTE', Current
::$database)
1152 ||
$currentUserIsRoutineDefiner;
1154 // There is a problem with Util::currentUserHasPrivilege():
1155 // it does not detect all kinds of privileges, for example
1156 // a direct privilege on a specific routine. So, at this point,
1157 // we show the Execute link, hoping that the user has the correct rights.
1158 // Also, information_schema might be hiding the ROUTINE_DEFINITION
1159 // but a routine with no input parameters can be nonetheless executed.
1161 // Check if the routine has any input parameters. If it does,
1162 // we will show a dialog to get values for these parameters,
1163 // otherwise we can execute it directly.
1165 if ($routine->type
=== 'FUNCTION') {
1166 $definition = self
::getFunctionDefinition($this->dbi
, Current
::$database, $routine->name
);
1168 $definition = self
::getProcedureDefinition($this->dbi
, Current
::$database, $routine->name
);
1171 $executeAction = '';
1173 if ($definition !== null) {
1174 $parser = new Parser('DELIMITER $$' . "\n" . $definition);
1176 /** @var CreateStatement $stmt */
1177 $stmt = $parser->statements
[0];
1179 $params = RoutineUtils
::getParameters($stmt);
1181 if ($hasExecutePrivilege) {
1182 $executeAction = 'execute_routine';
1183 for ($i = 0; $i < $params['num']; $i++
) {
1184 if ($routine->type
=== 'PROCEDURE' && $params['dir'][$i] === 'OUT') {
1188 $executeAction = 'execute_dialog';
1195 'db' => Current
::$database,
1196 'table' => Current
::$table,
1197 'sql_drop' => $sqlDrop,
1198 'routine' => $routine,
1199 'row_class' => $rowClass,
1200 'has_edit_privilege' => $hasEditPrivilege,
1201 'has_export_privilege' => $hasExportPrivilege,
1202 'has_execute_privilege' => $hasExecutePrivilege,
1203 'execute_action' => $executeAction,
1208 * returns details about the PROCEDUREs or FUNCTIONs for a specific database
1209 * or details about a specific routine
1211 * @param string $db db name
1212 * @param string|null $which PROCEDURE | FUNCTION or null for both
1213 * @param string $name name of the routine (to fetch a specific routine)
1217 public static function getDetails(
1218 DatabaseInterface
$dbi,
1220 string|
null $which = null,
1223 if (! Config
::getInstance()->selectedServer
['DisableIS']) {
1224 $query = QueryGenerator
::getInformationSchemaRoutinesRequest(
1225 $dbi->quoteString($db),
1226 in_array($which, ['FUNCTION', 'PROCEDURE'], true) ?
$which : null,
1227 $name === '' ?
null : $dbi->quoteString($name),
1229 $routines = $dbi->fetchResult($query);
1233 if ($which === 'FUNCTION' ||
$which == null) {
1234 $query = 'SHOW FUNCTION STATUS WHERE `Db` = ' . $dbi->quoteString($db);
1236 $query .= ' AND `Name` = ' . $dbi->quoteString($name);
1239 $routines = $dbi->fetchResult($query);
1242 if ($which === 'PROCEDURE' ||
$which == null) {
1243 $query = 'SHOW PROCEDURE STATUS WHERE `Db` = ' . $dbi->quoteString($db);
1245 $query .= ' AND `Name` = ' . $dbi->quoteString($name);
1248 $routines = array_merge($routines, $dbi->fetchResult($query));
1253 /** @var array{Name:string, Type:string, DTD_IDENTIFIER:string|null} $routine */
1254 foreach ($routines as $routine) {
1255 $ret[] = new Routine($routine['Name'], $routine['Type'], $routine['DTD_IDENTIFIER'] ??
'');
1258 // Sort results by name
1259 $name = array_column($ret, 'name');
1260 array_multisort($name, SORT_ASC
, $ret);
1265 public static function getFunctionDefinition(DatabaseInterface
$dbi, string $db, string $name): string|
null
1267 $result = $dbi->fetchValue(
1268 'SHOW CREATE FUNCTION ' . Util
::backquote($db) . '.' . Util
::backquote($name),
1272 return is_string($result) ?
$result : null;
1275 public static function getProcedureDefinition(DatabaseInterface
$dbi, string $db, string $name): string|
null
1277 $result = $dbi->fetchValue(
1278 'SHOW CREATE PROCEDURE ' . Util
::backquote($db) . '.' . Util
::backquote($name),
1282 return is_string($result) ?
$result : null;
1286 * @return array<int, string>
1287 * @psalm-return list<non-empty-string>
1289 public static function getFunctionNames(DatabaseInterface
$dbi, string $db): array
1291 /** @psalm-var list<array{Db: string, Name: string, Type: string}> $functions */
1292 $functions = $dbi->fetchResult('SHOW FUNCTION STATUS;');
1294 foreach ($functions as $function) {
1295 if ($function['Db'] !== $db ||
$function['Type'] !== 'FUNCTION' ||
$function['Name'] === '') {
1299 $names[] = $function['Name'];
1306 * @return array<int, string>
1307 * @psalm-return list<non-empty-string>
1309 public static function getProcedureNames(DatabaseInterface
$dbi, string $db): array
1311 /** @psalm-var list<array{Db: string, Name: string, Type: string}> $procedures */
1312 $procedures = $dbi->fetchResult('SHOW PROCEDURE STATUS;');
1314 foreach ($procedures as $procedure) {
1315 if ($procedure['Db'] !== $db ||
$procedure['Type'] !== 'PROCEDURE' ||
$procedure['Name'] === '') {
1319 $names[] = $procedure['Name'];