Routines   F
last analyzed

Complexity

Total Complexity 178

Size/Duplication

Total Lines 1214
Duplicated Lines 0 %

Test Coverage

Coverage 41.96%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 178
eloc 642
dl 0
loc 1214
ccs 282
cts 672
cp 0.4196
rs 1.958
c 2
b 0
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
A getParameterRow() 0 49 4
A getParameters() 0 34 5
A flushPrivileges() 0 22 2
A backupPrivileges() 0 18 4
B handleRequestCreateOrEdit() 0 91 10
F getDataFromRequest() 0 112 26
A __construct() 0 5 1
B create() 0 59 6
A getRoutineCount() 0 8 1
C getDataFromName() 0 91 13
A getDetails() 0 29 3
A getFunctionDefinition() 0 8 2
C processFunctionSpecificParameters() 0 47 12
D processParamsAndBuild() 0 83 20
A browseRow() 0 10 3
B getRow() 0 41 6
B handleExecuteRoutine() 0 85 9
B getQueriesFromRoutineForm() 0 60 11
A getErrorCount() 0 3 1
A getProcedureDefinition() 0 8 2
C getExecuteForm() 0 54 14
F getQueryFromRequest() 0 105 22
A getNames() 0 10 1

How to fix   Complexity   

Complex Class

Complex classes like Routines often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Routines, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\Database;
6
7
use PhpMyAdmin\Charsets;
8
use PhpMyAdmin\Config;
9
use PhpMyAdmin\Current;
10
use PhpMyAdmin\Dbal\DatabaseInterface;
11
use PhpMyAdmin\Html\Generator;
12
use PhpMyAdmin\Http\ServerRequest;
13
use PhpMyAdmin\Message;
14
use PhpMyAdmin\Query\Generator as QueryGenerator;
15
use PhpMyAdmin\SqlParser\Parser;
16
use PhpMyAdmin\SqlParser\Statements\CreateStatement;
17
use PhpMyAdmin\SqlParser\TokensList;
18
use PhpMyAdmin\TypeClass;
19
use PhpMyAdmin\UserPrivileges;
20
use PhpMyAdmin\Util;
21
22
use function __;
23
use function _ngettext;
24
use function array_merge;
25
use function count;
26
use function explode;
27
use function htmlentities;
28
use function htmlspecialchars;
29
use function implode;
30
use function in_array;
31
use function is_array;
32
use function is_string;
33
use function max;
34
use function mb_strtolower;
35
use function mb_strtoupper;
36
use function preg_match;
37
use function sprintf;
38
use function str_contains;
39
use function str_ends_with;
40
use function str_starts_with;
41
use function stripos;
42
43
use const ENT_QUOTES;
44
45
/**
46
 * Functions for routine management.
47
 */
