Postgres::getLanguages()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 21
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 8
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 21
rs 10
1
<?php
2
3
/**
4
 * PHPPgAdmin 6.1.3
5
 */
6
7
namespace PHPPgAdmin\Database;
8
9
/**
10
 * A Class that implements the DB Interface for Postgres
11
 * Note: This Class uses ADODB and returns RecordSets.
12
 *
13
 * Id: Postgres.php,v 1.320 2008/02/20 20:43:09 ioguix Exp $
14
 */
15
class Postgres extends ADOdbBase
16
{
17
    use \PHPPgAdmin\Traits\HelperTrait;
18
    use \PHPPgAdmin\Database\Traits\AggregateTrait;
19
    use \PHPPgAdmin\Database\Traits\DatabaseTrait;
20
    use \PHPPgAdmin\Database\Traits\DomainTrait;
21
    use \PHPPgAdmin\Database\Traits\FtsTrait;
22
    use \PHPPgAdmin\Database\Traits\FunctionTrait;
23
    use \PHPPgAdmin\Database\Traits\IndexTrait;
24
    use \PHPPgAdmin\Database\Traits\OperatorTrait;
25
    use \PHPPgAdmin\Database\Traits\RoleTrait;
26
    use \PHPPgAdmin\Database\Traits\SchemaTrait;
27
    use \PHPPgAdmin\Database\Traits\SequenceTrait;
28
    use \PHPPgAdmin\Database\Traits\TablespaceTrait;
29
    use \PHPPgAdmin\Database\Traits\TableTrait;
30
    use \PHPPgAdmin\Database\Traits\TypeTrait;
31
    use \PHPPgAdmin\Database\Traits\ViewTrait;
32
    use \PHPPgAdmin\Database\Traits\StatsTrait;
33
    use \PHPPgAdmin\Database\Traits\PrivilegesTrait;
34
35
    public $lang;
36
37
    public $conf;
38
39
    /**
40
     * @var float
41
     */
42
    public $major_version = 9.6;
43
44
    /**
45
     * @var class-string
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
46
     */
47
    public $help_classname = \PHPPgAdmin\Help\PostgresDoc::class;
48
49
    /**
50
     * @var \PHPPgAdmin\Help\PostgresDoc
51
     */
52
    public $help_class;
53
54
    protected $container;
55
56
    protected $server_info;
57
58
    public function __construct(&$conn, $container, $server_info)
59
    {
60
        //$this->prtrace('major_version :' . $this->major_version);
61
        $this->conn = $conn;
62
        $this->container = $container;
63
64
        $this->lang = $container->get('lang');
65
        $this->conf = $container->get('conf');
66
        $this->server_info = $server_info;
67
        $this->help_class = new $this->help_classname($this->conf, $this->major_version);
68
        $this->lastExecutedSql = '';
69
    }
70
71
    /**
72
     * Fetch a URL (or array of URLs) for a given help page.
73
     *
74
     * @param string $help
75
     *
76
     * @return null|array|string the help page or pages related to the $help topic, or null if none exists
77
     */
78
    public function getHelp($help)
79
    {
80
        $this->help_page = $this->help_class->getHelpTopics();
81
        $this->help_base = $this->help_class->getHelpBase();
82
83
        if (!$topicResult = $this->help_class->getHelpTopic($help)) {
84
            return null;
85
        }
86
87
        if (\is_array($topicResult)) {
88
            $urls = [];
89
90
            foreach ($topicResult as $link) {
91
                $urls[] = $this->help_base . $link;
92
            }
93
94
            return $urls;
95
        }
96
97
        return $this->help_base . $topicResult;
98
    }
99
100
    /**
101
     * Gets the help pages.
102
     * get help page by instancing the corresponding help class
103
     * if $this->help_page and $this->help_base are set, this function is a noop.
104
     *
105
     * @return array<array-key, string>|null|string
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key, string>|null|string at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key, string>|null|string.
Loading history...
106
     */
107
    public function getHelpPages()
108
    {
109
        return $this->help_class->getHelpTopics();
110
    }
111
112
    // Formatting functions
113
114
    /**
115
     * Outputs the HTML code for a particular field.
116
     *
117
     * @param string $name   The name to give the field
118
     * @param mixed  $value  The value of the field.  Note this could be 'numeric(7,2)' sort of thing...
119
     * @param string $type   The database type of the field
120
     * @param array  $extras An array of attributes name as key and attributes' values as value
121
     */
122
    public function printField($name, $value, $type, $extras = []): void
123
    {
124
        $lang = $this->lang;
125
126
        // Determine actions string
127
        $extra_str = '';
128
129
        foreach ($extras as $k => $v) {
130
            $extra_str .= " {$k}=\"" . \htmlspecialchars($v) . '"';
131
        }
132
133
        switch (\mb_substr($type, 0, 9)) {
134
            case 'bool':
135
            case 'boolean':
136
                if (null !== $value && '' === $value) {
137
                    $value = null;
138
                } elseif ('true' === $value) {
139
                    $value = 't';
140
                } elseif ('false' === $value) {
141
                    $value = 'f';
142
                }
143
144
                // If value is null, 't' or 'f'...
145
                if (null === $value || 't' === $value || 'f' === $value) {
146
                    echo '<select name="', \htmlspecialchars($name), "\"{$extra_str}>\n";
147
                    echo '<option value=""', (null === $value) ? ' selected="selected"' : '', "></option>\n";
148
                    echo '<option value="t"', ('t' === $value) ? ' selected="selected"' : '', ">{$lang['strtrue']}</option>\n";
149
                    echo '<option value="f"', ('f' === $value) ? ' selected="selected"' : '', ">{$lang['strfalse']}</option>\n";
150
                    echo "</select>\n";
151
                } else {
152
                    echo '<input name="', \htmlspecialchars($name), '" value="', \htmlspecialchars($value), "\" size=\"35\"{$extra_str} />\n";
153
                }
154
155
                break;
156
            case 'bytea':
157
            case 'bytea[]':
158
                if (null !== $value) {
159
                    $value = $this->escapeBytea($value);
160
                }
161
                // no break
162
            case 'text':
163
            case 'text[]':
164
            case 'json':
165
            case 'jsonb':
166
            case 'xml':
167
            case 'xml[]':
168
                $n = \mb_substr_count($value, "\n");
169
                $n = 5 > $n ? \max(2, $n) : $n;
170
                $n = 20 < $n ? 20 : $n;
171
                echo '<textarea name="', \htmlspecialchars($name), "\" rows=\"{$n}\" cols=\"85\"{$extra_str}>\n";
172
                echo \htmlspecialchars($value);
173
                echo "</textarea>\n";
174
175
                break;
176
            case 'character':
177
            case 'character[]':
178
                $n = \mb_substr_count($value, "\n");
179
                $n = 5 > $n ? 5 : $n;
180
                $n = 20 < $n ? 20 : $n;
181
                echo '<textarea name="', \htmlspecialchars($name), "\" rows=\"{$n}\" cols=\"35\"{$extra_str}>\n";
182
                echo \htmlspecialchars($value);
183
                echo "</textarea>\n";
184
185
                break;
186
187
            default:
188
                echo '<input name="', \htmlspecialchars($name), '" value="', \htmlspecialchars($value), "\" size=\"35\"{$extra_str} />\n";
189
190
                break;
191
        }
192
    }
193
194
    /**
195
     * Searches all system catalogs to find objects that match a certain name.
196
     *
197
     * @param string $term   The search term
198
     * @param string $filter The object type to restrict to ('' means no restriction)
199
     *
200
     * @return int|\PHPPgAdmin\ADORecordSet A recordset
201
     */
202
    public function findObject($term, $filter)
203
    {
204
        $conf = $this->conf;
205
206
        /*about escaping:
207
         * SET standard_conforming_string is not available before 8.2
208
         * So we must use PostgreSQL specific notation :/
209
         * E'' notation is not available before 8.1
210
         * $$ is available since 8.0
211
         * Nothing specific from 7.4
212
         */
213
214
        // Escape search term for ILIKE match
215
        $this->clean($term);
216
        $this->clean($filter);
217
        $term = \str_replace('_', '\_', $term);
218
        $term = \str_replace('%', '\%', $term);
219
220
        // Exclude system relations if necessary
221
        if (!$conf['show_system']) {
222
            // XXX: The mention of information_schema here is in the wrong place, but
223
            // it's the quickest fix to exclude the info schema from 7.4
224
            $where = " AND pn.nspname NOT LIKE \$_PATERN_\$pg\\_%\$_PATERN_\$ AND pn.nspname != 'information_schema'";
225
            $lan_where = 'AND pl.lanispl';
226
        } else {
227
            $where = '';
228
            $lan_where = '';
229
        }
230
231
        // Apply outer filter
232
        $sql = '';
233
234
        if ('' !== $filter) {
235
            $sql = 'SELECT * FROM (';
236
        }
237
238
        $term = "\$_PATERN_\$%{$term}%\$_PATERN_\$";
239
240
        $sql .= "
241
			SELECT 'SCHEMA' AS type, oid, NULL AS schemaname, NULL AS relname, nspname AS name
242
				FROM pg_catalog.pg_namespace pn WHERE nspname ILIKE {$term} {$where}
243
			UNION ALL
244
			SELECT CASE WHEN relkind='r' THEN 'TABLE' WHEN relkind='v' THEN 'VIEW' WHEN relkind='S' THEN 'SEQUENCE' END, pc.oid,
245
				pn.nspname, NULL, pc.relname FROM pg_catalog.pg_class pc, pg_catalog.pg_namespace pn
246
				WHERE pc.relnamespace=pn.oid AND relkind IN ('r', 'v', 'S') AND relname ILIKE {$term} {$where}
247
			UNION ALL
248
			SELECT CASE WHEN pc.relkind='r' THEN 'COLUMNTABLE' ELSE 'COLUMNVIEW' END, NULL, pn.nspname, pc.relname, pa.attname FROM pg_catalog.pg_class pc, pg_catalog.pg_namespace pn,
249
				pg_catalog.pg_attribute pa WHERE pc.relnamespace=pn.oid AND pc.oid=pa.attrelid
250
				AND pa.attname ILIKE {$term} AND pa.attnum > 0 AND NOT pa.attisdropped AND pc.relkind IN ('r', 'v') {$where}
251
			UNION ALL
252
			SELECT 'FUNCTION', pp.oid, pn.nspname, NULL, pp.proname || '(' || pg_catalog.oidvectortypes(pp.proargtypes) || ')' FROM pg_catalog.pg_proc pp, pg_catalog.pg_namespace pn
253
				WHERE pp.pronamespace=pn.oid AND NOT pp.proisagg AND pp.proname ILIKE {$term} {$where}
254
			UNION ALL
255
			SELECT 'INDEX', NULL, pn.nspname, pc.relname, pc2.relname FROM pg_catalog.pg_class pc, pg_catalog.pg_namespace pn,
256
				pg_catalog.pg_index pi, pg_catalog.pg_class pc2 WHERE pc.relnamespace=pn.oid AND pc.oid=pi.indrelid
257
				AND pi.indexrelid=pc2.oid
258
				AND NOT EXISTS (
259
					SELECT 1 FROM pg_catalog.pg_depend d JOIN pg_catalog.pg_constraint c
260
					ON (d.refclassid = c.tableoid AND d.refobjid = c.oid)
261
					WHERE d.classid = pc2.tableoid AND d.objid = pc2.oid AND d.deptype = 'i' AND c.contype IN ('u', 'p')
262
				)
263
				AND pc2.relname ILIKE {$term} {$where}
264
			UNION ALL
265
			SELECT 'CONSTRAINTTABLE', NULL, pn.nspname, pc.relname, pc2.conname FROM pg_catalog.pg_class pc, pg_catalog.pg_namespace pn,
266
				pg_catalog.pg_constraint pc2 WHERE pc.relnamespace=pn.oid AND pc.oid=pc2.conrelid AND pc2.conrelid != 0
267
				AND CASE WHEN pc2.contype IN ('f', 'c') THEN TRUE ELSE NOT EXISTS (
268
					SELECT 1 FROM pg_catalog.pg_depend d JOIN pg_catalog.pg_constraint c
269
					ON (d.refclassid = c.tableoid AND d.refobjid = c.oid)
270
					WHERE d.classid = pc2.tableoid AND d.objid = pc2.oid AND d.deptype = 'i' AND c.contype IN ('u', 'p')
271
				) END
272
				AND pc2.conname ILIKE {$term} {$where}
273
			UNION ALL
274
			SELECT 'CONSTRAINTDOMAIN', pt.oid, pn.nspname, pt.typname, pc.conname FROM pg_catalog.pg_type pt, pg_catalog.pg_namespace pn,
275
				pg_catalog.pg_constraint pc WHERE pt.typnamespace=pn.oid AND pt.oid=pc.contypid AND pc.contypid != 0
276
				AND pc.conname ILIKE {$term} {$where}
277
			UNION ALL
278
			SELECT 'TRIGGER', NULL, pn.nspname, pc.relname, pt.tgname FROM pg_catalog.pg_class pc, pg_catalog.pg_namespace pn,
279
				pg_catalog.pg_trigger pt WHERE pc.relnamespace=pn.oid AND pc.oid=pt.tgrelid
280
					AND ( pt.tgconstraint = 0 OR NOT EXISTS
281
					(SELECT 1 FROM pg_catalog.pg_depend d JOIN pg_catalog.pg_constraint c
282
					ON (d.refclassid = c.tableoid AND d.refobjid = c.oid)
283
					WHERE d.classid = pt.tableoid AND d.objid = pt.oid AND d.deptype = 'i' AND c.contype = 'f'))
284
				AND pt.tgname ILIKE {$term} {$where}
285
			UNION ALL
286
			SELECT 'RULETABLE', NULL, pn.nspname AS schemaname, c.relname AS tablename, r.rulename FROM pg_catalog.pg_rewrite r
287
				JOIN pg_catalog.pg_class c ON c.oid = r.ev_class
288
				LEFT JOIN pg_catalog.pg_namespace pn ON pn.oid = c.relnamespace
289
				WHERE c.relkind='r' AND r.rulename != '_RETURN' AND r.rulename ILIKE {$term} {$where}
290
			UNION ALL
291
			SELECT 'RULEVIEW', NULL, pn.nspname AS schemaname, c.relname AS tablename, r.rulename FROM pg_catalog.pg_rewrite r
292
				JOIN pg_catalog.pg_class c ON c.oid = r.ev_class
293
				LEFT JOIN pg_catalog.pg_namespace pn ON pn.oid = c.relnamespace
294
				WHERE c.relkind='v' AND r.rulename != '_RETURN' AND r.rulename ILIKE {$term} {$where}
295
		";
296
297
        // Add advanced objects if show_advanced is set
298
        if ($conf['show_advanced']) {
299
            $sql .= "
300
				UNION ALL
301
				SELECT CASE WHEN pt.typtype='d' THEN 'DOMAIN' ELSE 'TYPE' END, pt.oid, pn.nspname, NULL,
302
					pt.typname FROM pg_catalog.pg_type pt, pg_catalog.pg_namespace pn
303
					WHERE pt.typnamespace=pn.oid AND typname ILIKE {$term}
304
					AND (pt.typrelid = 0 OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = pt.typrelid))
305
					{$where}
306
			 	UNION ALL
307
				SELECT 'OPERATOR', po.oid, pn.nspname, NULL, po.oprname FROM pg_catalog.pg_operator po, pg_catalog.pg_namespace pn
308
					WHERE po.oprnamespace=pn.oid AND oprname ILIKE {$term} {$where}
309
				UNION ALL
310
				SELECT 'CONVERSION', pc.oid, pn.nspname, NULL, pc.conname FROM pg_catalog.pg_conversion pc,
311
					pg_catalog.pg_namespace pn WHERE pc.connamespace=pn.oid AND conname ILIKE {$term} {$where}
312
				UNION ALL
313
				SELECT 'LANGUAGE', pl.oid, NULL, NULL, pl.lanname FROM pg_catalog.pg_language pl
314
					WHERE lanname ILIKE {$term} {$lan_where}
315
				UNION ALL
316
				SELECT DISTINCT ON (p.proname) 'AGGREGATE', p.oid, pn.nspname, NULL, p.proname FROM pg_catalog.pg_proc p
317
					LEFT JOIN pg_catalog.pg_namespace pn ON p.pronamespace=pn.oid
318
					WHERE p.proisagg AND p.proname ILIKE {$term} {$where}
319
				UNION ALL
320
				SELECT DISTINCT ON (po.opcname) 'OPCLASS', po.oid, pn.nspname, NULL, po.opcname FROM pg_catalog.pg_opclass po,
321
					pg_catalog.pg_namespace pn WHERE po.opcnamespace=pn.oid
322
					AND po.opcname ILIKE {$term} {$where}
323
			";
324
        } else {
325
            // Otherwise just add domains
326
            $sql .= "
327
				UNION ALL
328
				SELECT 'DOMAIN', pt.oid, pn.nspname, NULL,
329
					pt.typname FROM pg_catalog.pg_type pt, pg_catalog.pg_namespace pn
330
					WHERE pt.typnamespace=pn.oid AND pt.typtype='d' AND typname ILIKE {$term}
331
					AND (pt.typrelid = 0 OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = pt.typrelid))
332
					{$where}
333
			";
334
        }
