Postgres::browseQuery()   F
last analyzed

Complexity

Conditions 24
Paths 182

Size

Total Lines 107
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 24
eloc 51
c 1
b 0
f 0
nc 182
nop 8
dl 0
loc 107
rs 3.4833

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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