Test Failed
Pull Request — develop (#340)
by Felipe
03:49
created

Postgres::browseQuery()   F

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.0.0
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
        //\Kint::dump($sql);
298
299
        // Add advanced objects if show_advanced is set
300
        if ($conf['show_advanced']) {
301
            $sql .= "
302
				UNION ALL
303
				SELECT CASE WHEN pt.typtype='d' THEN 'DOMAIN' ELSE 'TYPE' END, pt.oid, pn.nspname, NULL,
304
					pt.typname FROM pg_catalog.pg_type pt, pg_catalog.pg_namespace pn
305
					WHERE pt.typnamespace=pn.oid AND typname ILIKE {$term}
306
					AND (pt.typrelid = 0 OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = pt.typrelid))
307
					{$where}
308
			 	UNION ALL
309
				SELECT 'OPERATOR', po.oid, pn.nspname, NULL, po.oprname FROM pg_catalog.pg_operator po, pg_catalog.pg_namespace pn
310
					WHERE po.oprnamespace=pn.oid AND oprname ILIKE {$term} {$where}
311
				UNION ALL
312
				SELECT 'CONVERSION', pc.oid, pn.nspname, NULL, pc.conname FROM pg_catalog.pg_conversion pc,
313
					pg_catalog.pg_namespace pn WHERE pc.connamespace=pn.oid AND conname ILIKE {$term} {$where}
314
				UNION ALL
315
				SELECT 'LANGUAGE', pl.oid, NULL, NULL, pl.lanname FROM pg_catalog.pg_language pl
316
					WHERE lanname ILIKE {$term} {$lan_where}
317
				UNION ALL
318
				SELECT DISTINCT ON (p.proname) 'AGGREGATE', p.oid, pn.nspname, NULL, p.proname FROM pg_catalog.pg_proc p
319
					LEFT JOIN pg_catalog.pg_namespace pn ON p.pronamespace=pn.oid
320
					WHERE p.proisagg AND p.proname ILIKE {$term} {$where}
321
				UNION ALL
322
				SELECT DISTINCT ON (po.opcname) 'OPCLASS', po.oid, pn.nspname, NULL, po.opcname FROM pg_catalog.pg_opclass po,
323
					pg_catalog.pg_namespace pn WHERE po.opcnamespace=pn.oid
324
					AND po.opcname ILIKE {$term} {$where}
325
			";
326
        } else {
327
            // Otherwise just add domains
328
            $sql .= "
329
				UNION ALL
330
				SELECT 'DOMAIN', pt.oid, pn.nspname, NULL,
331
					pt.typname FROM pg_catalog.pg_type pt, pg_catalog.pg_namespace pn
332
					WHERE pt.typnamespace=pn.oid AND pt.typtype='d' AND typname ILIKE {$term}
333
					AND (pt.typrelid = 0 OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = pt.typrelid))
334
					{$where}
335
			";
336
        }
337
338
        if ('' !== $filter) {
339
            // We use like to make RULE, CONSTRAINT and COLUMN searches work
340
            $sql .= ") AS sub WHERE type LIKE '{$filter}%' ";
341
        }
342
343
        $sql .= 'ORDER BY type, schemaname, relname, name';
344
345
        return $this->selectSet($sql);
346
    }
347
348
    /**
349
     * Gets all languages.
350
     *
351
     * @param bool $all True to get all languages, regardless of show_system
352
     *
353
     * @return int|\PHPPgAdmin\ADORecordSet A recordset
354
     */
355
    public function getLanguages($all = false)
356
    {
357
        $conf = $this->conf;
358
359
        if ($conf['show_system'] || $all) {
360
            $where = '';
361
        } else {
362
            $where = 'WHERE lanispl';
363
        }
364
365
        $sql = "
366
			SELECT
367
				lanname, lanpltrusted,
368
				lanplcallfoid::pg_catalog.regproc AS lanplcallf
369
			FROM
370
				pg_catalog.pg_language
371
			{$where}
372
			ORDER BY lanname
373
		";
374
375
        return $this->selectSet($sql);
376
    }