335
336
        if ('' !== $filter) {
337
            // We use like to make RULE, CONSTRAINT and COLUMN searches work
338
            $sql .= ") AS sub WHERE type LIKE '{$filter}%' ";
339
        }
340
341
        $sql .= 'ORDER BY type, schemaname, relname, name';
342
343
        return $this->selectSet($sql);
344
    }
345
346
    /**
347
     * Gets all languages.
348
     *
349
     * @param bool $all True to get all languages, regardless of show_system
350
     *
351
     * @return int|\PHPPgAdmin\ADORecordSet A recordset
352
     */
353
    public function getLanguages($all = false)
354
    {
355
        $conf = $this->conf;
356
357
        if ($conf['show_system'] || $all) {
358
            $where = '';
359
        } else {
360
            $where = 'WHERE lanispl';
361
        }
362
363
        $sql = "
364
			SELECT
365
				lanname, lanpltrusted,
366
				lanplcallfoid::pg_catalog.regproc AS lanplcallf
367
			FROM
368
				pg_catalog.pg_language
369
			{$where}
370
			ORDER BY lanname
371
		";
372
373
        return $this->selectSet($sql);
374
    }
375
376
    /**
377
     * Executes an SQL script as a series of SQL statements.  Returns
378
     * the result of the final step.  This is a very complicated lexer
379
     * based on the REL7_4_STABLE src/bin/psql/mainloop.c lexer in
380
     * the PostgreSQL source code.
381
     * XXX: It does not handle multibyte languages properly.
382
     *
383
     * @param string        $name     Entry in $_FILES to use
384
     * @param null|callable $callback (optional) Callback function to call with each query, its result and line number
385
     *
386
     * @return bool|mixed true for general success, false on any failure, or resultset
387
     */
