Passed
Pull Request — develop (#340)
by Felipe
03:46
created

Postgres::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 7
c 2
b 0
f 0
nc 1
nop 3
dl 0
loc 11
rs 10
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