Passed
Push — master ( ad2b10...145306 )
by Michael
08:18
created

XoopsMySQLDatabase::isStrict()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 3
eloc 3
c 1
b 1
f 0
nc 4
nop 0
dl 0
loc 6
rs 10
1
<?php
2
/**
3
 * MySQL access using MySQLi extension
4
 *
5
 * You may not change or alter any portion of this comment or credits
6
 * of supporting developers from this source code or any supporting source code
7
 * which is considered copyrighted (c) material of the original comment or credit authors.
8
 * This program is distributed in the hope that it will be useful,
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
 *
12
 * @copyright       (c) 2000-2025 XOOPS Project (https://xoops.org)
13
 * @license             GNU GPL 2 (https://www.gnu.org/licenses/gpl-2.0.html)
14
 * @package             class
15
 * @subpackage          database
16
 * @since               1.0.0
17
 * @author              Kazumi Ono <[email protected]>
18
 * @author              Rodney Fulk <[email protected]>
19
 */
20
defined('XOOPS_ROOT_PATH') || die('Restricted access');
21
22
include_once XOOPS_ROOT_PATH . '/class/database/database.php';
23
24
/**
25
 * Connection to a MySQL database using MySQLi extension
26
 */
27
abstract class XoopsMySQLDatabase extends XoopsDatabase
28
{
29
    /**
30
     * Strict guard is active in dev or when XOOPS debug mode is on.
31
     */
32
    private function isStrict(): bool
33
    {
34
        // Respect environment switch and also auto-enable when XOOPS debug is on
35
        $envStrict  = (defined('XOOPS_DB_STRICT') && XOOPS_DB_STRICT);
0 ignored issues
show
Bug introduced by
The constant XOOPS_DB_STRICT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
36
        $xoopsDebug = !empty($GLOBALS['xoopsConfig']['debug_mode']);
37
        return $envStrict || $xoopsDebug;
38
    }
39
    /**
40
     * Database connection
41
     *
42
     * @var XoopsDatabase|mysqli
43
     */
44
    public $conn;
45
46
    /**
47
     * connect to the database
48
     *
49
     * @param bool $selectdb select the database now?
50
     * @return bool successful?
51
     */
52
    public function connect($selectdb = true)
53
    {
54
        if (!extension_loaded('mysqli')) {
55
            throw new \Exception('notrace:mysqli extension not loaded');
56
57
            return false;
0 ignored issues
show
Unused Code introduced by
return false is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
58
        }
59
60
        $this->allowWebChanges = ($_SERVER['REQUEST_METHOD'] !== 'GET');
61
62
        if ($selectdb) {
63
            $dbname = constant('XOOPS_DB_NAME');
64
        } else {
65
            $dbname = '';
66
        }
67
        mysqli_report(MYSQLI_REPORT_OFF);
68
        if (XOOPS_DB_PCONNECT == 1) {
0 ignored issues
show
introduced by
The condition XOOPS_DB_PCONNECT == 1 is always false.
Loading history...
69
            $this->conn = new mysqli('p:' . XOOPS_DB_HOST, XOOPS_DB_USER, XOOPS_DB_PASS, $dbname);
70
        } else {
71
            $this->conn = new mysqli(XOOPS_DB_HOST, XOOPS_DB_USER, XOOPS_DB_PASS, $dbname);
72
        }
73
74
        // errno is 0 if connect was successful
75
        if (0 !== $this->conn->connect_errno) {
76
            return false;
77
        }
78
79
        if (defined('XOOPS_DB_CHARSET') && ('' !== XOOPS_DB_CHARSET)) {
0 ignored issues
show
introduced by
The condition '' !== XOOPS_DB_CHARSET is always false.
Loading history...
80
            // $this->queryF("SET NAMES '" . XOOPS_DB_CHARSET . "'");
81
            $this->conn->set_charset(XOOPS_DB_CHARSET);
82
        }
83
        $this->queryF('SET SQL_BIG_SELECTS = 1');
84
85
        return true;
86
    }
87
88
    /**
89
     * generate an ID for a new row
90
     *
91
     * This is for compatibility only. Will always return 0, because MySQL supports
92
     * autoincrement for primary keys.
93
     *
94
     * @param string $sequence name of the sequence from which to get the next ID
95
     * @return int always 0, because mysql has support for autoincrement
96
     */
97
    public function genId($sequence)
0 ignored issues
show
Unused Code introduced by
The parameter $sequence is not used and could be removed. ( Ignorable by Annotation )

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

97
    public function genId(/** @scrutinizer ignore-unused */ $sequence)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
98
    {
99
        return 0; // will use auto_increment
100
    }
101
102
    /**
103
     * Get a result row as an enumerated array
104
     *
105
     * @param \mysqli_result $result
106
     *
107
     * @return array|false false on end of data
108
     */
109
    public function fetchRow($result)
110
    {
111
        $row = @mysqli_fetch_row($result);
112
        return $row ?? false;
113
    }
114
115
    /**
116
     * Fetch a result row as an associative array
117
     *
118
     * @param \mysqli_result $result
119
     *
120
     * @return array|false false on end of data
121
     */
122
    public function fetchArray($result)
123
    {
124
        $row = @mysqli_fetch_assoc($result);
125
        return $row ?? false;
126
127
    }
128
129
    /**
130
     * Fetch a result row as an associative array
131
     *
132
     * @param \mysqli_result $result
133
     *
134
     * @return array|false false on end of data
135
     */
136
    public function fetchBoth($result)
137
    {
138
        $row = @mysqli_fetch_array($result, MYSQLI_BOTH);
139
        return $row ?? false;
140
    }
141
142
    /**
143
     * XoopsMySQLDatabase::fetchObject()
144
     *
145
     * @param \mysqli_result $result
146
     * @return stdClass|false false on end of data
147
     */
148
    public function fetchObject($result)
149
    {
150
        $row = @mysqli_fetch_object($result);
151
        return $row ?? false;
152
    }
153
154
    /**
155
     * Get the ID generated from the previous INSERT operation
156
     *
157
     * @return int|string
158
     */
159
    public function getInsertId()
160
    {
161
        return mysqli_insert_id($this->conn);
0 ignored issues
show
Bug introduced by
It seems like $this->conn can also be of type XoopsDatabase; however, parameter $mysql of mysqli_insert_id() does only seem to accept mysqli, maybe add an additional type check? ( Ignorable by Annotation )

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

161
        return mysqli_insert_id(/** @scrutinizer ignore-type */ $this->conn);
Loading history...
162
    }
163
164
    /**
165
     * Get number of rows in result
166
     *
167
     * @param \mysqli_result $result
168
     *
169
     * @return int
170
     */
171
    public function getRowsNum($result)
172
    {
173
        return (int)@mysqli_num_rows($result);
174
    }
175
176
    /**
177
     * Get number of affected rows
178
     *
179
     * @return int
180
     */
181
    public function getAffectedRows()
182
    {
183
        return (int)mysqli_affected_rows($this->conn);
0 ignored issues
show
Bug introduced by
It seems like $this->conn can also be of type XoopsDatabase; however, parameter $mysql of mysqli_affected_rows() does only seem to accept mysqli, maybe add an additional type check? ( Ignorable by Annotation )

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

183
        return (int)mysqli_affected_rows(/** @scrutinizer ignore-type */ $this->conn);
Loading history...
184
    }
185
186
    /**
187
     * Close MySQL connection
188
     *
189
     * @return void
190
     */
191
    public function close()
192
    {
193
        mysqli_close($this->conn);
0 ignored issues
show
Bug introduced by
It seems like $this->conn can also be of type XoopsDatabase; however, parameter $mysql of mysqli_close() does only seem to accept mysqli, maybe add an additional type check? ( Ignorable by Annotation )

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

193
        mysqli_close(/** @scrutinizer ignore-type */ $this->conn);
Loading history...
194
    }
195
196
    /**
197
     * will free all memory associated with the result identifier result.
198
     *
199
     * @param \mysqli_result $result result
200
     *
201
     * @return void
202
     */
203
    public function freeRecordSet($result)
204
    {
205
        mysqli_free_result($result);
206
    }
207
208
    /**
209
     * Returns the text of the error message from previous MySQL operation
210
     *
211
     * @return string Returns the error text from the last MySQL function, or '' (the empty string) if no error occurred.
212
     */
213
    public function error()
214
    {
215
        return @mysqli_error($this->conn);
0 ignored issues
show
Bug introduced by
It seems like $this->conn can also be of type XoopsDatabase; however, parameter $mysql of mysqli_error() does only seem to accept mysqli, maybe add an additional type check? ( Ignorable by Annotation )

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

215
        return @mysqli_error(/** @scrutinizer ignore-type */ $this->conn);
Loading history...
216
    }
217
218
    /**
219
     * Returns the numerical value of the error message from previous MySQL operation
220
     *
221
     * @return int Returns the error number from the last MySQL function, or 0 (zero) if no error occurred.
222
     */
223
    public function errno()
224
    {
225
        return @mysqli_errno($this->conn);
0 ignored issues
show
Bug introduced by
It seems like $this->conn can also be of type XoopsDatabase; however, parameter $mysql of mysqli_errno() does only seem to accept mysqli, maybe add an additional type check? ( Ignorable by Annotation )

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

225
        return @mysqli_errno(/** @scrutinizer ignore-type */ $this->conn);
Loading history...
226
    }
227
228
    /**
229
     * Returns escaped string text with single quotes around it to be safely stored in database
230
     *
231
     * @param string $str unescaped string text
232
     * @return string escaped string text with single quotes around
233
     * @deprecated : delegate to exec().
234
     */
235
    public function quoteString($str)
236
    {
237
238
        if (is_object($this->logger)) {
239
            $this->logger->addDeprecated(__METHOD__ . " is deprecated since XOOPS 2.5.12, please use 'quote()' instead.");
240
        }
241
242
        return $this->quote($str);
243
    }
244
245
    /**
246
     * Quotes a string for use in a query.
247
     *
248
     * @param string $string string to quote/escape for use in query
249
     *
250
     * @return string
251
     */
252
    public function quote($string)
253
    {
254
        $quoted = $this->escape($string);
255
        return "'{$quoted}'";
256
    }
257
258
    /**
259
     * Escapes a string for use in a query. Does not add surrounding quotes.
260
     *
261
     * @param string $string string to escape
262
     *
263
     * @return string
264
     */
265
    public function escape($string)
266
    {
267
        return mysqli_real_escape_string($this->conn, (string) $string);
0 ignored issues
show
Bug introduced by
It seems like $this->conn can also be of type XoopsDatabase; however, parameter $mysql of mysqli_real_escape_string() does only seem to accept mysqli, maybe add an additional type check? ( Ignorable by Annotation )

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

267
        return mysqli_real_escape_string(/** @scrutinizer ignore-type */ $this->conn, (string) $string);
Loading history...
268
    }
269
270
    /**
271
     * perform a query on the database
272
     *
273
     * @param string $sql   a valid MySQL query
274
     * @param int    $limit number of records to return
275
     * @param int    $start offset of first record to return
276
     * @return mysqli_result|bool query result or FALSE if successful
277
     *                      or TRUE if successful and no result
278
     */
279
    public function queryF($sql, $limit = 0, $start = 0)
280
    {
281
        if (!empty($limit)) {
282
            if (empty($start)) {
283
                $start = 0;
284
            }
285
            $sql .= ' LIMIT ' . (int)$start . ', ' . (int)$limit;
286
        }
287
        $this->logger->startTime('query_time');
288
        $result = mysqli_query($this->conn, $sql);
0 ignored issues
show
Bug introduced by
It seems like $this->conn can also be of type XoopsDatabase; however, parameter $mysql of mysqli_query() does only seem to accept mysqli, maybe add an additional type check? ( Ignorable by Annotation )

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

288
        $result = mysqli_query(/** @scrutinizer ignore-type */ $this->conn, $sql);
Loading history...
289
        $this->logger->stopTime('query_time');
290
        $t = $this->logger->dumpTime('query_time', true);
291
292
        if ($result) {
293
            $this->logger->addQuery($sql, null, null, $t);
294
            return $result;             // mysqli_result for SELECT, true for writes
295
        } else {
296
            $this->logger->addQuery($sql, $this->error(), $this->errno(), $t);
297
            return false;
298
        }
299
    }
300
301
    /**
302
     * perform a query
303
     *
304
     * This method is empty and does nothing! It should therefore only be
305
     * used if nothing is exactly what you want done! ;-)
306
     *
307
     * @param string $sql   a valid MySQL query
308
     * @param int|null    $limit number of records to return
309
     * @param int|null    $start offset of first record to return
310
     *
311
     * @return \mysqli_result|bool false on failure; true only if a write slipped through (BC)
312
     */
313
    public function query(string $sql, ?int $limit = null, ?int $start = null)
314
    {
315
        // Dev-only guard: query() should be read-like
316
        if ($this->isStrict()) {
317
            if (!preg_match('/^\s*(SELECT|WITH|SHOW|DESCRIBE|EXPLAIN)\b/i', $sql)) {
318
                if (is_object($this->logger)) {
319
                    $this->logger->addExtra('DB', 'query() called with a mutating statement; use exec()');
320
                }
321
                trigger_error('query() called with a mutating statement; use exec()', E_USER_WARNING);
322
                // continue for BC
323
            }
324
        }
325
326
        // Pagination if requested (null = no pagination)
327
        if ($limit !== null) {
328
            $start = max(0, $start ?? 0);
329
            $sql .= ' LIMIT ' . (int)$limit . ' OFFSET ' . $start;
330
        }
331
332
        // Connection type guard for static analyzers and safety
333
        if (!($this->conn instanceof \mysqli)) {
334
            trigger_error('Invalid or uninitialized mysqli connection', E_USER_WARNING);
335
            return false;
336
        }
337
338
        // Timing + execution
339
        if (is_object($this->logger)) $this->logger->startTime('query_time');
340
        $res = \mysqli_query($this->conn, $sql);
341
        if (is_object($this->logger)) {
342
            $this->logger->stopTime('query_time');
343
            $t = $this->logger->dumpTime('query_time', true);
344
        } else {
345
            $t = 0;
346
        }
347
348
        if ($res === false) {
349
            if (is_object($this->logger)) {
350
                $this->logger->addQuery($sql, $this->error(), $this->errno(), $t);
351
            }
352
            return false;
353
        }
354
355
        // Log success
356
        if (is_object($this->logger)) {
357
            $this->logger->addQuery($sql, null, null, $t);
358
        }
359
360
        // If a write slipped into query() (rare), mysqli returns true — keep BC
361
        return $res;
362
    }
363
364
365
    /**
366
     * perform queries from SQL dump file in a batch
367
     *
368
     * @param string $file file path to an SQL dump file
369
     * @return bool FALSE if failed reading SQL file or TRUE if the file has been read and queries executed
370
     */
371
    public function queryFromFile($file)
372
    {
373
        if (false !== ($fp = fopen($file, 'r'))) {
374
            include_once XOOPS_ROOT_PATH . '/class/database/sqlutility.php';
375
            $sql_queries = trim(fread($fp, filesize($file)));
376
            SqlUtility::splitMySqlFile($pieces, $sql_queries);
377
            foreach ($pieces as $query) {
378
                // [0] contains the prefixed query
379
                // [4] contains unprefixed table name
380
                $prefixed_query = SqlUtility::prefixQuery(trim($query), $this->prefix());
381
                if ($prefixed_query != false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison !== instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
382
                    $this->query($prefixed_query[0]);
383
                }
384
            }
385
386
            return true;
387
        }
388
389
        return false;
390
    }
391
392
    /**
393
     * Get field name
394
     *
395
     * @param \mysqli_result $result query result
396
     * @param int           $offset numerical field index
397
     *
398
     * @return string
399
     */
400
    public function getFieldName($result, $offset)
401
    {
402
        return $result->fetch_field_direct($offset)->name;
403
    }
404
405
    /**
406
     * Get field type
407
     *
408
     * @param \mysqli_result $result query result
409
     * @param int           $offset numerical field index
410
     *
411
     * @return string
412
     */
413
    public function getFieldType($result, $offset)
414
    {
415
        $typecode = $result->fetch_field_direct($offset)->type;
416
        switch ($typecode) {
417
            case MYSQLI_TYPE_DECIMAL:
418
            case MYSQLI_TYPE_NEWDECIMAL:
419
                $type = 'decimal';
420
                break;
421
            case MYSQLI_TYPE_BIT:
422
                $type = 'bit';
423
                break;
424
            case MYSQLI_TYPE_TINY:
425
            case MYSQLI_TYPE_CHAR:
426
                $type = 'tinyint';
427
                break;
428
            case MYSQLI_TYPE_SHORT:
429
                $type = 'smallint';
430
                break;
431
            case MYSQLI_TYPE_LONG:
432
                $type = 'int';
433
                break;
434
            case MYSQLI_TYPE_FLOAT:
435
                $type = 'float';
436
                break;
437
            case MYSQLI_TYPE_DOUBLE:
438
                $type = 'double';
439
                break;
440
            case MYSQLI_TYPE_NULL:
441
                $type = 'NULL';
442
                break;
443
            case MYSQLI_TYPE_TIMESTAMP:
444
                $type = 'timestamp';
445
                break;
446
            case MYSQLI_TYPE_LONGLONG:
447
                $type = 'bigint';
448
                break;
449
            case MYSQLI_TYPE_INT24:
450
                $type = 'mediumint';
451
                break;
452
            case MYSQLI_TYPE_NEWDATE:
453
            case MYSQLI_TYPE_DATE:
454
                $type = 'date';
455
                break;
456
            case MYSQLI_TYPE_TIME:
457
                $type = 'time';
458
                break;
459
            case MYSQLI_TYPE_DATETIME:
460
                $type = 'datetime';
461
                break;
462
            case MYSQLI_TYPE_YEAR:
463
                $type = 'year';
464
                break;
465
            case MYSQLI_TYPE_INTERVAL:
466
                $type = 'interval';
467
                break;
468
            case MYSQLI_TYPE_ENUM:
469
                $type = 'enum';
470
                break;
471
            case MYSQLI_TYPE_SET:
472
                $type = 'set';
473
                break;
474
            case MYSQLI_TYPE_TINY_BLOB:
475
                $type = 'tinyblob';
476
                break;
477
            case MYSQLI_TYPE_MEDIUM_BLOB:
478
                $type = 'mediumblob';
479
                break;
480
            case MYSQLI_TYPE_LONG_BLOB:
481
                $type = 'longblob';
482
                break;
483
            case MYSQLI_TYPE_BLOB:
484
                $type = 'blob';
485
                break;
486
            case MYSQLI_TYPE_VAR_STRING:
487
                $type = 'varchar';
488
                break;
489
            case MYSQLI_TYPE_STRING:
490
                $type = 'char';
491
                break;
492
            case MYSQLI_TYPE_GEOMETRY:
493
                $type = 'geometry';
494
                break;
495
            default:
496
                $type = 'unknown';
497
                break;
498
        }
499
500
        return $type;
501
    }
502
503
    /**
504
     * Get number of fields in result
505
     *
506
     * @param \mysqli_result $result query result
507
     *
508
     * @return int
509
     */
510
    public function getFieldsNum($result)
511
    {
512
        return mysqli_num_fields($result);
513
    }
514
515
    /**
516
     * getServerVersion get version of the mysql server
517
     *
518
     * @return string
519
     */
520
    public function getServerVersion()
521
    {
522
        return mysqli_get_server_info($this->conn);
0 ignored issues
show
Bug introduced by
It seems like $this->conn can also be of type XoopsDatabase; however, parameter $mysql of mysqli_get_server_info() does only seem to accept mysqli, maybe add an additional type check? ( Ignorable by Annotation )

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

522
        return mysqli_get_server_info(/** @scrutinizer ignore-type */ $this->conn);
Loading history...
523
    }
524
525
    /**
526
     * Test the passed result to determine if it is a valid result set
527
     *
528
     * @param mixed $result value to test
529
     *
530
     * @return bool true if $result is a database result set, otherwise false
531
     */
532
    public function isResultSet($result)
533
    {
534
        return is_a($result, 'mysqli_result');
535
    }
536
537
    public function exec(string $sql): bool
538
    {
539
        // Dev-only guard: exec() should be write-like
540
        if ($this->isStrict()) {
541
            if (preg_match('/^\s*(SELECT|WITH|SHOW|DESCRIBE|EXPLAIN)\b/i', $sql)) {
542
                if (is_object($this->logger)) {
543
                    $this->logger->addExtra('DB', 'exec() called with a read-only statement');
544
                }
545
                trigger_error('exec() called with a read-only statement', E_USER_WARNING);
546
                // continue for BC
547
            }
548
        }
549
550
        if (!($this->conn instanceof \mysqli)) {
551
            trigger_error('Invalid or uninitialized mysqli connection', E_USER_WARNING);
552
            return false;
553
        }
554
555
        // Timing + execution
556
        if (is_object($this->logger)) $this->logger->startTime('query_time');
557
        $res = \mysqli_query($this->conn, $sql);
558
        if (is_object($this->logger)) {
559
            $this->logger->stopTime('query_time');
560
            $t = $this->logger->dumpTime('query_time', true);
561
        } else {
562
            $t = 0;
563
        }
564
565
        if ($res === false) {
566
            if (is_object($this->logger)) {
567
                $this->logger->addQuery($sql, $this->error(), $this->errno(), $t);
568
        }
569
            return false;
570
        }
571
572
        // If someone passes a SELECT by mistake, mysqli returns a result; free it and treat as success (BC)
573
        if ($res instanceof \mysqli_result) {
574
                \mysqli_free_result($res);
575
        }
576
577
        if (is_object($this->logger)) {
578
            $this->logger->addQuery($sql, null, null, $t);
579
    }
580
        return true;
581
    }
582
}
583
584
/**
585
 * Safe Connection to a MySQL database.
586
 *
587
 * Delegates to parent; signature matches parent for LSP.
588
 */