388
    public function executeScript($name, $callback = null)
389
    {
390
        // This whole function isn't very encapsulated, but hey...
391
        $conn = $this->conn->_connectionID;
392
393
        if (!\is_uploaded_file($_FILES[$name]['tmp_name'])) {
394
            return false;
395
        }
396
397
        $fd = \fopen($_FILES[$name]['tmp_name'], 'rb');
398
399
        if (false === $fd) {
400
            return false;
401
        }
402
403
        // Build up each SQL statement, they can be multiline
404
        $query_buf = null;
405
        $query_start = 0;
406
        $in_quote = 0;
407
        $in_xcomment = 0;
408
        $bslash_count = 0;
409
        $dol_quote = '';
410
        $paren_level = 0;
411
        $len = 0;
412
        $i = 0;
413
        $prevlen = 0;
414
        $thislen = 0;
415
        $lineno = 0;
416
417
        // Loop over each line in the file
418
        while (!\feof($fd)) {
419
            $line = \fgets($fd);
420
            ++$lineno;
421
422
            // Nothing left on line? Then ignore...
423
            if ('' === \trim($line)) {
424
                continue;
425
            }
426
427
            $len = \mb_strlen($line);
428
            $query_start = 0;
429
430
            /**
431
             * Parse line, looking for command separators.
432
             *
433
             * The current character is at line[i], the prior character at line[i
434
             * - prevlen], the next character at line[i + thislen].
435
             */
436
            $prevlen = 0;
437
            $thislen = (0 < $len) ? 1 : 0;
438
439
            for ($i = 0; $i < $len; $this->advance_1($i, $prevlen, $thislen)) {
440
                /* was the previous character a backslash? */
441
                if (0 < $i && '\\' === \mb_substr($line, $i - $prevlen, 1)) {
442
                    $this->prtrace('bslash_count', $bslash_count, $line);
443
                    ++$bslash_count;
444
                } else {
445
                    $bslash_count = 0;
446
                }
447
448
                /*
449
                 * It is important to place the in_* test routines before the
450
                 * in_* detection routines. i.e. we have to test if we are in
451
                 * a quote before testing for comments.
452
                 */
453
454
                /* in quote? */
455
                if (0 !== $in_quote) {
456
                    //$this->prtrace('in_quote', $in_quote, $line);
457
                    /*
458
                     * end of quote if matching non-backslashed character.
459
                     * backslashes don't count for double quotes, though.
460
                     */
461
                    if (\mb_substr($line, $i, 1) === $in_quote &&
462
                        (0 === $bslash_count % 2 || '"' === $in_quote)
463
                    ) {
464
                        $in_quote = 0;
465
                    }
466
                } elseif ($dol_quote) {
467
                    $this->prtrace('dol_quote', $dol_quote, $line);
468
469
                    if (0 === \strncmp(\mb_substr($line, $i), $dol_quote, \mb_strlen($dol_quote))) {
470
                        $this->advance_1($i, $prevlen, $thislen);
471
472
                        while ('$' !== \mb_substr($line, $i, 1)) {
473
                            $this->advance_1($i, $prevlen, $thislen);
474
                        }
475
476
                        $dol_quote = '';
477
                    }
478
                } elseif ('/*' === \mb_substr($line, $i, 2)) {
479
                    $this->prtrace('open_xcomment', $in_xcomment, $line, $i, $prevlen, $thislen);
480
481
                    if (0 === $in_xcomment) {
482
                        ++$in_xcomment;
483
                        $finishpos = \mb_strpos(\mb_substr($line, $i, $len), '*/');
484
485
                        if (false === $finishpos) {
486
                            $line = \mb_substr($line, 0, $i); /* remove comment */
487
488
                            break;
489
                        }
490
                        $pre = \mb_substr($line, 0, $i);
491
                        $post = \mb_substr($line, $i + 2 + $finishpos, $len);
492
                        $line = $pre . ' ' . $post;
493
                        $in_xcomment = 0;
494
                        $i = 0;
495
                    }
496
                } elseif ($in_xcomment) {
497
                    $position = \mb_strpos(\mb_substr($line, $i, $len), '*/');
498
499
                    if (false === $position) {
500
                        $line = '';
501
502
                        break;
503
                    }
504
505
                    $substr = \mb_substr($line, $i, 2);
506
507
                    if ('*/' === $substr && !--$in_xcomment) {
508
                        $line = \mb_substr($line, $i + 2, $len);
509
                        $i += 2;
510
                        $this->advance_1($i, $prevlen, $thislen);
511
                    }
512
                    // old logic
513
                    //  } else if (substr($line, $i, 2) == '/*') {
514
                    //      if ($in_xcomment == 0) {
515
                    //          ++$in_xcomment;
516
                    //          $this->advance_1($i, $prevlen, $thislen);
517
                    //      }
518
                    //  } else if ($in_xcomment) {
519
                    //      $substr = substr($line, $i, 2);
520
                    //      if ($substr == '*/' && !--$in_xcomment) {
521
                    //          $this->advance_1($i, $prevlen, $thislen);
522
                    //      }
523
                } elseif ('\'' === \mb_substr($line, $i, 1) || '"' === \mb_substr($line, $i, 1)) {
524
                    $in_quote = \mb_substr($line, $i, 1);
525
                } elseif (!$dol_quote && $this->valid_dolquote(\mb_substr($line, $i))) {
526
                    $dol_end = \mb_strpos(\mb_substr($line, $i + 1), '$');
527
                    $dol_quote = \mb_substr($line, $i, $dol_end + 1);
528
                    $this->advance_1($i, $prevlen, $thislen);
529
530
                    while ('$' !== \mb_substr($line, $i, 1)) {
531
                        $this->advance_1($i, $prevlen, $thislen);
532
                    }
533
                } else {
534
                    if ('--' === \mb_substr($line, $i, 2)) {
535
                        $line = \mb_substr($line, 0, $i); /* remove comment */
536
537
                        break;
538
                    } /* count nested parentheses */
539
540
                    if ('(' === \mb_substr($line, $i, 1)) {
541
                        ++$paren_level;
542
                    } elseif (')' === \mb_substr($line, $i, 1) && 0 < $paren_level) {
543
                        --$paren_level;
544
                    } elseif (';' === \mb_substr($line, $i, 1) && !$bslash_count && !$paren_level) {
545
                        $subline = \mb_substr(\mb_substr($line, 0, $i), $query_start);
546
                        /*
547
                         * insert a cosmetic newline, if this is not the first
548
                         * line in the buffer
549
                         */
550
                        if (0 < \mb_strlen($query_buf)) {
551
                            $query_buf .= "\n";
552
                        }
553
554
                        /* append the line to the query buffer */
555
                        $query_buf .= $subline;
556
                        /* is there anything in the query_buf? */
557
                        if (\trim($query_buf)) {
558
                            $query_buf .= ';';
559
560
                            // Execute the query. PHP cannot execute
561
                            // empty queries, unlike libpq
562
                            $res = \pg_query($conn, $query_buf);
0 ignored issues
show
Bug introduced by
$conn of type boolean is incompatible with the type resource expected by parameter $connection of pg_query(). ( Ignorable by Annotation )

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

562
                            $res = \pg_query(/** @scrutinizer ignore-type */ $conn, $query_buf);
Loading history...
563
564
                            // Call the callback function for display
565
                            if (null !== $callback) {
566
                                $callback($query_buf, $res, $lineno);
567
                            }
568
569
                            // Check for COPY request
570
                            if (4 === \pg_result_status($res)) {
571
                                // 4 == PGSQL_COPY_FROM
572
                                while (!\feof($fd)) {
573
                                    $copy = \fgets($fd, 32768);
574
                                    ++$lineno;
575
                                    \pg_put_line($conn, $copy);
0 ignored issues
show
Bug introduced by
$conn of type boolean is incompatible with the type resource expected by parameter $connection of pg_put_line(). ( Ignorable by Annotation )

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

575
                                    \pg_put_line(/** @scrutinizer ignore-type */ $conn, $copy);
Loading history...
576
577
                                    if ("\\.\n" === $copy || "\\.\r\n" === $copy) {
578
                                        \pg_end_copy($conn);
0 ignored issues
show
Bug introduced by
$conn of type boolean is incompatible with the type resource expected by parameter $connection of pg_end_copy(). ( Ignorable by Annotation )

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

578
                                        \pg_end_copy(/** @scrutinizer ignore-type */ $conn);
Loading history...
579
580
                                        break;
581
                                    }
582
                                }
583
                            }
584
                        }
585
                        $query_buf = null;
586
                        $query_start = $i + $thislen;
587
                    } elseif (\preg_match('/^[_[:alpha:]]$/', \mb_substr($line, $i, 1))) {
588
                        $sub = \mb_substr($line, $i, $thislen);
589
590
                        while (\preg_match('/^[\$_A-Za-z0-9]$/', $sub)) {
591
                            /* keep going while we still have identifier chars */
592
                            $this->advance_1($i, $prevlen, $thislen);
593
                            $sub = \mb_substr($line, $i, $thislen);
594
                        }
595
                        // Since we're now over the next character to be examined, it is necessary
596
                        // to move back one space.
597
                        $i -= $prevlen;
598
                    }
599
                }
600
            } // end for
601
602
            /* Put the rest of the line in the query buffer. */
603
            $subline = \mb_substr($line, $query_start);
604
605
            if ($in_quote || $dol_quote || \strspn($subline, " \t\n\r") !== \mb_strlen($subline)) {
606
                if (0 < \mb_strlen($query_buf)) {
607
                    $query_buf .= "\n";
608
                }
609
610
                $query_buf .= $subline;
611
            }
612
613
            $line = null;
614
        } // end while
615
616
        $res = true;
617
        /*
618
         * Process query at the end of file without a semicolon, so long as
619
         * it's non-empty.
620
         */
621
        if (0 < \mb_strlen($query_buf) && \strspn($query_buf, " \t\n\r") !== \mb_strlen($query_buf)) {
622
            // Execute the query
623
            $res = \pg_query($conn, $query_buf);
624
625
            // Call the callback function for display
626
            if (null !== $callback) {
627
                $callback($query_buf, $res, $lineno);
628
            }
629
630
            // Check for COPY request
631
            if (4 === \pg_result_status($res)) {
632
                // 4 == PGSQL_COPY_FROM
633
                while (!\feof($fd)) {
634
                    $copy = \fgets($fd, 32768);
635
                    ++$lineno;
636
                    \pg_put_line($conn, $copy);
637
638
                    if ("\\.\n" === $copy || "\\.\r\n" === $copy) {
639
                        \pg_end_copy($conn);
640
641
                        break;
642
                    }
643
                }
644
            }
645
        }
646
647
        \fclose($fd);
648
649
        return $res;
650
    }