377
378
    /**
379
     * Executes an SQL script as a series of SQL statements.  Returns
380
     * the result of the final step.  This is a very complicated lexer
381
     * based on the REL7_4_STABLE src/bin/psql/mainloop.c lexer in
382
     * the PostgreSQL source code.
383
     * XXX: It does not handle multibyte languages properly.
384
     *
385
     * @param string        $name     Entry in $_FILES to use
386
     * @param null|callable $callback (optional) Callback function to call with each query, its result and line number
387
     *
388
     * @return bool|mixed true for general success, false on any failure, or resultset
389
     */
390
    public function executeScript($name, $callback = null)
391
    {
392
        // This whole function isn't very encapsulated, but hey...
393
        $conn = $this->conn->_connectionID;
394
395
        if (!\is_uploaded_file($_FILES[$name]['tmp_name'])) {
396
            return false;
397
        }
398
399
        $fd = \fopen($_FILES[$name]['tmp_name'], 'rb');
400
401
        if (false === $fd) {
402
            return false;
403
        }
404
405
        // Build up each SQL statement, they can be multiline
406
        $query_buf = null;
407
        $query_start = 0;
408
        $in_quote = 0;
409
        $in_xcomment = 0;
410
        $bslash_count = 0;
411
        $dol_quote = '';
412
        $paren_level = 0;
413
        $len = 0;
414
        $i = 0;
415
        $prevlen = 0;
416
        $thislen = 0;
417
        $lineno = 0;
418
419
        // Loop over each line in the file
420
        while (!\feof($fd)) {
421
            $line = \fgets($fd);
422
            ++$lineno;
423
424
            // Nothing left on line? Then ignore...
425
            if ('' === \trim($line)) {
426
                continue;
427
            }
428
429
            $len = \mb_strlen($line);
430
            $query_start = 0;
431
432
            /**
433
             * Parse line, looking for command separators.
434
             *
435
             * The current character is at line[i], the prior character at line[i
436
             * - prevlen], the next character at line[i + thislen].
437
             */
438
            $prevlen = 0;
439
            $thislen = (0 < $len) ? 1 : 0;
440
441
            for ($i = 0; $i < $len; $this->advance_1($i, $prevlen, $thislen)) {
442
                /* was the previous character a backslash? */
443
                if (0 < $i && '\\' === \mb_substr($line, $i - $prevlen, 1)) {
444
                    $this->prtrace('bslash_count', $bslash_count, $line);
445
                    ++$bslash_count;
446
                } else {
447
                    $bslash_count = 0;
448
                }
449
450
                /*
451
                 * It is important to place the in_* test routines before the
452
                 * in_* detection routines. i.e. we have to test if we are in
453
                 * a quote before testing for comments.
454
                 */
455
456
                /* in quote? */
457
                if (0 !== $in_quote) {
458
                    //$this->prtrace('in_quote', $in_quote, $line);
459
                    /*
460
                     * end of quote if matching non-backslashed character.
461
                     * backslashes don't count for double quotes, though.
462
                     */
463
                    if (\mb_substr($line, $i, 1) === $in_quote &&
464
                        (0 === $bslash_count % 2 || '"' === $in_quote)
465
                    ) {
466
                        $in_quote = 0;
467
                    }
468
                } elseif ($dol_quote) {
469
                    $this->prtrace('dol_quote', $dol_quote, $line);
470
471
                    if (0 === \strncmp(\mb_substr($line, $i), $dol_quote, \mb_strlen($dol_quote))) {
472
                        $this->advance_1($i, $prevlen, $thislen);
473
474
                        while ('$' !== \mb_substr($line, $i, 1)) {
475
                            $this->advance_1($i, $prevlen, $thislen);
476
                        }
477
478
                        $dol_quote = '';
479
                    }
480
                } elseif ('/*' === \mb_substr($line, $i, 2)) {
481
                    $this->prtrace('open_xcomment', $in_xcomment, $line, $i, $prevlen, $thislen);
482
483
                    if (0 === $in_xcomment) {
484
                        ++$in_xcomment;
485
                        $finishpos = \mb_strpos(\mb_substr($line, $i, $len), '*/');
486
487
                        if (false === $finishpos) {
488
                            $line = \mb_substr($line, 0, $i); /* remove comment */
489
490
                            break;
491
                        }
492
                        $pre = \mb_substr($line, 0, $i);
493
                        $post = \mb_substr($line, $i + 2 + $finishpos, $len);
494
                        $line = $pre . ' ' . $post;
495
                        $in_xcomment = 0;
496
                        $i = 0;
497
                    }
498
                } elseif ($in_xcomment) {
499
                    $position = \mb_strpos(\mb_substr($line, $i, $len), '*/');
500
501
                    if (false === $position) {
502
                        $line = '';
503
504
                        break;
505
                    }
506
507
                    $substr = \mb_substr($line, $i, 2);
508
509
                    if ('*/' === $substr && !--$in_xcomment) {
510
                        $line = \mb_substr($line, $i + 2, $len);
511
                        $i += 2;
512
                        $this->advance_1($i, $prevlen, $thislen);
513
                    }
514
                    // old logic
515
                    //  } else if (substr($line, $i, 2) == '/*') {
516
                    //      if ($in_xcomment == 0) {
517
                    //          ++$in_xcomment;
518
                    //          $this->advance_1($i, $prevlen, $thislen);
519
                    //      }
520
                    //  } else if ($in_xcomment) {
521
                    //      $substr = substr($line, $i, 2);
522
                    //      if ($substr == '*/' && !--$in_xcomment) {
523
                    //          $this->advance_1($i, $prevlen, $thislen);
524
                    //      }
525
                } elseif ('\'' === \mb_substr($line, $i, 1) || '"' === \mb_substr($line, $i, 1)) {
526
                    $in_quote = \mb_substr($line, $i, 1);
527
                } elseif (!$dol_quote && $this->valid_dolquote(\mb_substr($line, $i))) {
528
                    $dol_end = \mb_strpos(\mb_substr($line, $i + 1), '$');
529
                    $dol_quote = \mb_substr($line, $i, $dol_end + 1);
530
                    $this->advance_1($i, $prevlen, $thislen);
531
532
                    while ('$' !== \mb_substr($line, $i, 1)) {
533
                        $this->advance_1($i, $prevlen, $thislen);
534
                    }
535
                } else {
536
                    if ('--' === \mb_substr($line, $i, 2)) {
537
                        $line = \mb_substr($line, 0, $i); /* remove comment */
538
539
                        break;
540
                    } /* count nested parentheses */
541
542
                    if ('(' === \mb_substr($line, $i, 1)) {
543
                        ++$paren_level;
544
                    } elseif (')' === \mb_substr($line, $i, 1) && 0 < $paren_level) {
545
                        --$paren_level;
546
                    } elseif (';' === \mb_substr($line, $i, 1) && !$bslash_count && !$paren_level) {
547
                        $subline = \mb_substr(\mb_substr($line, 0, $i), $query_start);
548
                        /*
549
                         * insert a cosmetic newline, if this is not the first
550
                         * line in the buffer
551
                         */
552
                        if (0 < \mb_strlen($query_buf)) {
553
                            $query_buf .= "\n";
554
                        }
555
556
                        /* append the line to the query buffer */
557
                        $query_buf .= $subline;
558
                        /* is there anything in the query_buf? */
559
                        if (\trim($query_buf)) {
560
                            $query_buf .= ';';
561
562
                            // Execute the query. PHP cannot execute
563
                            // empty queries, unlike libpq
564
                            $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

564
                            $res = \pg_query(/** @scrutinizer ignore-type */ $conn, $query_buf);
Loading history...
565
566
                            // Call the callback function for display
567
                            if (null !== $callback) {
568
                                $callback($query_buf, $res, $lineno);
569
                            }
570
571
                            // Check for COPY request
572
                            if (4 === \pg_result_status($res)) {
573
                                // 4 == PGSQL_COPY_FROM
574
                                while (!\feof($fd)) {
575
                                    $copy = \fgets($fd, 32768);
576
                                    ++$lineno;
577
                                    \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

577
                                    \pg_put_line(/** @scrutinizer ignore-type */ $conn, $copy);
Loading history...
578
579
                                    if ("\\.\n" === $copy || "\\.\r\n" === $copy) {
580
                                        \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

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

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