48
class Routines
49
{
50
    /** @var array<int, string> */
51
    public readonly array $directions;
52
53
    /** @var array<int, string> */
54
    public readonly array $sqlDataAccess;
55
56
    /** @var array<int, string> */
57
    public readonly array $numericOptions;
58
59
    /** @var array<string> */
60
    private array $errors = [];
61
62 72
    public function __construct(private DatabaseInterface $dbi)
63
    {
64 72
        $this->directions = ['IN', 'OUT', 'INOUT'];
0 ignored issues
show
Bug introduced by
The property directions is declared read-only in PhpMyAdmin\Database\Routines.
Loading history...
65 72
        $this->sqlDataAccess = ['CONTAINS SQL', 'NO SQL', 'READS SQL DATA', 'MODIFIES SQL DATA'];
0 ignored issues
show
Bug introduced by
The property sqlDataAccess is declared read-only in PhpMyAdmin\Database\Routines.
Loading history...
66 72
        $this->numericOptions = ['UNSIGNED', 'ZEROFILL', 'UNSIGNED ZEROFILL'];
0 ignored issues
show
Bug introduced by
The property numericOptions is declared read-only in PhpMyAdmin\Database\Routines.
Loading history...
67
    }
68
69
    /**
70
     * Handle request to create or edit a routine
71
     */
72
    public function handleRequestCreateOrEdit(
73
        UserPrivileges $userPrivileges,
74
        string $db,
75
        ServerRequest $request,
76
    ): string {
77
        $sqlQuery = '';
78
        $routineQuery = $this->getQueryFromRequest($request);
79
80
        // set by getQueryFromRequest()
81
        if ($this->errors === []) {
82
            // Execute the created query
83
            if (! empty($_POST['editor_process_edit'])) {
84
                if (! in_array($_POST['item_original_type'], ['PROCEDURE', 'FUNCTION'], true)) {
85
                    $this->errors[] = sprintf(
86
                        __('Invalid routine type: "%s"'),
87
                        htmlspecialchars($_POST['item_original_type']),
88
                    );
89
                } else {
90
                    // Backup the old routine, in case something goes wrong
91
                    if ($_POST['item_original_type'] === 'FUNCTION') {
92
                        $createRoutine = self::getFunctionDefinition($this->dbi, $db, $_POST['item_original_name']);
93
                    } else {
94
                        $createRoutine = self::getProcedureDefinition($this->dbi, $db, $_POST['item_original_name']);
95
                    }
96
97
                    $privilegesBackup = $this->backupPrivileges($userPrivileges);
98
99
                    $dropRoutine = 'DROP ' . $_POST['item_original_type'] . ' '
100
                        . Util::backquote($_POST['item_original_name'])
101
                        . ";\n";
102
                    $result = $this->dbi->tryQuery($dropRoutine);
103
                    if (! $result) {
104
                        $this->errors[] = sprintf(
105
                            __('The following query has failed: "%s"'),
106
                            htmlspecialchars($dropRoutine),
107
                        )
108
                        . '<br>'
109
                        . __('MySQL said: ') . $this->dbi->getError();
110
                    } else {
111
                        [$newErrors, Current::$message] = $this->create(
112
                            $userPrivileges,
113
                            $routineQuery,
114
                            $createRoutine,
115
                            $privilegesBackup,
116
                        );
117
                        if ($newErrors === []) {
118
                            $sqlQuery = $dropRoutine . $routineQuery;
119
                        } else {
120
                            $this->errors = array_merge($this->errors, $newErrors);
121
                        }
122
123
                        unset($newErrors);
124
                    }
125
                }
126
            } else {
127
                // 'Add a new routine' mode
128
                $result = $this->dbi->tryQuery($routineQuery);
129
                if (! $result) {
130
                    $this->errors[] = sprintf(
131
                        __('The following query has failed: "%s"'),
132
                        htmlspecialchars($routineQuery),
133
                    )
134
                    . '<br><br>'
135
                    . __('MySQL said: ') . $this->dbi->getError();
136
                } else {
137
                    Current::$message = Message::success(
138
                        __('Routine %1$s has been created.'),
139
                    );
140
                    Current::$message->addParam(
141
                        Util::backquote($_POST['item_name']),
142
                    );
143
                    $sqlQuery = $routineQuery;
144
                }
145
            }
146
        }
147
148
        if ($this->errors !== []) {
149
            Current::$message = Message::error(
150
                __(
151
                    'One or more errors have occurred while processing your request:',
152
                ),
153
            );
154
            Current::$message->addHtml('<ul>');
155
            foreach ($this->errors as $string) {
156
                Current::$message->addHtml('<li>' . $string . '</li>');
157
            }
158
159
            Current::$message->addHtml('</ul>');
160
        }
161
162
        return Generator::getMessage(Current::$message ?? Message::success(), $sqlQuery);
163
    }
164
165
    /**
166
     * Backup the privileges
167
     *
168
     * @return string[][]
169
     */
170
    public function backupPrivileges(UserPrivileges $userPrivileges): array
171
    {
172
        if (! $userPrivileges->routines || ! $userPrivileges->isReload) {
173
            return [];
174
        }
175
176
        // Backup the Old Privileges before dropping
177
        // if $_POST['item_adjust_privileges'] set
178
        if (empty($_POST['item_adjust_privileges'])) {
179
            return [];
180
        }
181
182
        $privilegesBackupQuery = 'SELECT * FROM ' . Util::backquote('mysql')
183
        . '.' . Util::backquote('procs_priv')
184
        . ' WHERE Routine_name = ' . $this->dbi->quoteString($_POST['item_original_name'])
185
        . ' AND Routine_type = ' . $this->dbi->quoteString($_POST['item_original_type']);
186
187
        return $this->dbi->fetchResult($privilegesBackupQuery, 0);
188
    }
189
190
    /**
191
     * Create the routine
192
     *
193
     * @param string     $routineQuery     Query to create routine
194
     * @param string     $createRoutine    Query to restore routine
195
     * @param string[][] $privilegesBackup Privileges backup
196
     *
197
     * @return array{string[], Message|null}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{string[], Message|null} at position 2 could not be parsed: Expected ':' at position 2, but found 'string'.
Loading history...
198
     */
199
    public function create(
200
        UserPrivileges $userPrivileges,
201
        string $routineQuery,
202
        string $createRoutine,
203
        array $privilegesBackup,
204
    ): array {
205
        $result = $this->dbi->tryQuery($routineQuery);
206
        if (! $result) {
207
            $errors = [];
208
            $errors[] = sprintf(
209
                __('The following query has failed: "%s"'),
210
                htmlspecialchars($routineQuery),
211
            )
212
            . '<br>'
213
            . __('MySQL said: ') . $this->dbi->getError();
214
            // We dropped the old routine,
215
            // but were unable to create the new one
216
            // Try to restore the backup query
217
            $result = $this->dbi->tryQuery($createRoutine);
218
            if (! $result) {
219
                // OMG, this is really bad! We dropped the query,
220
                // failed to create a new one
221
                // and now even the backup query does not execute!
222
                // This should not happen, but we better handle
223
                // this just in case.
224
                $errors[] = __('Sorry, we failed to restore the dropped routine.') . '<br>'
225
                . __('The backed up query was:')
226
                . '"' . htmlspecialchars($createRoutine) . '"<br>'
227
                . __('MySQL said: ') . $this->dbi->getError();
228
            }
229
230
            return [$errors, null];
231
        }
232
233
        // Default value
234
        $resultAdjust = false;
235
236
        if ($userPrivileges->routines && $userPrivileges->isReload) {
237
            // Insert all the previous privileges
238
            // but with the new name and the new type
239
            foreach ($privilegesBackup as $priv) {
240
                $adjustProcPrivilege = 'INSERT INTO '
241
                    . Util::backquote('mysql') . '.'
242
                    . Util::backquote('procs_priv')
243
                    . ' VALUES(' . $this->dbi->quoteString($priv[0]) . ', '
244
                    . $this->dbi->quoteString($priv[1]) . ', ' . $this->dbi->quoteString($priv[2]) . ', '
245
                    . $this->dbi->quoteString($_POST['item_name']) . ', '
246
                    . $this->dbi->quoteString($_POST['item_type']) . ', '
247
                    . $this->dbi->quoteString($priv[5]) . ', '
248
                    . $this->dbi->quoteString($priv[6]) . ', '
249
                    . $this->dbi->quoteString($priv[7]) . ');';
250
                $this->dbi->query($adjustProcPrivilege);
251
                $resultAdjust = true;
252
            }
253
        }
254
255
        $message = $this->flushPrivileges($resultAdjust);
256
257
        return [[], $message];
258
    }
259
260
    /**
261
     * Flush privileges and get message
262
     *
263
     * @param bool $flushPrivileges Flush privileges
264
     */
265
    public function flushPrivileges(bool $flushPrivileges): Message
266
    {
267
        if ($flushPrivileges) {
268
            // Flush the Privileges
269
            $this->dbi->tryQuery('FLUSH PRIVILEGES;');
270
271
            $message = Message::success(
272
                __(
273
                    'Routine %1$s has been modified. Privileges have been adjusted.',
274
                ),
275
            );
276
        } else {
277
            $message = Message::success(
278
                __('Routine %1$s has been modified.'),
279
            );
280
        }
281
282
        $message->addParam(
283
            Util::backquote($_POST['item_name']),
284
        );
285
286
        return $message;
287
    }
288
289
    /**
290
     * This function will generate the values that are required to
291
     * complete the editor form. It is especially necessary to handle
292
     * the 'Add another parameter', 'Remove last parameter' and
293
     * 'Change routine type' functionalities when JS is disabled.
294
     *
295
     * @return mixed[]    Data necessary to create the routine editor.
296
     */
297 12
    public function getDataFromRequest(): array
298
    {
299 12
        $retval = [];
300 12
        $indices = [
301 12
            'item_name',
302 12
            'item_original_name',
303 12
            'item_returnlength',
304 12
            'item_returnopts_num',
305 12
            'item_returnopts_text',
306 12
            'item_definition',
307 12
            'item_comment',
308 12
            'item_definer',
309 12
        ];
310 12
        foreach ($indices as $index) {
311 12
            $retval[$index] = $_POST[$index] ?? '';
312
        }
313
314 12
        $retval['item_type'] = 'PROCEDURE';
315 12
        $retval['item_type_toggle'] = 'FUNCTION';
316 12
        if (isset($_POST['item_type']) && $_POST['item_type'] === 'FUNCTION') {
317 4
            $retval['item_type'] = 'FUNCTION';
318 4
            $retval['item_type_toggle'] = 'PROCEDURE';
319
        }
320
321 12
        $retval['item_original_type'] = 'PROCEDURE';
322 12
        if (isset($_POST['item_original_type']) && $_POST['item_original_type'] === 'FUNCTION') {
323 4
            $retval['item_original_type'] = 'FUNCTION';
324
        }
325
326 12
        $retval['item_num_params'] = 0;
327 12
        $retval['item_param_dir'] = [];
328 12
        $retval['item_param_name'] = [];
329 12
        $retval['item_param_type'] = [];
330 12
        $retval['item_param_length'] = [];
331 12
        $retval['item_param_opts_num'] = [];
332 12
        $retval['item_param_opts_text'] = [];
333
        if (
334
            isset(
335 12
                $_POST['item_param_name'],
336 12
                $_POST['item_param_type'],
337 12
                $_POST['item_param_length'],
338 12
                $_POST['item_param_opts_num'],
339 12
                $_POST['item_param_opts_text'],
340
            )
341 12
            && is_array($_POST['item_param_name'])
342 12
            && is_array($_POST['item_param_type'])
343 12
            && is_array($_POST['item_param_length'])
344 12
            && is_array($_POST['item_param_opts_num'])
345 12
            && is_array($_POST['item_param_opts_text'])
346
        ) {
347 8
            if ($_POST['item_type'] === 'PROCEDURE') {
348 4
                $retval['item_param_dir'] = $_POST['item_param_dir'];
349 4
                foreach ($retval['item_param_dir'] as $key => $value) {
350 4
                    if (in_array($value, $this->directions, true)) {
351 4
                        continue;
352
                    }
353
354 4
                    $retval['item_param_dir'][$key] = '';
355
                }
356
            }
357
358 8
            $retval['item_param_name'] = $_POST['item_param_name'];
359 8
            $retval['item_param_type'] = $_POST['item_param_type'];
360 8
            foreach ($retval['item_param_type'] as $key => $value) {
361 8
                if (in_array($value, Util::getSupportedDatatypes(), true)) {
362 8
                    continue;
363
                }
364
365 8
                $retval['item_param_type'][$key] = '';
366
            }
367
368 8
            $retval['item_param_length'] = $_POST['item_param_length'];
369 8
            $retval['item_param_opts_num'] = $_POST['item_param_opts_num'];
370 8
            $retval['item_param_opts_text'] = $_POST['item_param_opts_text'];
371 8
            $retval['item_num_params'] = max(
372 8
                count($retval['item_param_name']),
373 8
                count($retval['item_param_type']),
374 8
                count($retval['item_param_length']),
375 8
                count($retval['item_param_opts_num']),
376 8
                count($retval['item_param_opts_text']),
377 8
            );
378
        }
379
380 12
        $retval['item_returntype'] = '';
381
        if (
382 12
            isset($_POST['item_returntype'])
383 12
            && in_array($_POST['item_returntype'], Util::getSupportedDatatypes(), true)
384
        ) {
385 4
            $retval['item_returntype'] = $_POST['item_returntype'];
386
        }
387
388 12
        $retval['item_isdeterministic'] = '';
389 12
        if (isset($_POST['item_isdeterministic']) && mb_strtolower($_POST['item_isdeterministic']) === 'on') {
390 4
            $retval['item_isdeterministic'] = " checked='checked'";
391
        }
392
393 12
        $retval['item_securitytype_definer'] = '';
394 12
        $retval['item_securitytype_invoker'] = '';
395 12
        if (isset($_POST['item_securitytype'])) {
396 8
            if ($_POST['item_securitytype'] === 'DEFINER') {
397 4
                $retval['item_securitytype_definer'] = " selected='selected'";
398 4
            } elseif ($_POST['item_securitytype'] === 'INVOKER') {
399 4
                $retval['item_securitytype_invoker'] = " selected='selected'";
400
            }
401
        }
402
403 12
        $retval['item_sqldataaccess'] = '';
404 12
        if (isset($_POST['item_sqldataaccess']) && in_array($_POST['item_sqldataaccess'], $this->sqlDataAccess, true)) {
405 4
            $retval['item_sqldataaccess'] = $_POST['item_sqldataaccess'];
406
        }
407
408 12
        return $retval;
409
    }
410
411
    /**
412
     * This function will generate the values that are required to complete
413
     * the "Edit routine" form given the name of a routine.
414
     *
415
     * @param string $name The name of the routine.
416
     * @param string $type Type of routine (ROUTINE|PROCEDURE)
417
     * @param bool   $all  Whether to return all data or just the info about parameters.
418
     *
419
     * @return mixed[]|null    Data necessary to create the routine editor.
420
     */
421
    public function getDataFromName(string $name, string $type, bool $all = true): array|null
422
    {
423
        $retval = [];
424
425
        // Build and execute the query
426
        $fields = 'SPECIFIC_NAME, ROUTINE_TYPE, DTD_IDENTIFIER, '
427
                 . 'ROUTINE_DEFINITION, IS_DETERMINISTIC, SQL_DATA_ACCESS, '
428
                 . 'ROUTINE_COMMENT, SECURITY_TYPE';
429
        $where = 'ROUTINE_SCHEMA ' . Util::getCollateForIS() . '=' . $this->dbi->quoteString(Current::$database)
430
                 . ' AND SPECIFIC_NAME=' . $this->dbi->quoteString($name)
431
                 . ' AND ROUTINE_TYPE=' . $this->dbi->quoteString($type);
432
        $query = 'SELECT ' . $fields . ' FROM INFORMATION_SCHEMA.ROUTINES WHERE ' . $where . ';';
433
434
        $routine = $this->dbi->fetchSingleRow($query);
435
436
        if ($routine === []) {
437
            return null;
438
        }
439
440
        // Get required data
441
        $retval['item_name'] = $routine['SPECIFIC_NAME'];
442
        $retval['item_type'] = $routine['ROUTINE_TYPE'];
443
444
        if ($routine['ROUTINE_TYPE'] === 'FUNCTION') {
445
            $definition = self::getFunctionDefinition($this->dbi, Current::$database, $routine['SPECIFIC_NAME']);
446
        } else {
447
            $definition = self::getProcedureDefinition($this->dbi, Current::$database, $routine['SPECIFIC_NAME']);
448
        }
449
450
        if ($definition === null) {
451
            return null;
452
        }
453
454
        $parser = new Parser('DELIMITER $$' . "\n" . $definition);
455
456
        /** @var CreateStatement $stmt */
457
        $stmt = $parser->statements[0];
458
459
        // Do not use $routine['ROUTINE_DEFINITION'] because of a MySQL escaping issue: #15370
460
        $body = TokensList::buildFromArray($stmt->body);
461
        if ($body === '') {
462
            // Fallback just in case the parser fails
463
            $body = (string) $routine['ROUTINE_DEFINITION'];
464
        }
465
466
        $retval = array_merge($retval, $this->getParameters($stmt));
467
        $retval['item_param_opts_text'] = $retval['item_param_opts_num'];
468
469
        // Get extra data
470
        if (! $all) {
471
            return $retval;
472
        }
473
474
        $retval['item_type_toggle'] = $retval['item_type'] === 'FUNCTION' ? 'PROCEDURE' : 'FUNCTION';
475
476
        $retval['item_returntype'] = '';
477
        $retval['item_returnlength'] = '';
478
        $retval['item_returnopts_num'] = '';
479
        $retval['item_returnopts_text'] = '';
480
481
        if (! empty($routine['DTD_IDENTIFIER'])) {
482
            $options = [];
483
            foreach ($stmt->return->options->options as $opt) {
484
                $options[] = is_string($opt) ? $opt : $opt['value'];
485
            }
486
487
            $retval['item_returntype'] = $stmt->return->name;
488
            $retval['item_returnlength'] = implode(',', $stmt->return->parameters);
489
            $retval['item_returnopts_num'] = implode(' ', $options);
490
            $retval['item_returnopts_text'] = implode(' ', $options);
491
        }
492
493
        $retval['item_definer'] = $stmt->options->get('DEFINER');
0 ignored issues
show
Bug introduced by
The method get() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

493
        /** @scrutinizer ignore-call */ 
494
        $retval['item_definer'] = $stmt->options->get('DEFINER');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
494
        $retval['item_definition'] = $body;
495
        $retval['item_isdeterministic'] = '';
496
        if ($routine['IS_DETERMINISTIC'] === 'YES') {
497
            $retval['item_isdeterministic'] = " checked='checked'";
498
        }
499
500
        $retval['item_securitytype_definer'] = '';
501
        $retval['item_securitytype_invoker'] = '';
502
        if ($routine['SECURITY_TYPE'] === 'DEFINER') {
503
            $retval['item_securitytype_definer'] = " selected='selected'";
504
        } elseif ($routine['SECURITY_TYPE'] === 'INVOKER') {
505
            $retval['item_securitytype_invoker'] = " selected='selected'";
506
        }
507
508
        $retval['item_sqldataaccess'] = $routine['SQL_DATA_ACCESS'];
509
        $retval['item_comment'] = $routine['ROUTINE_COMMENT'];
510
511
        return $retval;
512
    }
513
514
    /**
515
     * Gets the parameters of a routine from the parse tree.
516
     *
517
     * @param CreateStatement $statement the statement to be processed
518
     *
519
     * @return array<string, int|array<int, mixed[]|string|null>>
520
     */
521
    private function getParameters(CreateStatement $statement): array
522
    {
523
        $retval = [
524
            'item_num_params' => 0,
525
            'item_param_dir' => [],
526
            'item_param_name' => [],
527
            'item_param_type' => [],
528
            'item_param_length' => [],
529
            'item_param_length_arr' => [],
530
            'item_param_opts_num' => [],
531
        ];
532
533
        if ($statement->parameters !== null) {
534
            $idx = 0;
535
            foreach ($statement->parameters as $param) {
536
                $retval['item_param_dir'][$idx] = $param->inOut;
537
                $retval['item_param_name'][$idx] = $param->name;
538
                $retval['item_param_type'][$idx] = $param->type->name;
539
                $retval['item_param_length'][$idx] = implode(',', $param->type->parameters);
540
                $retval['item_param_length_arr'][$idx] = $param->type->parameters;
541
                $retval['item_param_opts_num'][$idx] = [];
542
                foreach ($param->type->options->options as $opt) {
543
                    $retval['item_param_opts_num'][$idx][] = is_string($opt) ?
544
                        $opt : $opt['value'];
545
                }
546
547
                $retval['item_param_opts_num'][$idx] = implode(' ', $retval['item_param_opts_num'][$idx]);
548
                ++$idx;
549
            }
550
551
            $retval['item_num_params'] = $idx;
552
        }
553
554
        return $retval;
555
    }
556
557
    /**
558
     * Creates one row for the parameter table used in the routine editor.
559
     *
560
     * @param mixed[] $routine Data for the routine returned by
561
     *                       getDataFromRequest() or getDataFromName()
562
     * @param mixed   $index   Either a numeric index of the row being processed
563
     *                         or NULL to create a template row for AJAX request
564
     * @param string  $class   Class used to hide the direction column, if the
565
     *                         row is for a stored function.
566
     *
567
     * @return mixed[]
568
     */
569
    public function getParameterRow(array $routine = [], mixed $index = null, string $class = ''): array
570
    {
571
        if ($index === null) {
572
            // template row for AJAX request
573
            $i = 0;
574
            $index = '%s';
575
            $dropClass = '';
576
            $routine = [
577
                'item_param_dir' => [''],
578
                'item_param_name' => [''],
579
                'item_param_type' => [''],
580
                'item_param_length' => [''],
581
                'item_param_opts_num' => [''],
582
                'item_param_opts_text' => [''],
583
            ];
584
        } elseif ($routine !== []) {
585
            // regular row for routine editor
586
            $dropClass = ' hide';
587
            $i = $index;
588
        } else {
589
            // No input data. This shouldn't happen,
590
            // but better be safe than sorry.
591
            return [];
592
        }
593
594
        $allCharsets = Charsets::getCharsets($this->dbi, Config::getInstance()->selectedServer['DisableIS']);
0 ignored issues
show
Deprecated Code introduced by
The function PhpMyAdmin\Config::getInstance() has been deprecated: Use dependency injection instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

594
        $allCharsets = Charsets::getCharsets($this->dbi, /** @scrutinizer ignore-deprecated */ Config::getInstance()->selectedServer['DisableIS']);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
595
        $charsets = [];
596
        foreach ($allCharsets as $charset) {
597
            $charsets[] = [
598
                'name' => $charset->getName(),
599
                'description' => $charset->getDescription(),
600
                'is_selected' => $charset->getName() === mb_strtolower($routine['item_param_opts_text'][$i]),
601
            ];
602
        }
603
604
        return [
605
            'class' => $class,
606
            'index' => $index,
607
            'param_directions' => $this->directions,
608
            'param_opts_num' => $this->numericOptions,
609
            'item_param_dir' => $routine['item_param_dir'][$i] ?? '',
610
            'item_param_name' => $routine['item_param_name'][$i] ?? '',
611
            'item_param_length' => $routine['item_param_length'][$i] ?? '',
612
            'item_param_opts_num' => $routine['item_param_opts_num'][$i] ?? '',
613
            'supported_datatypes' => Generator::getSupportedDatatypes(
614
                $this->dbi->types->mapAliasToMysqlType($routine['item_param_type'][$i]),
615
            ),
616
            'charsets' => $charsets,
617
            'drop_class' => $dropClass,
618
        ];
619
    }
620
621
    /**
622
     * Set the found errors and build the params
623
     *
624
     * @param string[] $itemParamName     The parameter names
625
     * @param string[] $itemParamDir      The direction parameter (see $this->directions)
626
     * @param mixed[]  $itemParamType     The parameter type
627
     * @param mixed[]  $itemParamLength   A length or not for the parameter
628
     * @param mixed[]  $itemParamOpsText  An optional charset for the parameter
629
     * @param mixed[]  $itemParamOpsNum   An optional parameter for a $itemParamType NUMBER
630
     * @param bool     $warnedAboutLength A boolean that will be switched if a the length warning is given
631
     */
632 16
    private function processParamsAndBuild(
633
        array $itemParamName,
634
        array $itemParamDir,
635
        array $itemParamType,
636
        array $itemParamLength,
637
        array $itemParamOpsText,
638
        array $itemParamOpsNum,
639
        RoutineType $itemType,
640
        bool &$warnedAboutLength,
641
    ): string {
642 16
        $params = '';
643 16
        $warnedAboutDir = false;
644
645 16
        for ($i = 0, $nb = count($itemParamName); $i < $nb; $i++) {
646 16
            if (empty($itemParamName[$i]) || empty($itemParamType[$i])) {
647 4
                $this->errors[] = __('You must provide a name and a type for each routine parameter.');
648 4
                break;
649
            }
650
651
            if (
652 12
                $itemType === RoutineType::Procedure
653 12
                && ! empty($itemParamDir[$i])
654 12
                && in_array($itemParamDir[$i], $this->directions, true)
655
            ) {
656 8
                $params .= $itemParamDir[$i] . ' '
657 8
                    . Util::backquote($itemParamName[$i])
658 8
                    . ' ' . $itemParamType[$i];
659 8
            } elseif ($itemType === RoutineType::Function) {
660 4
                $params .= Util::backquote($itemParamName[$i])
661 4
                    . ' ' . $itemParamType[$i];
662 4
            } elseif (! $warnedAboutDir) {
663 4
                $warnedAboutDir = true;
664 4
                $this->errors[] = sprintf(
665 4
                    __('Invalid direction "%s" given for parameter.'),
666 4
                    htmlspecialchars($itemParamDir[$i]),
667 4
                );
668
            }
669
670
            if (
671 12
                $itemParamLength[$i] != ''
672 12
                && preg_match(
673 12
                    '@^(DATE|TINYBLOB|TINYTEXT|BLOB|TEXT|MEDIUMBLOB|MEDIUMTEXT|LONGBLOB|LONGTEXT|SERIAL|BOOLEAN)$@i',
674 12
                    $itemParamType[$i],
675 12
                ) !== 1
676
            ) {
677 12
                $params .= '(' . $itemParamLength[$i] . ')';
678
            } elseif (
679 4
                $itemParamLength[$i] == ''
680 4
                && preg_match('@^(ENUM|SET|VARCHAR|VARBINARY)$@i', $itemParamType[$i]) === 1
681
            ) {
682 4
                if (! $warnedAboutLength) {
683 4
                    $warnedAboutLength = true;
684 4
                    $this->errors[] = __(
685 4
                        'You must provide length/values for routine parameters'
686 4
                        . ' of type ENUM, SET, VARCHAR and VARBINARY.',
687 4
                    );
688
                }
689
            }
690
691 12
            if (! empty($itemParamOpsText[$i])) {
692 12
                if ($this->dbi->types->getTypeClass($itemParamType[$i]) === TypeClass::Char) {
693 12
                    if (! in_array($itemParamType[$i], ['VARBINARY', 'BINARY'], true)) {
694 12
                        $params .= ' CHARSET '
695 12
                            . mb_strtolower($itemParamOpsText[$i]);
696
                    }
697
                }
698
            }
699
700 12
            if (! empty($itemParamOpsNum[$i])) {
701 8
                if ($this->dbi->types->getTypeClass($itemParamType[$i]) === TypeClass::Number) {
702 8
                    $params .= ' '
703 8
                        . mb_strtoupper($itemParamOpsNum[$i]);
704
                }
705
            }
706
707 12
            if ($i === count($itemParamName) - 1) {
708 12
                continue;
709
            }
710
711 8
            $params .= ', ';
712
        }
713
714 16
        return $params;
715
    }
716
717
    /**
718
     * Set the found errors and build the query
719
     *
720
     * @param string $query             The existing query
721
     * @param bool   $warnedAboutLength If the length warning was given
722
     */
723 16
    private function processFunctionSpecificParameters(
724
        string $query,
725
        bool $warnedAboutLength,
726
    ): string {
727 16
        $itemReturnType = $_POST['item_returntype'] ?? null;
728
729 16
        if ($itemReturnType !== '' && in_array($itemReturnType, Util::getSupportedDatatypes(), true)) {
730 12
            $query .= 'RETURNS ' . $itemReturnType;
731
        } else {
732 4
            $this->errors[] = __('You must provide a valid return type for the routine.');
733
        }
734
735
        if (
736 16
            ! empty($_POST['item_returnlength'])
737 16
            && preg_match(
738 16
                '@^(DATE|DATETIME|TIME|TINYBLOB|TINYTEXT|BLOB|TEXT|'
739 16
                . 'MEDIUMBLOB|MEDIUMTEXT|LONGBLOB|LONGTEXT|SERIAL|BOOLEAN)$@i',
740 16
                $itemReturnType,
741 16
            ) !== 1
742
        ) {
743 8
            $query .= '(' . $_POST['item_returnlength'] . ')';
744
        } elseif (
745 8
            empty($_POST['item_returnlength'])
746 8
            && preg_match('@^(ENUM|SET|VARCHAR|VARBINARY)$@i', $itemReturnType) === 1
747
        ) {
748 4
            if (! $warnedAboutLength) {
749 4
                $this->errors[] = __(
750 4
                    'You must provide length/values for routine parameters of type ENUM, SET, VARCHAR and VARBINARY.',
751 4
                );
752
            }
753
        }
754
755 16
        if (! empty($_POST['item_returnopts_text'])) {
756 8
            if ($this->dbi->types->getTypeClass($itemReturnType) === TypeClass::Char) {
757 8
                $query .= ' CHARSET '
758 8
                    . mb_strtolower($_POST['item_returnopts_text']);
759
            }
760
        }
761
762 16
        if (! empty($_POST['item_returnopts_num'])) {
763 4
            if ($this->dbi->types->getTypeClass($itemReturnType) === TypeClass::Number) {
764 4
                $query .= ' '
765 4
                    . mb_strtoupper($_POST['item_returnopts_num']);
766
            }
767
        }
768
769 16
        return $query . ' ';
770
    }
771
772
    /**
773
     * Composes the query necessary to create a routine from an HTTP request.
774
     *
775
     * @return string  The CREATE [ROUTINE | PROCEDURE] query.
776
     */
777 36
    public function getQueryFromRequest(ServerRequest $request): string
778
    {
779 36
        $itemType = RoutineType::tryFrom($request->getParsedBodyParamAsString('item_type', ''));
780 36
        $itemDefiner = $request->getParsedBodyParamAsString('item_definer', '');
781 36
        $itemName = $request->getParsedBodyParamAsString('item_name', '');
782
783 36
        $query = 'CREATE ';
784 36
        if ($itemDefiner !== '') {
785 12
            if (str_contains($itemDefiner, '@')) {
786 8
                $arr = explode('@', $itemDefiner);
787
788 8
                $doBackquote = true;
789 8
                if (str_starts_with($arr[0], '`') && str_ends_with($arr[0], '`')) {
790
                    $doBackquote = false;
791
                }
792
793 8
                $query .= 'DEFINER=' . Util::backquoteCompat($arr[0], 'NONE', $doBackquote);
794
795 8
                $doBackquote = true;
796 8
                if (str_starts_with($arr[1], '`') && str_ends_with($arr[1], '`')) {
797
                    $doBackquote = false;
798
                }
799
800 8
                $query .= '@' . Util::backquoteCompat($arr[1], 'NONE', $doBackquote) . ' ';
801
            } else {
802 4
                $this->errors[] = __('The definer must be in the "username@hostname" format!');
803
            }
804
        }
805
806 36
        if ($itemType !== null) {
807 32
            $query .= $itemType->value . ' ';
808
        } else {
809 4
            $this->errors[] = __('Invalid routine type!');
810
        }
811
812 36
        if ($itemName !== '') {
813 32
            $query .= Util::backquote($itemName);
814
        } else {
815 4
            $this->errors[] = __('You must provide a routine name!');
816
        }
817
818 36
        $warnedAboutLength = false;
819
820 36
        $itemParamName = $request->getParsedBodyParam('item_param_name', '');
821 36
        $itemParamType = $request->getParsedBodyParam('item_param_type', '');
822 36
        $itemParamLength = $request->getParsedBodyParam('item_param_length', '');
823 36
        $itemParamDir = (array) $request->getParsedBodyParam('item_param_dir', []);
824 36
        $itemParamOpsText = (array) $request->getParsedBodyParam('item_param_opts_text', []);
825 36
        $itemParamOpsNum = (array) $request->getParsedBodyParam('item_param_opts_num', []);
826
827 36
        $params = '';
828
        if (
829 36
            $itemParamName !== []
830 36
            && $itemParamType !== []
831 36
            && $itemParamLength !== []
832 36
            && is_array($itemParamName)
833 36
            && is_array($itemParamType)
834 36
            && is_array($itemParamLength)
835
        ) {
836 16
            $params = $this->processParamsAndBuild(
837 16
                $itemParamName,
838 16
                $itemParamDir,
839 16
                $itemParamType,
840 16
                $itemParamLength,
841 16
                $itemParamOpsText,
842 16
                $itemParamOpsNum,
843 16
                $itemType,
0 ignored issues
show
Bug introduced by
It seems like $itemType can also be of type null; however, parameter $itemType of PhpMyAdmin\Database\Rout...processParamsAndBuild() does only seem to accept PhpMyAdmin\Database\RoutineType, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

843
                /** @scrutinizer ignore-type */ $itemType,
Loading history...
844 16
                $warnedAboutLength, // Will possibly be modified by the function
845 16
            );
846
        }
847
848 36
        $query .= '(' . $params . ') ';
849 36
        if ($itemType === RoutineType::Function) {
850 16
            $query = $this->processFunctionSpecificParameters($query, $warnedAboutLength);
851
        }
852
853 36
        $itemComment = $request->getParsedBodyParamAsString('item_comment', '');
854 36
        if ($itemComment !== '') {
855 12
            $query .= 'COMMENT ' . $this->dbi->quoteString($itemComment) . ' ';
856
        }
857
858 36
        if ($request->hasBodyParam('item_isdeterministic')) {
859 12
            $query .= 'DETERMINISTIC ';
860
        } else {
861 24
            $query .= 'NOT DETERMINISTIC ';
862
        }
863
864 36
        $itemSqlDataAccess = $request->getParsedBodyParamAsString('item_sqldataaccess', '');
865 36
        if (in_array($itemSqlDataAccess, $this->sqlDataAccess, true)) {
866 8
            $query .= $itemSqlDataAccess . ' ';
867
        }
868
869 36
        $itemSecurityType = $request->getParsedBodyParamAsString('item_securitytype', '');
870 36
        if ($itemSecurityType === 'DEFINER' || $itemSecurityType === 'INVOKER') {
871 32
            $query .= 'SQL SECURITY ' . $itemSecurityType . ' ';
872
        }
873
874 36
        $itemDefinition = $request->getParsedBodyParamAsString('item_definition', '');
875 36
        if ($itemDefinition !== '') {
876 32
            $query .= $itemDefinition;
877
        } else {
878 4
            $this->errors[] = __('You must provide a routine definition.');
879
        }
880
881 36
        return $query;
882
    }
883
884
    /**
885
     * @param mixed[] $routine The routine params
886
     *
887
     * @return string[] The SQL queries / SQL query parts
888
     */
889
    private function getQueriesFromRoutineForm(array $routine): array
890
    {
891
        $queries = [];
892
        $outParams = [];
893
        $args = [];
894
        $allFunctions = $this->dbi->types->getAllFunctions();
895
        for ($i = 0; $i < $routine['item_num_params']; $i++) {
896
            if (isset($_POST['params'][$routine['item_param_name'][$i]])) {
897
                $value = $_POST['params'][$routine['item_param_name'][$i]];
898
                if (is_array($value)) { // is SET type
899
                    $value = implode(',', $value);
900
                }
901
902
                if (
903
                    ! empty($_POST['funcs'][$routine['item_param_name'][$i]])
904
                    && in_array($_POST['funcs'][$routine['item_param_name'][$i]], $allFunctions, true)
905
                ) {
906
                    $queries[] = sprintf(
907
                        'SET @p%d=%s(%s);',
908
                        $i,
909
                        $_POST['funcs'][$routine['item_param_name'][$i]],
910
                        $this->dbi->quoteString($value),
911
                    );
912
                } else {
913
                    $queries[] = 'SET @p' . $i . '=' . $this->dbi->quoteString($value) . ';';
914
                }
915
            }
916
917
            $args[] = '@p' . $i;
918
919
            if ($routine['item_type'] !== 'PROCEDURE') {
920
                continue;
921
            }
922
923
            if ($routine['item_param_dir'][$i] !== 'OUT' && $routine['item_param_dir'][$i] !== 'INOUT') {
924
                continue;
925
            }
926
927
            $outParams[] = '@p' . $i . ' AS ' . Util::backquote($routine['item_param_name'][$i]);
928
        }
929
930
        if ($routine['item_type'] === 'PROCEDURE') {
931
            $queries[] = sprintf(
932
                'CALL %s(%s);',
933
                Util::backquote($routine['item_name']),
934
                implode(', ', $args),
935
            );
936
            if ($outParams !== []) {
937
                $queries[] = 'SELECT ' . implode(', ', $outParams) . ';';
938
            }
939
        } else {
940
            $queries[] = sprintf(
941
                'SELECT %s(%s) AS %s;',
942
                Util::backquote($routine['item_name']),
943
                implode(', ', $args),
944
                Util::backquote($routine['item_name']),
945
            );
946
        }
947
948
        return $queries;
949
    }
950
951
    /**
952
     * @param mixed[] $routine
953
     *
954
     * @psalm-return array{string, Message}
955
     */
956
    public function handleExecuteRoutine(array $routine): array
957
    {
958
        $queries = $this->getQueriesFromRoutineForm($routine);
959
960
        $affected = 0;
961
        $resultHtmlTables = '';
962
        $nbResultsetToDisplay = 0;
963
964
        foreach ($queries as $query) {
965
            $result = $this->dbi->tryQuery($query);
966
967
            // Generate output
968
            while ($result !== false) {
969
                if ($result->numRows() > 0) {
970
                    $resultHtmlTables .= '<table class="table table-striped w-auto"><tr>';
971
                    foreach ($result->getFieldNames() as $field) {
972
                        $resultHtmlTables .= '<th>';
973
                        $resultHtmlTables .= htmlspecialchars($field);
974
                        $resultHtmlTables .= '</th>';
975
                    }
976
977
                    $resultHtmlTables .= '</tr>';
978
979
                    foreach ($result as $row) {
980
                        $resultHtmlTables .= '<tr>' . $this->browseRow($row) . '</tr>';
981
                    }
982
983
                    $resultHtmlTables .= '</table>';
984
                    $nbResultsetToDisplay++;
985
                    $affected = $result->numRows();
986
                }
987
988
                $result = $this->dbi->nextResult();
989
            }
990
991
            // We must check for an error after fetching the results because
992
            // either tryQuery might have produced an error or any of nextResult calls.
993
            if ($this->dbi->getError() !== '') {
994
                $message = Message::error(
995
                    sprintf(
996
                        __('The following query has failed: "%s"'),
997
                        htmlspecialchars($query),
998
                    )
999
                    . '<br><br>'
1000
                    . __('MySQL said: ') . $this->dbi->getError(),
1001
                );
1002
1003
                return ['', $message];
1004
            }
1005
        }
1006
1007
        // Pass the SQL queries through the "pretty printer"
1008
        $output = Generator::formatSql(implode("\n", $queries));
1009
        // Display results
1010
        $output .= '<div class="card my-3"><div class="card-header">';
1011
        $output .= sprintf(
1012
            __('Execution results of routine %s'),
1013
            htmlspecialchars(Util::backquote($routine['item_name'])),
1014
        );
1015
        $output .= '</div><div class="card-body">';
1016
        $output .= $resultHtmlTables;
1017
        $output .= '</div></div>';
1018
1019
        $message = __('Your SQL query has been executed successfully.');
1020
        if ($routine['item_type'] === 'PROCEDURE') {
1021
            $message .= '<br>';
1022
1023
            // TODO : message need to be modified according to the
1024
            // output from the routine
1025
            $message .= sprintf(
1026
                _ngettext(
1027
                    '%d row affected by the last statement inside the procedure.',
1028
                    '%d rows affected by the last statement inside the procedure.',
1029
                    (int) $affected,
1030
                ),
1031
                $affected,
1032
            );
1033
        }
1034
1035
        if ($nbResultsetToDisplay === 0) {
0 ignored issues
show
introduced by
The condition $nbResultsetToDisplay === 0 is always true.
Loading history...
1036
            $notice = __('MySQL returned an empty result set (i.e. zero rows).');
1037
            $output .= Message::notice($notice)->getDisplay();
1038
        }
1039
1040
        return [$output, Message::success($message)];
1041
    }
1042
1043
    /**
1044
     * Browse row array
1045
     *
1046
     * @param (string|null)[] $row Columns
1047
     */
1048
    private function browseRow(array $row): string
1049
    {
1050
        $output = '';
1051
        foreach ($row as $value) {
1052
            $value = $value === null ? '<i>NULL</i>' : htmlspecialchars($value);
1053
1054
            $output .= '<td>' . $value . '</td>';
1055
        }
1056
1057
        return $output;
1058
    }
1059
1060
    /**
1061
     * Creates the HTML code that shows the routine execution dialog.
1062
     *
1063
     * @param mixed[] $routine Data for the routine returned by getDataFromName()
1064
     *
1065
     * @psalm-return array{mixed[], mixed[]}
1066
     */
1067
    public function getExecuteForm(array $routine): array
1068
    {
1069
        // Escape special characters
1070
        $routine['item_name'] = htmlentities($routine['item_name'], ENT_QUOTES);
1071
        for ($i = 0; $i < $routine['item_num_params']; $i++) {
1072
            $routine['item_param_name'][$i] = htmlentities($routine['item_param_name'][$i], ENT_QUOTES);
1073
        }
1074
1075
        $params = [];
1076
1077
        for ($i = 0; $i < $routine['item_num_params']; $i++) {
1078
            if ($routine['item_type'] === 'PROCEDURE' && $routine['item_param_dir'][$i] === 'OUT') {
1079
                continue;
1080
            }
1081
1082
            if (Config::getInstance()->settings['ShowFunctionFields']) {
0 ignored issues
show
Deprecated Code introduced by
The function PhpMyAdmin\Config::getInstance() has been deprecated: Use dependency injection instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1082
            if (/** @scrutinizer ignore-deprecated */ Config::getInstance()->settings['ShowFunctionFields']) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1083
                if (
1084
                    stripos($routine['item_param_type'][$i], 'enum') !== false
1085
                    || stripos($routine['item_param_type'][$i], 'set') !== false
1086
                ) {
1087
                    $params[$i]['generator'] = null;
1088
                } else {
1089
                    $defaultFunction = Generator::getDefaultFunctionForField(
1090
                        mb_strtolower($routine['item_param_type'][$i]),
1091
                        false,
1092
                        '',
1093
                        '',
1094
                        false,
1095
                        '',
1096
                        '',
1097
                        false,
1098
                    );
1099
                    $params[$i]['generator'] = Generator::getFunctionsForField($defaultFunction);
1100
                }
1101
            }
1102
1103
            if ($routine['item_param_type'][$i] === 'DATETIME' || $routine['item_param_type'][$i] === 'TIMESTAMP') {
1104
                $params[$i]['class'] = 'datetimefield';
1105
            } elseif ($routine['item_param_type'][$i] === 'DATE') {
1106
                $params[$i]['class'] = 'datefield';
1107
            }
1108
1109
            if (in_array($routine['item_param_type'][$i], ['ENUM', 'SET'], true)) {
1110
                $params[$i]['input_type'] = $routine['item_param_type'][$i] === 'ENUM' ? 'radio' : 'checkbox';
1111
                foreach ($routine['item_param_length_arr'][$i] as $value) {
1112
                    $value = htmlentities(Util::unQuote($value), ENT_QUOTES);
1113
                    $params[$i]['htmlentities'][] = $value;
1114
                }
1115
            } else {
1116
                $params[$i]['input_type'] = 'text';
1117
            }
1118
        }
1119
1120
        return [$routine, $params];
1121
    }
1122
1123
    /**
1124
     * Creates the contents for a row in the list of routines
1125
     *
1126
     * @param string $rowClass Additional class
1127
     *
1128
     * @return mixed[]
1129
     */
1130 4
    public function getRow(Routine $routine, string $rowClass = ''): array
1131
    {
1132 4
        $sqlDrop = sprintf(
1133 4
            'DROP %s IF EXISTS %s',
1134 4
            $routine->type,
1135 4
            Util::backquote($routine->name),
1136 4
        );
1137
1138 4
        $currentUser = $this->dbi->getCurrentUser();
1139 4
        $currentUserIsRoutineDefiner = $currentUser === $routine->definer;
1140
1141
        // Since editing a procedure involved dropping and recreating, check also for
1142
        // CREATE ROUTINE privilege to avoid lost procedures.
1143 4
        $hasCreateRoutine = Util::currentUserHasPrivilege('CREATE ROUTINE', Current::$database);
1144 4
        $hasEditPrivilege = ($hasCreateRoutine && $currentUserIsRoutineDefiner)
1145 4
                            || $this->dbi->isSuperUser();
1146 4
        $hasExportPrivilege = ($hasCreateRoutine && $currentUserIsRoutineDefiner)
1147 4
                            || $this->dbi->isSuperUser();
1148 4
        $hasExecutePrivilege = Util::currentUserHasPrivilege('EXECUTE', Current::$database)
1149 4
                            || $currentUserIsRoutineDefiner;
1150
1151
        // There is a problem with Util::currentUserHasPrivilege():
1152
        // it does not detect all kinds of privileges, for example
1153
        // a direct privilege on a specific routine. So, at this point,
1154
        // we show the Execute link, hoping that the user has the correct rights.
1155
        // Also, information_schema might be hiding the ROUTINE_DEFINITION
1156
        // but a routine with no input parameters can be nonetheless executed.
1157
1158
        // Check if the routine has any input parameters. If it does,
1159
        // we will show a dialog to get values for these parameters,
1160
        // otherwise we can execute it directly.
1161
1162 4
        return [
1163 4
            'db' => Current::$database,
1164 4
            'table' => Current::$table,
1165 4
            'sql_drop' => $sqlDrop,
1166 4
            'routine' => $routine,
1167 4
            'row_class' => $rowClass,
1168 4
            'has_edit_privilege' => $hasEditPrivilege,
1169 4
            'has_export_privilege' => $hasExportPrivilege,
1170 4
            'has_execute_privilege' => $hasExecutePrivilege,
1171 4
        ];
1172
    }
1173
1174
    /**
1175
     * returns details about the PROCEDUREs or FUNCTIONs for a specific database
1176
     * or details about a specific routine
1177
     *
1178
     * @param string $name name of the routine (to fetch a specific routine)
1179
     *
1180
     * @return Routine[]
1181
     */
1182 8
    public static function getDetails(
1183
        DatabaseInterface $dbi,
1184
        string $db,
1185
        RoutineType|null $which = null,
1186
        string $name = '',
1187
        int $limit = 0,
1188
        int $offset = 0,
1189
    ): array {
1190 8
        $query = QueryGenerator::getInformationSchemaRoutinesRequest(
1191 8
            $dbi->quoteString($db),
1192 8
            $which,
1193 8
            $name === '' ? null : $dbi->quoteString($name),
1194 8
            $limit,
1195 8
            $offset,
1196 8
        );
1197 8
        $routines = $dbi->fetchResultSimple($query);
1198
1199 8
        $ret = [];
1200
        /** @var array{Name:string, Type:string, Definer:string, DTD_IDENTIFIER:string|null} $routine */
1201 8
        foreach ($routines as $routine) {
1202 4
            $ret[] = new Routine(
1203 4
                $routine['Name'],
1204 4
                $routine['Type'],
1205 4
                $routine['DTD_IDENTIFIER'] ?? '',
1206 4
                $routine['Definer'],
1207 4
            );
1208
        }
1209
1210 8
        return $ret;
1211
    }
1212
1213 8
    public static function getRoutineCount(DatabaseInterface $dbi, string $db, RoutineType|null $which = null): int
1214
    {
1215 8
        $query = QueryGenerator::getInformationSchemaRoutinesCountRequest(
1216 8
            $dbi->quoteString($db),
1217 8
            $which,
1218 8
        );
1219
1220 8
        return (int) $dbi->fetchValue($query);
1221
    }
1222
1223
    public static function getFunctionDefinition(DatabaseInterface $dbi, string $db, string $name): string|null
1224
    {
1225
        $result = $dbi->fetchValue(
1226
            'SHOW CREATE FUNCTION ' . Util::backquote($db) . '.' . Util::backquote($name),
1227
            'Create Function',
1228
        );
1229
1230
        return is_string($result) ? $result : null;
1231
    }
1232
1233
    public static function getProcedureDefinition(DatabaseInterface $dbi, string $db, string $name): string|null
1234
    {
1235
        $result = $dbi->fetchValue(
1236
            'SHOW CREATE PROCEDURE ' . Util::backquote($db) . '.' . Util::backquote($name),
1237
            'Create Procedure',
1238
        );
1239
1240
        return is_string($result) ? $result : null;
1241
    }
1242
1243
    /**
1244
     * @return array<int, string>
1245
     * @psalm-return list<non-empty-string>
1246
     */
1247 16
    public static function getNames(DatabaseInterface $dbi, string $db, RoutineType $type): array
1248
    {
1249
        /** @var list<non-empty-string> $names */
1250 16
        $names = $dbi->fetchSingleColumn(
1251 16
            'SELECT SPECIFIC_NAME FROM information_schema.ROUTINES'
1252 16
            . ' WHERE ROUTINE_SCHEMA = ' . $dbi->quoteString($db)
1253 16
            . " AND ROUTINE_TYPE = '" . $type->value . "' AND SPECIFIC_NAME != ''",
1254 16
        );
1255
1256 16
        return $names;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $names returns the type PhpMyAdmin\Database\list which is incompatible with the type-hinted return array.
Loading history...
1257
    }
1258
1259 44
    public function getErrorCount(): int
1260
    {
1261 44
        return count($this->errors);
1262
    }
1263
}
1264