651
652
    // Capabilities
653
654
    /**
655
     * Returns a recordset of all columns in a query.  Supports paging.
656
     *
657
     * @param string   $type      Either 'QUERY' if it is an SQL query, or 'TABLE' if it is a table identifier,
658
     *                            or 'SELECT" if it's a select query
659
     * @param string   $table     The base table of the query.  NULL for no table.
660
     * @param string   $query     The query that is being executed.  NULL for no query.
661
     * @param string   $sortkey   The column number to sort by, or '' or null for no sorting
662
     * @param string   $sortdir   The direction in which to sort the specified column ('asc' or 'desc')
663
     * @param null|int $page      The page of the relation to retrieve
664
     * @param null|int $page_size The number of rows per page
665
     * @param int      $max_pages (return-by-ref) The max number of pages in the relation
666
     *
667
     * @return int|\PHPPgAdmin\ADORecordSet A  recordset on success or an int with error code
668
     *                                      - -1 transaction error
669
     *                                      - -2 counting error
670
     *                                      - -3 page or page_size invalid
671
     *                                      - -4 unknown type
672
     *                                      - -5 failed setting transaction read only
673
     */
674
    public function browseQuery($type, $table, $query, $sortkey, $sortdir, $page, $page_size, &$max_pages)
675
    {
676
        // Check that we're not going to divide by zero
677
        if (!\is_numeric($page_size) || (int) $page_size !== $page_size || 0 >= $page_size) {
678
            return -3;
679
        }
680
681
        // If $type is TABLE, then generate the query
682
        switch ($type) {
683
            case 'TABLE':
684
                if (\preg_match('/^[0-9]+$/', $sortkey) && 0 < $sortkey) {
685
                    $orderby = [$sortkey => $sortdir];
686
                } else {
687
                    $orderby = [];
688
                }
689
690
                $query = $this->getSelectSQL($table, [], [], [], $orderby);
691
692
                break;
693
            case 'QUERY':
694
            case 'SELECT':
695
                // Trim query
696
                $query = \trim($query);
697
                // Trim off trailing semi-colon if there is one
698
                if (';' === \mb_substr($query, \mb_strlen($query) - 1, 1)) {
699
                    $query = \mb_substr($query, 0, \mb_strlen($query) - 1);
700
                }
701
702
                break;
703
704
            default:
705
                return -4;
706
        }
707
708
        // Generate count query
709
        $count = "SELECT COUNT(*) AS total FROM ({$query}) AS sub";
710
711
        // Open a transaction
712
        $status = $this->beginTransaction();
713
714
        if (0 !== $status) {
715
            return -1;
716
        }
717
718
        // If backend supports read only queries, then specify read only mode
719
        // to avoid side effects from repeating queries that do writes.
720
        if ($this->hasReadOnlyQueries()) {
721
            $status = $this->execute('SET TRANSACTION READ ONLY');
722
723
            if (0 !== $status) {
724
                $this->rollbackTransaction();
725
726
                return -5;
727
            }
728
        }
729
730
        // Count the number of rows
731
        $total = $this->browseQueryCount($count);
732
733
        if (0 > $total) {
734
            $this->rollbackTransaction();
735
736
            return -2;
737
        }
738
739
        // Calculate max pages
740
        $max_pages = \ceil($total / $page_size);
741
742
        // Check that page is less than or equal to max pages
743
        if (!\is_numeric($page) || (int) $page !== $page || $page > $max_pages || 1 > $page) {
744
            $this->rollbackTransaction();
745
746
            return -3;
747
        }
748
749
        // Set fetch mode to NUM so that duplicate field names are properly returned
750
        // for non-table queries.  Since the SELECT feature only allows selecting one
751
        // table, duplicate fields shouldn't appear.
752
        if ('QUERY' === $type) {
753
            $this->conn->setFetchMode(ADODB_FETCH_NUM);
0 ignored issues
show
Bug introduced by
PHPPgAdmin\Database\ADODB_FETCH_NUM of type integer is incompatible with the type The expected by parameter $mode of ADOConnection::SetFetchMode(). ( Ignorable by Annotation )

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

753
            $this->conn->setFetchMode(/** @scrutinizer ignore-type */ ADODB_FETCH_NUM);
Loading history...
754
        }
755
756
        // Figure out ORDER BY.  Sort key is always the column number (based from one)
757
        // of the column to order by.  Only need to do this for non-TABLE queries
758
        if ('TABLE' !== $type && \preg_match('/^[0-9]+$/', $sortkey) && 0 < $sortkey) {
759
            $orderby = " ORDER BY {$sortkey}";
760
            // Add sort order
761
            if ('desc' === $sortdir) {
762
                $orderby .= ' DESC';
763
            } else {
764
                $orderby .= ' ASC';
765
            }
766
        } else {
767
            $orderby = '';
768
        }
769
770
        // Actually retrieve the rows, with offset and limit
771
        $rs = $this->selectSet("SELECT * FROM ({$query}) AS sub {$orderby} LIMIT {$page_size} OFFSET " . ($page - 1) * $page_size);
772
        $status = $this->endTransaction();
773
774
        if (0 !== $status) {
775
            $this->rollbackTransaction();
776
777
            return -1;
778
        }
779
780
        return $rs;
781
    }