589
class XoopsMySQLDatabaseSafe extends XoopsMySQLDatabase
590
{
591
    /**
592
     * perform a query on the database
593
     *
594
     * @param string $sql   a valid MySQL query
595
     * @param int    $limit number of records to return
596
     * @param int    $start offset of first record to return
597
     * @return mysqli_result|bool query result or FALSE if successful
598
     *                      or TRUE if successful and no result
599
     */
600
    public function query($sql, $limit = 0, $start = 0)
601
    {
602
        return parent::query($sql, $limit ?: null, $start ?: null);
603
    }
604
}
605
606
/**
607
 * Read-Only connection to a MySQL database.
608
 *
609
 * This class allows only SELECT queries to be performed through its
610
 * {@link query()} method for security reasons.
611
 *
612
 * @author              Kazumi Ono <[email protected]>
613
 * @copyright       (c) 2000-2025 XOOPS Project (https://xoops.org)
614
 * @package             class
615
 * @subpackage          database
616
 */
617
class XoopsMySQLDatabaseProxy extends XoopsMySQLDatabase
618
{
619
    /**
620
     * perform a query on the database
621
     *
622
     * this method allows only SELECT queries for safety.
623
     *
624
     * @param string $sql   a valid MySQL query
625
     * @param int    $limit number of records to return
626
     * @param int    $start offset of first record to return
627
     *
628
     * @return mysqli_result|bool query result or FALSE if successful
629
     *                      or TRUE if successful and no result
630
     */
631
    public function query(string $sql, ?int $limit = null, ?int $start = null)
632
    {
633
        $sql = ltrim($sql);
634
        if (!$this->allowWebChanges && stripos($sql, 'select') !== 0) {
635
            trigger_error('Database updates are not allowed during processing of a GET request', E_USER_WARNING);
636
637
            return false;
638
        }
639
        // Execute via queryF() to preserve legacy path (and LIMIT semantics)
640
        if ($limit !== null) {
641
            $start = max(0, $start ?? 0);
642
            return $this->queryF($sql, (int)$limit, (int)$start);
643
    }
644
        return $this->queryF($sql);
645
    }
646
647
}
648