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

Postgres::getAttributeNames()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 34
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 20
c 1
b 0
f 0
nc 5
nop 2
dl 0
loc 34
rs 9.2888

1 Method

Rating   Name   Duplication   Size   Complexity  
A Postgres::getLanguages() 0 21 3
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