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

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

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

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

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