782
783
    /**
784
     * Generates the SQL for the 'select' function.
785
     *
786
     * @param string $table   The table from which to select
787
     * @param array  $show    An array of columns to show.  Empty array means all columns.
788
     * @param array  $values  An array mapping columns to values
789
     * @param array  $ops     An array of the operators to use
790
     * @param array  $orderby (optional) An array of column numbers or names (one based)
791
     *                        mapped to sort direction (asc or desc or '' or null) to order by
792
     *
793
     * @return string The SQL query
794
     */
795
    public function getSelectSQL($table, $show, $values = [], $ops = [], $orderby = [])
796
    {
797
        $this->fieldArrayClean($show);
798
799
        // If an empty array is passed in, then show all columns
800
        if (0 === \count($show)) {
801
            if ($this->hasObjectID($table)) {
802
                $sql = "SELECT \"{$this->id}\", * FROM ";
803
            } else {
804
                $sql = 'SELECT * FROM ';
805
            }
806
        } else {
807
            // Add oid column automatically to results for editing purposes
808
            if (!\in_array($this->id, $show, true) && $this->hasObjectID($table)) {
809
                $sql = "SELECT \"{$this->id}\", \"";
810
            } else {
811
                $sql = 'SELECT "';
812
            }
813
814
            $sql .= \implode('","', $show) . '" FROM ';
815
        }
816
        $this->prtrace(['id' => $this->id]);
817
        $this->fieldClean($table);
818
819
        if (isset($_REQUEST['schema'])) {
820
            $f_schema = $_REQUEST['schema'];
821
            $this->fieldClean($f_schema);
822
            $sql .= "\"{$f_schema}\".";
823
        }
824
        $sql .= "\"{$table}\"";
825
826
        // If we have values specified, add them to the WHERE clause
827
        $first = true;
828
829
        if (\is_array($values) && 0 < \count($values)) {
830
            foreach ($values as $k => $v) {
831
                if ('' !== $v || 'p' === $this->selectOps[$ops[$k]]) {
832
                    $this->fieldClean($k);
833
834
                    if ($first) {
835
                        $sql .= ' WHERE ';
836
                        $first = false;
837
                    } else {
838
                        $sql .= ' AND ';
839
                    }
840
                    // Different query format depending on operator type
841
                    switch ($this->selectOps[$ops[$k]]) {
842
                        case 'i':
843
                            // Only clean the field for the inline case
844
                            // this is because (x), subqueries need to
845
                            // to allow 'a','b' as input.
846
                            $this->clean($v);
847
                            $sql .= "\"{$k}\" {$ops[$k]} '{$v}'";
848
849
                            break;
850
                        case 'p':
851
                            $sql .= "\"{$k}\" {$ops[$k]}";
852
853
                            break;
854
                        case 'x':
855
                            $sql .= "\"{$k}\" {$ops[$k]} ({$v})";
856
857
                            break;
858
                        case 't':
859
                            $sql .= "\"{$k}\" {$ops[$k]}('{$v}')";
860
861
                            break;
862
863
                        default:
864
                            // Shouldn't happen
865
                    }
866
                }
867
            }
868
        }
869
870
        // ORDER BY
871
        if (\is_array($orderby) && 0 < \count($orderby)) {
872
            $sql .= ' ORDER BY ';
873
            $first = true;
874
875
            foreach ($orderby as $k => $v) {
876
                if ($first) {
877
                    $first = false;
878
                } else {
879
                    $sql .= ', ';
880
                }
881
882
                if (\preg_match('/^[0-9]+$/', $k)) {
883
                    $sql .= $k;
884
                } else {
885
                    $this->fieldClean($k);
886
                    $sql .= '"' . $k . '"';
887
                }
888
889
                if ('DESC' === \mb_strtoupper($v)) {
890
                    $sql .= ' DESC';
891
                }
892
            }
893
        }
894
895
        return $sql;
896
    }
