Passed
Push — main ( 742ea5...16e90c )
by Thierry
09:19 queued 07:12
created

Admin::applySqlFunction()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 2
nc 8
nop 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Lagdo\DbAdmin\Db;
4
5
use Lagdo\DbAdmin\Driver\UtilInterface;
6
use Lagdo\DbAdmin\Driver\DriverInterface;
7
8
use Exception;
9
10
use function trim;
11
use function substr;
12
use function strlen;
13
use function implode;
14
use function is_numeric;
15
use function preg_match;
16
use function preg_replace;
17
use function microtime;
18
use function strtoupper;
19
use function uniqid;
20
21
class Admin
22
{
23
    /**
24
     * @var Util
25
     */
26
    public $util;
27
28
    /**
29
     * @var DriverInterface
30
     */
31
    public $driver;
32
33
    /**
34
     * @var Translator
35
     */
36
    protected $trans;
37
38
    /**
39
     * The constructor
40
     *
41
     * @param Util $util
42
     * @param DriverInterface $driver
43
     * @param Translator $trans
44
     */
45
    public function __construct(Util $util, DriverInterface $driver, Translator $trans)
46
    {
47
        $this->util = $util;
48
        $this->driver = $driver;
49
        $this->trans = $trans;
50
    }
51
52
    /**
53
     * Create SQL condition from parsed query string
54
     *
55
     * @param array $where Parsed query string
56
     * @param array $fields
57
     *
58
     * @return string
59
     */
60
    public function where(array $where, array $fields = [])
61
    {
62
        $clauses = [];
63
        $wheres = $where["where"] ?? [];
64
        foreach ((array) $wheres as $key => $value) {
65
            $key = $this->util->bracketEscape($key, 1); // 1 - back
66
            $column = $this->util->escapeKey($key);
67
            $clauses[] = $column .
68
                // LIKE because of floats but slow with ints
69
                ($this->driver->jush() == "sql" && is_numeric($value) && preg_match('~\.~', $value) ? " LIKE " .
70
                $this->driver->quote($value) : ($this->driver->jush() == "mssql" ? " LIKE " .
71
                $this->driver->quote(preg_replace('~[_%[]~', '[\0]', $value)) : " = " . // LIKE because of text
72
                $this->driver->unconvertField($fields[$key], $this->driver->quote($value)))); //! enum and set
73
            if ($this->driver->jush() == "sql" &&
74
                preg_match('~char|text~', $fields[$key]->type) && preg_match("~[^ -@]~", $value)) {
75
                // not just [a-z] to catch non-ASCII characters
76
                $clauses[] = "$column = " . $this->driver->quote($value) . " COLLATE " . $this->driver->charset() . "_bin";
77
            }
78
        }
79
        $nulls = $where["null"] ?? [];
80
        foreach ((array) $nulls as $key) {
81
            $clauses[] = $this->util->escapeKey($key) . " IS NULL";
82
        }
83
        return implode(" AND ", $clauses);
84
    }
85
86
    /**
87
     * Remove current user definer from SQL command
88
     *
89
     * @param string $query
90
     *
91
     * @return string
92
     */
93
    public function removeDefiner(string $query)
94
    {
95
        return preg_replace('~^([A-Z =]+) DEFINER=`' .
96
            preg_replace('~@(.*)~', '`@`(%|\1)', $this->driver->user()) .
97
            '`~', '\1', $query); //! proper escaping of user
98
    }
99
100
    /**
101
     * Get the users and hosts
102
     *
103
     * @param string $database  The database name
104
     *
105
     * @return array
106
     */
107
    public function getUsers($database)
108
    {
109
        // From privileges.inc.php
110
        $statement = $this->driver->query("SELECT User, Host FROM mysql." .
111
            ($database == "" ? "user" : "db WHERE " . $this->driver->quote($database) . " LIKE Db") .
112
            " ORDER BY Host, User");
113
        // $grant = $statement;
114
        if (!$statement) {
115
            // list logged user, information_schema.USER_PRIVILEGES lists just the current user too
116
            $statement = $this->driver->query("SELECT SUBSTRING_INDEX(CURRENT_USER, '@', 1) " .
117
                "AS User, SUBSTRING_INDEX(CURRENT_USER, '@', -1) AS Host");
118
        }
119
        $users = [];
120
        while ($row = $statement->fetchAssoc()) {
121
            $users[] = $row;
122
        }
123
        return $users;
124
    }
125
126
    /**
127
     * Get the grants of a user on a given host
128
     *
129
     * @param string $user      The user name
130
     * @param string $host      The host name
131
     * @param string $password  The user password
132
     *
133
     * @return array
134
     */
135
    public function getUserGrants(string $user, string $host, string &$password)
136
    {
137
        // From user.inc.php
138
        $grants = [];
139
140
        //! use information_schema for MySQL 5 - column names in column privileges are not escaped
141
        if (($statement = $this->driver->query("SHOW GRANTS FOR " .
142
            $this->driver->quote($user) . "@" . $this->driver->quote($host)))) {
143
            while ($row = $statement->fetchRow()) {
144
                if (\preg_match('~GRANT (.*) ON (.*) TO ~', $row[0], $match) &&
145
                    \preg_match_all('~ *([^(,]*[^ ,(])( *\([^)]+\))?~', $match[1], $matches, PREG_SET_ORDER)) { //! escape the part between ON and TO
146
                    foreach ($matches as $val) {
147
                        $match2 = $match[2] ?? '';
148
                        $val2 = $val[2] ?? '';
149
                        if ($val[1] != "USAGE") {
150
                            $grants["$match2$val2"][$val[1]] = true;
151
                        }
152
                        if (\preg_match('~ WITH GRANT OPTION~', $row[0])) { //! don't check inside strings and identifiers
153
                            $grants["$match2$val2"]["GRANT OPTION"] = true;
154
                        }
155
                    }
156
                }
157
                if (\preg_match("~ IDENTIFIED BY PASSWORD '([^']+)~", $row[0], $match)) {
158
                    $password  = $match[1];
159
                }
160
            }
161
        }
162
163
        return $grants;
164
    }
165
166
    /**
167
     * @param array $features
168
     * @param array $row
169
     *
170
     * @return void
171
     */
172
    private function makeFeatures(array &$features, array $row)
173
    {
174
        $contexts = \explode(',', $row['Context']);
175
        foreach ($contexts as $context) {
176
            // Don't take 'Grant option' privileges.
177
            if ($row['Privilege'] === 'Grant option') {
178
                continue;
179
            }
180
            // Privileges of 'Server Admin' and 'File access on server' are merged
181
            if ($context === 'File access on server') {
182
                $context = 'Server Admin';
183
            }
184
            $privilege = $row['Privilege'];
185
            // Comment for this is 'No privileges - allow connect only'
186
            if ($context === 'Server Admin' && $privilege === 'Usage') {
187
                continue;
188
            }
189
            // MySQL bug #30305
190
            if ($context === 'Procedures' && $privilege === 'Create routine') {
191
                $context = 'Databases';
192
            }
193
            if (!isset($features[$context])) {
194
                $features[$context] = [];
195
            }
196
            $features[$context][$privilege] = $row['Comment'];
197
            if ($context === 'Tables' &&
198
                in_array($privilege, ['Select', 'Insert', 'Update', 'References'])) {
199
                $features['Columns'][$privilege] = $row['Comment'];
200
            }
201
        }
202
    }
203
204
    /**
205
     * Get the user privileges
206
     *
207
     * @param array $grants     The user grants
208
     *
209
     * @return array
210
     */
211
    public function getUserPrivileges(array $grants)
212
    {
213
        // From user.inc.php
214
        $features = [
215
            '' => [
216
                'All privileges' => '',
217
            ],
218
            'Columns' => [],
219
        ];
220
        $rows = $this->driver->rows('SHOW PRIVILEGES');
221
        foreach ($rows as $row) {
222
            $this->makeFeatures($features, $row);
223
        }
224
225
        foreach (array_keys($features['Tables']) as $privilege) {
226
            unset($features['Databases'][$privilege]);
227
        }
228
229
        $privileges = [];
230
        $contexts = [
231
            "" => "",
232
            "Server Admin" => $this->trans->lang('Server'),
233
            "Databases" => $this->trans->lang('Database'),
234
            "Tables" => $this->trans->lang('Table'),
235
            "Columns" => $this->trans->lang('Column'),
236
            "Procedures" => $this->trans->lang('Routine'),
237
        ];
238
        foreach ($contexts as $context => $desc) {
239
            foreach ($features[$context] as $privilege => $comment) {
240
                $detail = [$desc, $this->util->html($privilege)];
241
                // echo "<tr><td" . ($desc ? ">$desc<td" : " colspan='2'") .
242
                //     ' lang="en" title="' . $this->util->html($comment) . '">' . $this->util->html($privilege);
243
                $i = 0;
244
                foreach ($grants as $object => $grant) {
245
                    $name = "'grants[$i][" . $this->util->html(\strtoupper($privilege)) . "]'";
246
                    $value = $grant[\strtoupper($privilege)] ?? false;
247
                    if ($context == "Server Admin" && $object != (isset($grants["*.*"]) ? "*.*" : ".*")) {
248
                        $detail[] = '';
249
                    }
250
                    // elseif(isset($values["grant"]))
251
                    // {
252
                    //     $detail[] = "<select name=$name><option><option value='1'" .
253
                    //         ($value ? " selected" : "") . ">" . $this->trans->lang('Grant') .
254
                    //         "<option value='0'" . ($value == "0" ? " selected" : "") . ">" .
255
                    //         $this->trans->lang('Revoke') . "</select>";
256
                    // }
257
                    else {
258
                        $detail[] = "<input type='checkbox' name=$name" . ($value ? " checked />" : " />");
259
                    }
260
                    $i++;
261
                }
262
                $privileges[] = $detail;
263
            }
264
        }
265
266
        return $privileges;
267
    }
268
269
    /**
270
     * Query printed after execution in the message
271
     *
272
     * @param string $query Executed query
273
     *
274
     * @return string
275
     */
276
    private function messageQuery(string $query/*, string $time*/)
277
    {
278
        if (strlen($query) > 1e6) {
279
            // [\x80-\xFF] - valid UTF-8, \n - can end by one-line comment
280
            $query = preg_replace('~[\x80-\xFF]+$~', '', substr($query, 0, 1e6)) . "\n…";
281
        }
282
        return $query;
283
    }
284
285
    /**
286
     * Execute query
287
     *
288
     * @param string $query
289
     * @param bool $execute
290
     * @param bool $failed
291
     *
292
     * @return bool
293
     */
294
    private function executeQuery(string $query, bool $execute = true, bool $failed = false/*, string $time = ''*/)
295
    {
296
        if ($execute) {
297
            // $start = microtime(true);
298
            $failed = !$this->driver->query($query);
299
            // $time = $this->trans->formatTime($start);
300
        }
301
        if ($failed) {
302
            $sql = '';
303
            if ($query) {
304
                $sql = $this->messageQuery($query/*, $time*/);
305
            }
306
            throw new Exception($this->driver->error() . $sql);
307
        }
308
        return true;
309
    }
310
311
    /**
312
     * Execute remembered queries
313
     *
314
     * @param bool $failed
315
     *
316
     * @return bool
317
     */
318
    private function executeSavedQuery(bool $failed)
319
    {
320
        list($queries/*, $time*/) = $this->driver->queries();
321
        return $this->executeQuery($queries, false, $failed/*, $time*/);
322
    }
323
324
    /**
325
     * Find out foreign keys for each column
326
     *
327
     * @param string $table
328
     *
329
     * @return array
330
     */
331
    public function columnForeignKeys(string $table)
332
    {
333
        $keys = [];
334
        foreach ($this->driver->foreignKeys($table) as $foreignKey) {
335
            foreach ($foreignKey->source as $val) {
336
                $keys[$val][] = $foreignKey;
337
            }
338
        }
339
        return $keys;
340
    }
341
342
    /**
343
     * Drop old object and create a new one
344
     *
345
     * @param string $drop          Drop old object query
346
     * @param string $create        Create new object query
347
     * @param string $dropCreated   Drop new object query
348
     * @param string $test          Create test object query
349
     * @param string $dropTest      Drop test object query
350
     * @param string $oldName
351
     * @param string $newName
352
     *
353
     * @return string
354
     */
355
    protected function dropAndCreate(string $drop, string $create, string $dropCreated,
356
        string $test, string $dropTest, string $oldName, string $newName)
357
    {
358
        if ($oldName == '' && $newName == '') {
359
            $this->executeQuery($drop);
360
            return 'dropped';
361
        }
362
        if ($oldName == '') {
363
            $this->executeQuery($create);
364
            return 'created';
365
        }
366
        if ($oldName != $newName) {
367
            $created = $this->driver->execute($create);
368
            $dropped = $this->driver->execute($drop);
369
            // $this->executeSavedQuery(!($created && $this->driver->execute($drop)));
370
            if (!$dropped && $created) {
371
                $this->driver->execute($dropCreated);
372
            }
373
            return 'altered';
374
        }
375
        $this->executeSavedQuery(!($this->driver->execute($test) &&
376
            $this->driver->execute($dropTest) &&
377
            $this->driver->execute($drop) && $this->driver->execute($create)));
378
        return 'altered';
379
    }
380
381
    /**
382
     * Drop old object and redirect
383
     *
384
     * @param string $drop          Drop old object query
385
     *
386
     * @return void
387
     */
388
    public function drop(string $drop)
389
    {
390
        $this->executeQuery($drop);
391
    }
392
393
    /**
394
     * Create a view
395
     *
396
     * @param array  $values    The view values
397
     *
398
     * @return bool
399
     */
400
    public function createView(array $values)
401
    {
402
        // From view.inc.php
403
        $name = trim($values['name']);
404
        $type = $values['materialized'] ? ' MATERIALIZED VIEW ' : ' VIEW ';
405
406
        $sql = ($this->driver->jush() === 'mssql' ? 'ALTER' : 'CREATE OR REPLACE') .
407
            $type . $this->driver->table($name) . " AS\n" . $values['select'];
408
        return $this->executeQuery($sql);
409
    }
410
411
    /**
412
     * Update a view
413
     *
414
     * @param string $view      The view name
415
     * @param array  $values    The view values
416
     *
417
     * @return string
418
     */
419
    public function updateView(string $view, array $values)
420
    {
421
        // From view.inc.php
422
        $origType = 'VIEW';
423
        if ($this->driver->jush() === 'pgsql') {
424
            $status = $this->driver->tableStatus($view);
425
            $origType = strtoupper($status->engine);
426
        }
427
428
        $name = trim($values['name']);
429
        $type = $values['materialized'] ? 'MATERIALIZED VIEW' : 'VIEW';
430
        $tempName = $name . '_adminer_' . uniqid();
431
432
        return $this->dropAndCreate("DROP $origType " . $this->driver->table($view),
433
            "CREATE $type " . $this->driver->table($name) . " AS\n" . $values['select'],
434
            "DROP $type " . $this->driver->table($name),
435
            "CREATE $type " . $this->driver->table($tempName) . " AS\n" . $values['select'],
436
            "DROP $type " . $this->driver->table($tempName), $view, $name);
437
    }
438
439
    /**
440
     * Drop a view
441
     *
442
     * @param string $view      The view name
443
     *
444
     * @return bool
445
     */
446
    public function dropView(string $view)
447
    {
448
        // From view.inc.php
449
        $origType = 'VIEW';
450
        if ($this->driver->jush() == 'pgsql') {
451
            $status = $this->driver->tableStatus($view);
452
            $origType = strtoupper($status->engine);
453
        }
454
455
        $sql = "DROP $origType " . $this->driver->table($view);
456
        return $this->executeQuery($sql);
457
    }
458
}
459