diff options
| author | Philipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de> | 2024-09-24 10:53:31 +0200 |
|---|---|---|
| committer | Philipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de> | 2024-09-24 10:53:31 +0200 |
| commit | 4459dd7917f4d1c34f40bb68f0e991e9c3d53e4c (patch) | |
| tree | 5c07151ae61276d334e88f6309c30d439a85c12e /lib/classes/StudipPDO.php | |
| parent | da0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff) | |
| parent | 97a188592c679890a25c37ab78463add76a52ff7 (diff) | |
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'lib/classes/StudipPDO.php')
| -rw-r--r-- | lib/classes/StudipPDO.php | 385 |
1 files changed, 385 insertions, 0 deletions
diff --git a/lib/classes/StudipPDO.php b/lib/classes/StudipPDO.php new file mode 100644 index 0000000..77046f7 --- /dev/null +++ b/lib/classes/StudipPDO.php @@ -0,0 +1,385 @@ +<?php +/** + * StudipPDO.php - Stud.IP PDO class + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * @author Elmar Ludwig + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + */ + +/** + * This is a special variant of the standard PDO class that does + * not allow multiple statement execution. + */ +class StudipPDO extends PDO +{ + const PARAM_ARRAY = 100; + const PARAM_COLUMN = 101; + + // Counter for the queries sent to the database + public $query_count = 0; + + /** + * Verifies that the given SQL query only contains a single statement. + * + * @param string SQL statement to check + * @throws PDOException when the query contains multiple statements + */ + protected function verify($statement) + { + if (mb_strpos($statement, ';') !== false) { + if (preg_match('/;\s*\S/', self::replaceStrings($statement))) { + throw new PDOException('multiple statement execution not allowed'); + } + } + + // Count executed queries (this is placed here since this is the only + // method that is executed on every call to the database) + $this->query_count += 1; + } + + /** + * Replaces all string literals in the statement with placeholders. + * + * @param string SQL statement + * @return string modified SQL statement + */ + protected static function replaceStrings($statement) + { + $count = mb_substr_count($statement, '"') + mb_substr_count($statement, "'") + mb_substr_count($statement, '\\'); + + // use fast preg_replace() variant if possible + if ($count < 1000) { + $result = preg_replace('/"(""|\\\\.|[^\\\\"]+)*"|\'(\'\'|\\\\.|[^\\\\\']+)*\'/s', '?', $statement); + } + + if (!isset($result)) { + // split string into parts at quotes and backslash + $parts = preg_split('/([\\\\"\'])/', $statement, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + $result = ''; + $quote_chr = null; + + for ($part = current($parts); $part !== false; $part = next($parts)) { + // inside quotes, "" is ", '' is ' and \x is x + if ($quote_chr !== NULL) { + if ($part === $quote_chr) { + $part = next($parts); + + if ($part !== $quote_chr) { + // backtrack and terminate string + prev($parts); + $result .= '?'; + $quote_chr = NULL; + } + } else if ($part === '\\') { + // skip next part + next($parts); + } + } else if ($part === "'" || $part === '"') { + $quote_chr = $part; + $saved_pos = key($parts); + } else { + $result .= $part; + } + } + + if ($quote_chr !== NULL) { + // unterminated quote: copy to end of string + $result .= implode(array_slice($parts, $saved_pos)); + } + } + + return $result; + } + + /** + * Quotes the given value in a form appropriate for the type. + * If no explicit type is given, the value's PHP type is used. + * + * @param mixed $string PHP value to quote + * @param ?int $type parameter type (e.g. PDO::PARAM_STR) + * @return string|false quoted SQL string + */ + public function quote($string, $type = null): false|string + { + if (!isset($type)) { + if (is_null($string)) { + $type = PDO::PARAM_NULL; + } else if (is_bool($string)) { + $type = PDO::PARAM_BOOL; + } else if (is_int($string)) { + $type = PDO::PARAM_INT; + } else if (is_array($string)) { + $type = StudipPDO::PARAM_ARRAY; + } else { + $type = PDO::PARAM_STR; + } + } + + switch ($type) { + case PDO::PARAM_NULL: + return 'NULL'; + case PDO::PARAM_BOOL: + return $string ? '1' : '0'; + case PDO::PARAM_INT: + return (int) $string; + case StudipPDO::PARAM_ARRAY: + return is_array($string) && count($string) ? join(',', array_map([$this, 'quote'], $string)) : 'NULL'; + case StudipPDO::PARAM_COLUMN: + return preg_replace('/\\W/', '', $string); + default: + return parent::quote($string); + } + } + + /** + * Executes an SQL statement and returns the number of affected rows. + * + * @param string $statement SQL statement + */ + public function exec(string $statement): false|int + { + $this->verify($statement); + return parent::exec($statement); + } + + /** + * Executes an SQL statement, returning a result set as a statement object. + * + * @param string $statement SQL statement + * @param int $fetch_mode fetch mode (optional) + * @param mixed ...$fetch_args fetch mode parameters (see PDOStatement::setFetchMode) + * @return object PDOStatement object + */ + #[ReturnTypeWillChange] + public function query($statement, $fetch_mode = NULL, ...$fetch_args) + { + $this->verify($statement); + + if (isset($fetch_mode)) { + $stmt = parent::query($statement, $fetch_mode, ...$fetch_args); + } else { + $stmt = parent::query($statement); + } + + $studip_stmt = new StudipPDOStatement($this, $statement, []); + $studip_stmt->setStatement($stmt); + return $studip_stmt; + } + + /** + * Prepares a statement for execution and returns a statement object. + * + * @param string $statement SQL statement + * @param array $driver_options + * + * @return StudipPDOStatement + */ + #[ReturnTypeWillChange] + public function prepare($statement, $driver_options = []) + { + $this->verify($statement); + return new StudipPDOStatement($this, $statement, $driver_options); + } + + /** + * This method is intended only for use by the StudipPDOStatement class. + * + * @param string SQL statement + * @return object PDOStatement object + */ + public function prepareStatement($statement, $driver_options = []) + { + return parent::prepare($statement, $driver_options); + } + + /** + * Executes sql statement with given parameters, + * returns number of affected rows, use only for INSERT,UPDATE etc + * + * @param string $statement SQL statement to execute + * @param array $input_parameters parameters for statement + * @return integer number of affected rows + */ + public function execute($statement, $input_parameters = null) + { + $st = $this->prepare($statement); + $ok = $st->execute($input_parameters); + if ($ok === true) { + return $st->rowCount(); + } + return 0; + } + + /** + * Executes sql statement with given parameters, and fetch results + * as sequential array, each row as associative array + * optionally apply given callable on each row, with current row and key as parameter + * + * @param string $statement SQL statement to execute + * @param array $input_parameters parameters for statement + * @param callable $callable callable to be applied to each of the rows + * @return array result set as array of assoc arrays + */ + public function fetchAll($statement, $input_parameters = null, $callable = null) + { + $st = $this->prepare($statement); + $st->execute($input_parameters); + if (is_callable($callable)) { + $data = []; + $st->setFetchMode(PDO::FETCH_ASSOC); + foreach ($st as $key => $row) { + $data[$key] = call_user_func($callable, $row, $key); + } + } else { + $data = $st->fetchAll(PDO::FETCH_ASSOC); + } + return $data; + } + + /** + * Executes sql statement with given parameters, and fetch only + * the values from first column as sequential array + * optionally apply given callable on each row, with current value and key as parameter + * + * @see StudipPDOStatement::fetchFirst() + * @param string $statement SQL statement to execute + * @param array $input_parameters parameters for statement + * @param callable $callable callable to be applied to each of the rows + * @return array result set + */ + public function fetchFirst($statement, $input_parameters = null, $callable = null) + { + $st = $this->prepare($statement); + $st->execute($input_parameters); + $data = $st->fetchFirst(); + if (is_callable($callable)) { + foreach ($data as $key => $row) { + $data[$key] = call_user_func($callable, $row, $key); + } + } + return $data; + } + + /** + * Executes sql statement with given parameters, and fetch results + * as associative array, first columns value is used as a key, the others are grouped + * optionally apply given callable on each grouped row, with current row and key as parameter + * if no callable is given, 'current' is used, to return the first entry of the grouped row + * + * @see StudipPDOStatement::fetchGrouped() + * @param string $statement SQL statement to execute + * @param array $input_parameters parameters for statement + * @param callable $callable callable to be applied to each of the rows + * @return array result set + */ + public function fetchGrouped($statement, $input_parameters = null, $callable = null) + { + $st = $this->prepare($statement); + $st->execute($input_parameters); + $data = $st->fetchGrouped(PDO::FETCH_ASSOC, is_null($callable) ? 'current' : null); + if (is_callable($callable)) { + foreach ($data as $key => $row) { + $data[$key] = call_user_func($callable, $row, $key); + } + } + return $data; + } + + /** + * Executes sql statement with given parameters, and fetch results + * as associative array, first columns value is used as a key, the other one is grouped + * use only when selecting 2 columns + * optionally apply given callable on each grouped row, with current row and key as parameter + * + * @see StudipPDOStatement::fetchGroupedPairs() + * @param string $statement SQL statement to execute + * @param array $input_parameters parameters for statement + * @param callable $callable callable to be applied to each of the rows + * @return array result set + */ + public function fetchGroupedPairs($statement, $input_parameters = null, $callable = null) + { + $st = $this->prepare($statement); + $st->execute($input_parameters); + $data = $st->fetchGroupedPairs(); + if (is_callable($callable)) { + foreach ($data as $key => $row) { + $data[$key] = call_user_func($callable, $row, $key); + } + } + return $data; + } + + /** + * Executes sql statement with given parameters, and fetch results + * as associative array, first columns value is used as a key, the other one as the value + * use only when selecting 2 columns + * optionally apply given callable on each grouped row, with current row and key as parameter + * + * @see StudipPDOStatement::fetchGroupedPairs() + * @param string $statement SQL statement to execute + * @param array $input_parameters parameters for statement + * @param callable $callable callable to be applied to each of the rows + * @return array result set + */ + public function fetchPairs($statement, $input_parameters = null, $callable = null) + { + $st = $this->prepare($statement); + $st->execute($input_parameters); + $data = $st->fetchPairs(); + if (is_callable($callable)) { + foreach ($data as $key => $row) { + $data[$key] = call_user_func($callable, $row, $key); + } + } + return $data; + } + + /** + * Executes sql statement with given parameters, and fetch only the first row + * as associative array + * + * @see StudipPDOStatement::fetchOne() + * @param string $statement SQL statement to execute + * @param array $input_parameters parameters for statement + * @return array first row of result set + */ + public function fetchOne($statement, $input_parameters = null) + { + $st = $this->prepare($statement); + $st->execute($input_parameters); + return $st->fetchOne(); + } + + /** + * Executes sql statement with given parameters, and fetch only the value of one column + * third param denotes the column, zero indexed + * + * @param string $statement SQL statement to execute + * @param array $input_parameters parameters for statement + * @param integer $column number of column to fetch + * @return string value of chosen column + */ + public function fetchColumn($statement, $input_parameters = null, $column = 0) + { + $st = $this->prepare($statement); + $st->execute($input_parameters); + return $st->fetchColumn($column); + } + + /** + * Determine if the connected database is a MariaDB database. + * + * @return bool + */ + public function isMariaDB(): bool + { + return stripos($this->getAttribute(\PDO::ATTR_SERVER_VERSION), 'MariaDB') !== false; + } +} |