897
898
    /**
899
     * Finds the number of rows that would be returned by a
900
     * query.
901
     *
902
     * @param string $count The count query
903
     *
904
     * @return int|string The count of rows or -1 of no rows are found
905
     */
906
    public function browseQueryCount($count)
907
    {
908
        return $this->selectField($count, 'total');
909
    }
910
911
    /**
912
     * A private helper method for executeScript that advances the
913
     * character by 1.  In psql this is careful to take into account
914
     * multibyte languages, but we don't at the moment, so this function
915
     * is someone redundant, since it will always advance by 1.
916
     *
917
     * @param int $i       The current character position in the line
918
     * @param int $prevlen Length of previous character (ie. 1)
919
     * @param int $thislen Length of current character (ie. 1)
920
     */
921
    protected function advance_1(&$i, &$prevlen, &$thislen): void
922
    {
923
        $prevlen = $thislen;
924
        $i += $thislen;
925
        $thislen = 1;
926
    }
927
928
    /**
929
     * Private helper method to detect a valid $foo$ quote delimiter at
930
     * the start of the parameter dquote.
931
     *
932
     * @param string $dquote
933
     *
934
     * @return bool true if valid, false otherwise
935
     */
936
    protected function valid_dolquote($dquote)
937
    {
938
        // XXX: support multibyte
939
        return \preg_match('/^[$][$]/', $dquote) || \preg_match('/^[$][_[:alpha:]][_[:alnum:]]*[$]/', $dquote);
940
    }
941
}
942