Passed
Push — master ( bfc93d...b9464b )
by Maurício
08:55
created

Triggers::getEditorForm()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 13
nc 1
nop 4
dl 0
loc 16
rs 9.8333
c 0
b 0
f 0
ccs 14
cts 14
cp 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\Database;
6
7
use PhpMyAdmin\DatabaseInterface;
8
use PhpMyAdmin\Html\Generator;
9
use PhpMyAdmin\Message;
10
use PhpMyAdmin\Response;
11
use PhpMyAdmin\Template;
12
use PhpMyAdmin\Util;
13
14
use function count;
15
use function explode;
16
use function htmlspecialchars;
17
use function in_array;
18
use function mb_strpos;
19
use function mb_strtoupper;
20
use function sprintf;
21
use function trim;
22
23
/**
24
 * Functions for trigger management.
25
 */
26
class Triggers
27
{
28
    /** @var array<int, string> */
29
    private $time = ['BEFORE', 'AFTER'];
30
31
    /** @var array<int, string> */
32
    private $event = ['INSERT', 'UPDATE', 'DELETE'];
33
34
    /** @var DatabaseInterface */
35
    private $dbi;
36
37
    /** @var Template */
38
    private $template;
39
40
    /** @var Response */
41
    private $response;
42
43
    /**
44
     * @param DatabaseInterface $dbi      DatabaseInterface instance.
45
     * @param Template          $template Template instance.
46
     * @param Response          $response Response instance.
47
     */
48 96
    public function __construct(DatabaseInterface $dbi, Template $template, $response)
49
    {
50 96
        $this->dbi = $dbi;
51 96
        $this->template = $template;
52 96
        $this->response = $response;
53 96
    }
54
55
    /**
56
     * Main function for the triggers functionality
57
     *
58
     * @return void
59
     */
60
    public function main()
61
    {
62
        global $db, $table;
63
64
        /**
65
         * Process all requests
66
         */
67
        $this->handleEditor();
68
        $this->export();
69
70
        $items = $this->dbi->getTriggers($db, $table);
71
        $hasTriggerPrivilege = Util::currentUserHasPrivilege('TRIGGER', $db, $table);
72
        $isAjax = $this->response->isAjax() && empty($_REQUEST['ajax_page_request']);
73
74
        $rows = '';
75
        foreach ($items as $item) {
76
            $rows .= $this->template->render('database/triggers/row', [
77
                'db' => $db,
78
                'table' => $table,
79
                'trigger' => $item,
80
                'has_drop_privilege' => $hasTriggerPrivilege,
81
                'has_edit_privilege' => $hasTriggerPrivilege,
82
                'row_class' => $isAjax ? 'ajaxInsert hide' : '',
83
            ]);
84
        }
85
86
        echo $this->template->render('database/triggers/list', [
87
            'db' => $db,
88
            'table' => $table,
89
            'items' => $items,
90
            'rows' => $rows,
91
            'has_privilege' => $hasTriggerPrivilege,
92
        ]);
93
    }
94
95
    /**
96
     * Handles editor requests for adding or editing an item
97
     *
98
     * @return void
99
     */
100
    public function handleEditor()
101
    {
102
        global $db, $errors, $message, $table;
103
104
        if (
105
            ! empty($_POST['editor_process_add'])
106
            || ! empty($_POST['editor_process_edit'])
107
        ) {
108
            $sql_query = '';
109
110
            $item_query = $this->getQueryFromRequest();
111
112
            // set by getQueryFromRequest()
113
            if (! count($errors)) {
114
                // Execute the created query
115
                if (! empty($_POST['editor_process_edit'])) {
116
                    // Backup the old trigger, in case something goes wrong
117
                    $trigger = $this->getDataFromName($_POST['item_original_name']);
118
                    $create_item = $trigger['create'];
119
                    $drop_item = $trigger['drop'] . ';';
120
                    $result = $this->dbi->tryQuery($drop_item);
121
                    if (! $result) {
122
                        $errors[] = sprintf(
123
                            __('The following query has failed: "%s"'),
124
                            htmlspecialchars($drop_item)
125
                        )
126
                        . '<br>'
127
                        . __('MySQL said: ') . $this->dbi->getError();
128
                    } else {
129
                        $result = $this->dbi->tryQuery($item_query);
130
                        if (! $result) {
131
                            $errors[] = sprintf(
132
                                __('The following query has failed: "%s"'),
133
                                htmlspecialchars($item_query)
134
                            )
135
                            . '<br>'
136
                            . __('MySQL said: ') . $this->dbi->getError();
137
                            // We dropped the old item, but were unable to create the
138
                            // new one. Try to restore the backup query.
139
                            $result = $this->dbi->tryQuery($create_item);
140
141
                            $errors = $this->checkResult($result, $create_item, $errors);
142
                        } else {
143
                            $message = Message::success(
144
                                __('Trigger %1$s has been modified.')
145
                            );
146
                            $message->addParam(
147
                                Util::backquote($_POST['item_name'])
148
                            );
149
                            $sql_query = $drop_item . $item_query;
150
                        }
151
                    }
152
                } else {
153
                    // 'Add a new item' mode
154
                    $result = $this->dbi->tryQuery($item_query);
155
                    if (! $result) {
156
                        $errors[] = sprintf(
157
                            __('The following query has failed: "%s"'),
158
                            htmlspecialchars($item_query)
159
                        )
160
                        . '<br><br>'
161
                        . __('MySQL said: ') . $this->dbi->getError();
162
                    } else {
163
                        $message = Message::success(
164
                            __('Trigger %1$s has been created.')
165
                        );
166
                        $message->addParam(
167
                            Util::backquote($_POST['item_name'])
168
                        );
169
                        $sql_query = $item_query;
170
                    }
171
                }
172
            }
173
174
            if (count($errors)) {
175
                $message = Message::error(
176
                    '<b>'
177
                    . __(
178
                        'One or more errors have occurred while processing your request:'
179
                    )
180
                    . '</b>'
181
                );
182
                $message->addHtml('<ul>');
183
                foreach ($errors as $string) {
184
                    $message->addHtml('<li>' . $string . '</li>');
185
                }
186
187
                $message->addHtml('</ul>');
188
            }
189
190
            $output = Generator::getMessage($message, $sql_query);
191
192
            if ($this->response->isAjax()) {
193
                if ($message->isSuccess()) {
194
                    $items = $this->dbi->getTriggers($db, $table, '');
195
                    $trigger = false;
196
                    foreach ($items as $value) {
197
                        if ($value['name'] != $_POST['item_name']) {
198
                            continue;
199
                        }
200
201
                        $trigger = $value;
202
                    }
203
204
                    $insert = false;
205
                    if (
206
                        empty($table)
207
                        || ($trigger !== false && $table == $trigger['table'])
208
                    ) {
209
                        $insert = true;
210
                        $hasTriggerPrivilege = Util::currentUserHasPrivilege('TRIGGER', $db, $table);
211
                        $this->response->addJSON(
212
                            'new_row',
213
                            $this->template->render('database/triggers/row', [
214
                                'db' => $db,
215
                                'table' => $table,
216
                                'trigger' => $trigger,
217
                                'has_drop_privilege' => $hasTriggerPrivilege,
218
                                'has_edit_privilege' => $hasTriggerPrivilege,
219
                                'row_class' => '',
220
                            ])
221
                        );
222
                        $this->response->addJSON(
223
                            'name',
224
                            htmlspecialchars(
225
                                mb_strtoupper(
226
                                    $_POST['item_name']
227
                                )
228
                            )
229
                        );
230
                    }
231
232
                    $this->response->addJSON('insert', $insert);
233
                    $this->response->addJSON('message', $output);
234
                } else {
235
                    $this->response->addJSON('message', $message);
236
                    $this->response->setRequestStatus(false);
237
                }
238
239
                $this->response->addJSON('tableType', 'triggers');
240
                exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
241
            }
242
        }
243
244
        /**
245
         * Display a form used to add/edit a trigger, if necessary
246
         */
247
        if (
248
            ! count($errors)
249
            && (! empty($_POST['editor_process_add'])
250
            || ! empty($_POST['editor_process_edit'])
251
            || (empty($_REQUEST['add_item'])
252
            && empty($_REQUEST['edit_item']))) // FIXME: this must be simpler than that
253
        ) {
254
            return;
255
        }
256
257
        $mode = '';
258
        $item = null;
259
        $title = '';
260
        // Get the data for the form (if any)
261
        if (! empty($_REQUEST['add_item'])) {
262
            $title = __('Add trigger');
263
            $item = $this->getDataFromRequest();
264
            $mode = 'add';
265
        } elseif (! empty($_REQUEST['edit_item'])) {
266
            $title = __('Edit trigger');
267
            if (
268
                ! empty($_REQUEST['item_name'])
269
                && empty($_POST['editor_process_edit'])
270
            ) {
271
                $item = $this->getDataFromName($_REQUEST['item_name']);
272
                if ($item !== null) {
273
                    $item['item_original_name'] = $item['item_name'];
274
                }
275
            } else {
276
                $item = $this->getDataFromRequest();
277
            }
278
279
            $mode = 'edit';
280
        }
281
282
        $this->sendEditor($mode, $item, $title, $db);
283
    }
284
285
    /**
286
     * This function will generate the values that are required to for the editor
287
     *
288
     * @return array    Data necessary to create the editor.
289
     */
290 8
    public function getDataFromRequest()
291
    {
292 8
        $retval = [];
293 2
        $indices = [
294 6
            'item_name',
295
            'item_table',
296
            'item_original_name',
297
            'item_action_timing',
298
            'item_event_manipulation',
299
            'item_definition',
300
            'item_definer',
301
        ];
302 8
        foreach ($indices as $index) {
303 8
            $retval[$index] = $_POST[$index] ?? '';
304
        }
305
306 8
        return $retval;
307
    }
308
309
    /**
310
     * This function will generate the values that are required to complete
311
     * the "Edit trigger" form given the name of a trigger.
312
     *
313
     * @param string $name The name of the trigger.
314
     *
315
     * @return array|null Data necessary to create the editor.
316
     */
317
    public function getDataFromName($name): ?array
318
    {
319
        global $db, $table;
320
321
        $temp = [];
322
        $items = $this->dbi->getTriggers($db, $table, '');
323
        foreach ($items as $value) {
324
            if ($value['name'] != $name) {
325
                continue;
326
            }
327
328
            $temp = $value;
329
        }
330
331
        if (empty($temp)) {
332
            return null;
333
        }
334
335
        $retval = [];
336
        $retval['create']                  = $temp['create'];
337
        $retval['drop']                    = $temp['drop'];
338
        $retval['item_name']               = $temp['name'];
339
        $retval['item_table']              = $temp['table'];
340
        $retval['item_action_timing']      = $temp['action_timing'];
341
        $retval['item_event_manipulation'] = $temp['event_manipulation'];
342
        $retval['item_definition']         = $temp['definition'];
343
        $retval['item_definer']            = $temp['definer'];
344
345
        return $retval;
346
    }
347
348
    /**
349
     * Displays a form used to add/edit a trigger
350
     *
351
     * @param string $db
352
     * @param string $table
353
     * @param string $mode  If the editor will be used to edit a trigger or add a new one: 'edit' or 'add'.
354
     * @param array  $item  Data for the trigger returned by getDataFromRequest() or getDataFromName()
355
     */
356 72
    public function getEditorForm($db, $table, $mode, array $item): string
357
    {
358 72
        $query = 'SELECT `TABLE_NAME` FROM `INFORMATION_SCHEMA`.`TABLES` ';
359 72
        $query .= 'WHERE `TABLE_SCHEMA`=\'' . $this->dbi->escapeString($db) . '\' ';
360 72
        $query .= 'AND `TABLE_TYPE` IN (\'BASE TABLE\', \'SYSTEM VERSIONED\')';
361 72
        $tables = $this->dbi->fetchResult($query);
362
363 72
        return $this->template->render('database/triggers/editor_form', [
364 72
            'db' => $db,
365 72
            'table' => $table,
366 72
            'is_edit' => $mode === 'edit',
367 72
            'item' => $item,
368 72
            'tables' => $tables,
369 72
            'time' => $this->time,
370 72
            'events' => $this->event,
371 72
            'is_ajax' => $this->response->isAjax(),
372
        ]);
373
    }
374
375
    /**
376
     * Composes the query necessary to create a trigger from an HTTP request.
377
     *
378
     * @return string  The CREATE TRIGGER query.
379
     */
380 16
    public function getQueryFromRequest()
381
    {
382 16
        global $db, $errors;
383
384 16
        $query = 'CREATE ';
385 16
        if (! empty($_POST['item_definer'])) {
386
            if (
387 12
                mb_strpos($_POST['item_definer'], '@') !== false
388
            ) {
389 8
                $arr = explode('@', $_POST['item_definer']);
390 8
                $query .= 'DEFINER=' . Util::backquote($arr[0]);
391 8
                $query .= '@' . Util::backquote($arr[1]) . ' ';
392
            } else {
393 4
                $errors[] = __('The definer must be in the "username@hostname" format!');
394
            }
395
        }
396
397 16
        $query .= 'TRIGGER ';
398 16
        if (! empty($_POST['item_name'])) {
399 12
            $query .= Util::backquote($_POST['item_name']) . ' ';
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquote($_POST['item_name']) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

399
            $query .= /** @scrutinizer ignore-type */ Util::backquote($_POST['item_name']) . ' ';
Loading history...
400
        } else {
401 4
            $errors[] = __('You must provide a trigger name!');
402
        }
403
404
        if (
405 16
            ! empty($_POST['item_timing'])
406 16
            && in_array($_POST['item_timing'], $this->time)
407
        ) {
408 12
            $query .= $_POST['item_timing'] . ' ';
409
        } else {
410 4
            $errors[] = __('You must provide a valid timing for the trigger!');
411
        }
412
413
        if (
414 16
            ! empty($_POST['item_event'])
415 16
            && in_array($_POST['item_event'], $this->event)
416
        ) {
417 8
            $query .= $_POST['item_event'] . ' ';
418
        } else {
419 8
            $errors[] = __('You must provide a valid event for the trigger!');
420
        }
421
422 16
        $query .= 'ON ';
423
        if (
424 16
            ! empty($_POST['item_table'])
425 16
            && in_array($_POST['item_table'], $this->dbi->getTables($db))
426
        ) {
427 4
            $query .= Util::backquote($_POST['item_table']);
428
        } else {
429 12
            $errors[] = __('You must provide a valid table name!');
430
        }
431
432 16
        $query .= ' FOR EACH ROW ';
433 16
        if (! empty($_POST['item_definition'])) {
434 12
            $query .= $_POST['item_definition'];
435
        } else {
436 4
            $errors[] = __('You must provide a trigger definition.');
437
        }
438
439 16
        return $query;
440
    }
441
442
    /**
443
     * @param resource|bool $result          Query result
444
     * @param string        $createStatement Query
445
     * @param array         $errors          Errors
446
     *
447
     * @return array
448
     */
449
    private function checkResult($result, $createStatement, array $errors)
450
    {
451
        if ($result) {
452
            return $errors;
453
        }
454
455
        // OMG, this is really bad! We dropped the query,
456
        // failed to create a new one
457
        // and now even the backup query does not execute!
458
        // This should not happen, but we better handle
459
        // this just in case.
460
        $errors[] = __('Sorry, we failed to restore the dropped trigger.') . '<br>'
461
            . __('The backed up query was:')
462
            . '"' . htmlspecialchars($createStatement) . '"<br>'
463
            . __('MySQL said: ') . $this->dbi->getError();
464
465
        return $errors;
466
    }
467
468
    /**
469
     * Send editor via ajax or by echoing.
470
     *
471
     * @param string     $mode  Editor mode 'add' or 'edit'
472
     * @param array|null $item  Data necessary to create the editor
473
     * @param string     $title Title of the editor
474
     * @param string     $db    Database
475
     *
476
     * @return void
477
     */
478
    private function sendEditor($mode, ?array $item, $title, $db)
0 ignored issues
show
Unused Code introduced by
The parameter $db is not used and could be removed. ( Ignorable by Annotation )

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

478
    private function sendEditor($mode, ?array $item, $title, /** @scrutinizer ignore-unused */ $db)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
479
    {
480
        global $db, $table;
481
482
        if ($item !== null) {
0 ignored issues
show
introduced by
The condition $item !== null is always true.
Loading history...
483
            $editor = $this->getEditorForm($db, $table, $mode, $item);
484
            if ($this->response->isAjax()) {
485
                $this->response->addJSON('message', $editor);
486
                $this->response->addJSON('title', $title);
487
            } else {
488
                echo "\n\n<h2>" . $title . "</h2>\n\n" . $editor;
489
                unset($_POST);
490
            }
491
492
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
493
        }
494
495
        $message  = __('Error in processing request:') . ' ';
496
        $message .= sprintf(
497
            __('No trigger with name %1$s found in database %2$s.'),
498
            htmlspecialchars(Util::backquote($_REQUEST['item_name'])),
499
            htmlspecialchars(Util::backquote($db))
500
        );
501
        $message = Message::error($message);
502
        if ($this->response->isAjax()) {
503
            $this->response->setRequestStatus(false);
504
            $this->response->addJSON('message', $message);
505
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
506
        }
507
508
        echo $message->getDisplay();
509
    }
510
511
    private function export(): void
512
    {
513
        global $db, $table;
514
515
        if (empty($_GET['export_item']) || empty($_GET['item_name'])) {
516
            return;
517
        }
518
519
        $itemName = $_GET['item_name'];
520
        $triggers = $this->dbi->getTriggers($db, $table, '');
521
        $exportData = false;
522
523
        foreach ($triggers as $trigger) {
524
            if ($trigger['name'] === $itemName) {
525
                $exportData = $trigger['create'];
526
                break;
527
            }
528
        }
529
530
        if ($exportData !== false) {
531
            $title = sprintf(__('Export of trigger %s'), htmlspecialchars(Util::backquote($itemName)));
0 ignored issues
show
Bug introduced by
It seems like PhpMyAdmin\Util::backquote($itemName) can also be of type array; however, parameter $string of htmlspecialchars() does only seem to accept string, 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

531
            $title = sprintf(__('Export of trigger %s'), htmlspecialchars(/** @scrutinizer ignore-type */ Util::backquote($itemName)));
Loading history...
532
533
            if ($this->response->isAjax()) {
534
                $this->response->addJSON('message', htmlspecialchars(trim($exportData)));
535
                $this->response->addJSON('title', $title);
536
537
                exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
538
            }
539
540
            $this->response->addHTML($this->template->render('database/triggers/export', [
541
                'data' => $exportData,
542
                'item_name' => $itemName,
543
            ]));
544
545
            return;
546
        }
547
548
        $message = sprintf(
549
            __('Error in processing request: No trigger with name %1$s found in database %2$s.'),
550
            htmlspecialchars(Util::backquote($itemName)),
551
            htmlspecialchars(Util::backquote($db))
552
        );
553
        $message = Message::error($message);
554
555
        if ($this->response->isAjax()) {
556
            $this->response->setRequestStatus(false);
557
            $this->response->addJSON('message', $message);
558
559
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
560
        }
561
562
        $this->response->addHTML($message->getDisplay());
563
    }
564
}
565