Issues (217)

src/controllers/SqlController.php (7 issues)

1
<?php
2
3
/**
4
 * PHPPgAdmin 6.1.3
5
 */
6
7
namespace PHPPgAdmin\Controller;
8
9
/**
10
 * Base controller class.
11
 */
12
class SqlController extends BaseController
13
{
14
    public $query = '';
15
16
    public $subject = '';
17
18
    public $start_time;
19
20
    public $duration;
21
22
    public $controller_title = 'strqueryresults';
23
24
    /**
25
     * Default method to render the controller according to the action parameter.
26
     */
27
    public function render()
28
    {
29
        $data = $this->misc->getDatabaseAccessor();
30
31
        \set_time_limit(0);
32
33
        // We need to store the query in a session for editing purposes
34
        // We avoid GPC vars to avoid truncating long queries
35
        if (isset($_REQUEST['subject']) && 'history' === $_REQUEST['subject']) {
36
            // Or maybe we came from the history popup
37
            $_SESSION['sqlquery'] = $_SESSION['history'][$_REQUEST['server']][$_REQUEST['database']][$_GET['queryid']]['query'];
38
            $this->query = $_SESSION['sqlquery'];
39
        } elseif (isset($_POST['query'])) {
40
            // Or maybe we came from an sql form
41
            $_SESSION['sqlquery'] = $_POST['query'];
42
            $this->query = $_SESSION['sqlquery'];
43
        } else {
44
            echo 'could not find the query!!';
45
        }
46
47
        // Pagination maybe set by a get link that has it as FALSE,
48
        // if that's the case, unset the variable.
49
        if (isset($_REQUEST['paginate']) && 'f' === $_REQUEST['paginate']) {
50
            unset($_REQUEST['paginate'], $_POST['paginate'], $_GET['paginate']);
51
        }
52
53
        if (isset($_REQUEST['subject'])) {
54
            $this->subject = $_REQUEST['subject'];
55
        }
56
57
        // Check to see if pagination has been specified. In that case, send to display
58
        // script for pagination
59
        // if a file is given or the request is an explain, do not paginate
60
        if (isset($_REQUEST['paginate']) &&
61
            !(isset($_FILES['script']) && 0 < $_FILES['script']['size']) &&
62
            (0 === \preg_match('/^\s*explain/i', $this->query))) {
63
            //if (!(isset($_FILES['script']) && $_FILES['script']['size'] > 0)) {
64
65
            $display_controller = new DisplayController($this->getContainer());
66
67
            return $display_controller->render();
68
        }
69
70
        $this->printHeader($this->headerTitle(), null, true, 'header_sqledit.twig');
71
        $this->printBody();
72
        $this->printTrail('database');
73
        $this->printTitle($this->lang['strqueryresults']);
74
75
        // Set the schema search path
76
        if (isset($_REQUEST['search_path'])) {
77
            if (0 !== $data->setSearchPath(\array_map('trim', \explode(',', $_REQUEST['search_path'])))) {
78
                return $this->printFooter();
79
            }
80
        }
81
82
        // May as well try to time the query
83
        if (\function_exists('microtime')) {
84
            [$usec, $sec] = \explode(' ', \microtime());
85
            $this->start_time = ((float) $usec + (float) $sec);
86
        }
87
88
        $rs = $this->doDefault();
89
90
        $this->doFooter(true, 'footer_sqledit.twig', $rs);
91
    }
92
93
    public function doDefault()
94
    {
95
        $_connection = $this->misc->getConnection();
96
97
        try {
98
            // Execute the query.  If it's a script upload, special handling is necessary
99
            if (isset($_FILES['script']) && 0 < $_FILES['script']['size']) {
100
                return $this->execute_script();
101
            }
102
103
            return $this->execute_query();
104
        } catch (\PHPPgAdmin\ADOdbException $e) {
105
            $message = $e->getMessage();
0 ignored issues
show
The assignment to $message is dead and can be removed.
Loading history...
106
            $trace = $e->getTraceAsString();
0 ignored issues
show
The assignment to $trace is dead and can be removed.
Loading history...
107
            $lastError = $_connection->getLastError();
0 ignored issues
show
The assignment to $lastError is dead and can be removed.
Loading history...
108
109
            return null;
110
        }
111
    }
112
113
    private function execute_script()
114
    {
115
        $misc = $this->misc;
116
        $data = $this->misc->getDatabaseAccessor();
117
        $_connection = $this->misc->getConnection();
118
        $lang = $this->lang;
119
        /**
120
         * This is a callback function to display the result of each separate query.
121
         *
122
         * @param ADORecordSet $rs The recordset returned by the script execetor
123
         */
124
        $sqlCallback = static function ($query, $rs, $lineno) use ($data, $misc, $lang, $_connection): void {
125
            // Check if $rs is false, if so then there was a fatal error
126
            if (false === $rs) {
127
                echo \htmlspecialchars($_FILES['script']['name']), ':', $lineno, ': ', \nl2br(\htmlspecialchars($_connection->getLastError())), '<br/>' . \PHP_EOL;
128
            } else {
129
                // Print query results
130
                switch (\pg_result_status($rs)) {
131
                    case \PGSQL_TUPLES_OK:
132
                        // If rows returned, then display the results
133
                        $num_fields = \pg_numfields($rs);
134
                        echo "<p><table>\n<tr>";
135
136
                        for ($k = 0; $k < $num_fields; ++$k) {
137
                            echo '<th class="data">', $misc->printVal(\pg_fieldname($rs, $k)), '</th>';
138
                        }
139
140
                        $i = 0;
141
                        $row = \pg_fetch_row($rs);
142
143
                        while (false !== $row) {
144
                            $id = (0 === ($i % 2) ? '1' : '2');
145
                            echo "<tr class=\"data{$id}\">" . \PHP_EOL;
146
147
                            foreach ($row as $k => $v) {
148
                                echo '<td style="white-space:nowrap;">', $misc->printVal($v, \pg_fieldtype($rs, $k), ['null' => true]), '</td>';
149
                            }
150
                            echo '</tr>' . \PHP_EOL;
151
                            $row = \pg_fetch_row($rs);
152
                            ++$i;
153
                        }
154
155
                        echo '</table><br/>' . \PHP_EOL;
156
                        echo $i, " {$lang['strrows']}</p>" . \PHP_EOL;
157
158
                        break;
159
                    case \PGSQL_COMMAND_OK:
160
                        // If we have the command completion tag
161
                        if (\version_compare(\PHP_VERSION, '4.3', '>=')) {
162
                            echo \htmlspecialchars(\pg_result_status($rs, \PGSQL_STATUS_STRING)), '<br/>' . \PHP_EOL;
163
                        } elseif (0 < $data->conn->Affected_Rows()) {
164
                            // Otherwise if any rows have been affected
165
                            echo $data->conn->Affected_Rows(), " {$lang['strrowsaff']}<br/>" . \PHP_EOL;
0 ignored issues
show
Are you sure $data->conn->Affected_Rows() of type false can be used in echo? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

165
                            echo /** @scrutinizer ignore-type */ $data->conn->Affected_Rows(), " {$lang['strrowsaff']}<br/>" . \PHP_EOL;
Loading history...
166
                        }
167
                        // Otherwise output nothing...
168
                        break;
169
                    case \PGSQL_EMPTY_QUERY:
170
                        break;
171
172
                    default:
173
                        break;
174
                }
175
            }
176
        };
177
178
        return $data->executeScript('script', $sqlCallback);
179
    }
180
181
    private function execute_query()
182
    {
183
        $data = $this->misc->getDatabaseAccessor();
184
185
        // Set fetch mode to NUM so that duplicate field names are properly returned
186
        $data->conn->setFetchMode(\ADODB_FETCH_NUM);
0 ignored issues
show
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

186
        $data->conn->setFetchMode(/** @scrutinizer ignore-type */ \ADODB_FETCH_NUM);
Loading history...
187
        \set_time_limit(25000);
188
189
        /**
190
         * @var \ADORecordSet
191
         */
192
        $rs = $data->conn->Execute($this->query);
0 ignored issues
show
$this->query of type string is incompatible with the type SQL expected by parameter $sql of ADOConnection::Execute(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

192
        $rs = $data->conn->Execute(/** @scrutinizer ignore-type */ $this->query);
Loading history...
193
194
        echo '<form method="post" id="sqlform" action="' . $_SERVER['REQUEST_URI'] . '">';
195
        echo '<textarea width="90%" name="query"  id="query" rows="5" cols="100" resizable="true">';
196
197
        echo \htmlspecialchars($this->query);
198
        echo '</textarea><br>';
199
        echo $this->view->setForm();
200
        echo '<input type="submit"/></form>';
201
202
        // $rs will only be an object if there is no error
203
        if (\is_object($rs)) {
204
            // Request was run, saving it in history
205
            if (!isset($_REQUEST['nohistory'])) {
206
                $this->misc->saveScriptHistory($this->query);
207
            }
208
209
            // Now, depending on what happened do various things
210
211
            // First, if rows returned, then display the results
212
            if (0 < $rs->recordCount()) {
213
                echo "<table>\n<tr>";
214
215
                foreach ($rs->fields as $k => $v) {
216
                    $finfo = $rs->fetchField($k);
217
                    echo '<th class="data">', $this->misc->printVal($finfo->name), '</th>';
218
                }
219
                echo '</tr>' . \PHP_EOL;
220
                $i = 0;
221
222
                while (!$rs->EOF) {
223
                    $id = (0 === ($i % 2) ? '1' : '2');
224
                    echo "<tr class=\"data{$id}\">" . \PHP_EOL;
225
226
                    foreach ($rs->fields as $k => $v) {
227
                        $finfo = $rs->fetchField($k);
228
                        echo '<td style="white-space:nowrap;">', $this->misc->printVal($v, $finfo->type, ['null' => true]), '</td>';
229
                    }
230
                    echo '</tr>' . \PHP_EOL;
231
                    $rs->moveNext();
232
                    ++$i;
233
                }
234
                echo '</table>' . \PHP_EOL;
235
                echo '<p>', $rs->recordCount(), " {$this->lang['strrows']}</p>" . \PHP_EOL;
236
            } elseif (0 < $data->conn->Affected_Rows()) {
237
                // Otherwise if any rows have been affected
238
                echo '<p>', $data->conn->Affected_Rows(), " {$this->lang['strrowsaff']}</p>" . \PHP_EOL;
0 ignored issues
show
Are you sure $data->conn->Affected_Rows() of type false can be used in echo? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

238
                echo '<p>', /** @scrutinizer ignore-type */ $data->conn->Affected_Rows(), " {$this->lang['strrowsaff']}</p>" . \PHP_EOL;
Loading history...
239
            } else {
240
                // Otherwise nodata to print
241
                echo '<p>', $this->lang['strnodata'], '</p>' . \PHP_EOL;
242
            }
243
244
            return $rs;
245
        }
246
    }
247
248
    /**
249
     * @param true       $doBody
250
     * @param string     $template
251
     * @param null|mixed $rs
252
     */
253
    private function doFooter(bool $doBody = true, string $template = 'footer.twig', $rs = null)
254
    {
255
        $data = $this->misc->getDatabaseAccessor();
256
257
        // May as well try to time the query
258
        if (null !== $this->start_time) {
259
            [$usec, $sec] = \explode(' ', \microtime());
260
            $end_time = ((float) $usec + (float) $sec);
261
            // Get duration in milliseconds, round to 3dp's
262
            $this->duration = \number_format(($end_time - $this->start_time) * 1000, 3);
263
        }
264
265
        // Reload the browser as we may have made schema changes
266
        $this->view->setReloadBrowser(true);
267
268
        // Display duration if we know it
269
        if (null !== $this->duration) {
270
            echo '<p>', \sprintf($this->lang['strruntime'], $this->duration), '</p>' . \PHP_EOL;
271
        }
272
273
        echo "<p>{$this->lang['strsqlexecuted']}</p>" . \PHP_EOL;
274
275
        $navlinks = [];
276
        $fields = [
277
            'server' => $_REQUEST['server'],
278
            'database' => $_REQUEST['database'],
279
        ];
280
281
        if (isset($_REQUEST['schema'])) {
282
            $fields['schema'] = $_REQUEST['schema'];
283
        }
284
285
        // Return
286
        if (isset($_REQUEST['return'])) {
287
            $urlvars = $this->misc->getSubjectParams($_REQUEST['return']);
288
            $navlinks['back'] = [
289
                'attr' => [
290
                    'href' => [
291
                        'url' => $urlvars['url'],
292
                        'urlvars' => $urlvars['params'],
293
                    ],
294
                ],
295
                'content' => $this->lang['strback'],
296
            ];
297
        }
298
299
        // Edit
300
        $navlinks['alter'] = [
301
            'attr' => [
302
                'href' => [
303
                    'url' => 'database',
304
                    'urlvars' => \array_merge($fields, [
305
                        'action' => 'sql',
306
                    ]),
307
                ],
308
            ],
309
            'content' => $this->lang['streditsql'],
310
        ];
311
312
        // Create view and download
313
        if ('' !== $this->query && isset($rs) && \is_object($rs) && 0 < $rs->recordCount()) {
314
            // Report views don't set a schema, so we need to disable create view in that case
315
            if (isset($_REQUEST['schema'])) {
316
                $navlinks['createview'] = [
317
                    'attr' => [
318
                        'href' => [
319
                            'url' => 'views',
320
                            'urlvars' => \array_merge($fields, [
321
                                'action' => 'create',
322
                            ]),
323
                        ],
324
                    ],
325
                    'content' => $this->lang['strcreateview'],
326
                ];
327
            }
328
329
            if (isset($_REQUEST['search_path'])) {
330
                $fields['search_path'] = $_REQUEST['search_path'];
331
            }
332
333
            $navlinks['download'] = [
334
                'attr' => [
335
                    'href' => [
336
                        'url' => 'dataexport',
337
                        'urlvars' => $fields,
338
                    ],
339
                ],
340
                'content' => $this->lang['strdownload'],
341
            ];
342
        }
343
344
        $this->printNavLinks($navlinks, 'sql-form', \get_defined_vars());
345
346
        return $this->printFooter($doBody, $template);
347
    }
348
}
349