aboutsummaryrefslogtreecommitdiff
path: root/lib/classes/StudipPDO.php
diff options
context:
space:
mode:
authorPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
committerPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
commit4459dd7917f4d1c34f40bb68f0e991e9c3d53e4c (patch)
tree5c07151ae61276d334e88f6309c30d439a85c12e /lib/classes/StudipPDO.php
parentda0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff)
parent97a188592c679890a25c37ab78463add76a52ff7 (diff)
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'lib/classes/StudipPDO.php')
-rw-r--r--lib/classes/StudipPDO.php385
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;
+ }
+}