forked from Public/pics
		
	
		
			
				
	
	
		
			456 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			456 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
/*****************************************************************************
 | 
						|
 * Database.php
 | 
						|
 * Contains model class Database.
 | 
						|
 *
 | 
						|
 * Kabuki CMS (C) 2013-2025, Aaron van Geffen
 | 
						|
 *****************************************************************************/
 | 
						|
 | 
						|
class Database
 | 
						|
{
 | 
						|
	private $connection;
 | 
						|
	private $query_count = 0;
 | 
						|
	private $logged_queries = [];
 | 
						|
 | 
						|
	public function __construct($host, $user, $password, $name)
 | 
						|
	{
 | 
						|
		try
 | 
						|
		{
 | 
						|
			$this->connection = new PDO("mysql:host=$host;dbname=$name;charset=utf8mb4", $user, $password, [
 | 
						|
				PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
 | 
						|
				PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 | 
						|
				PDO::ATTR_EMULATE_PREPARES => false,
 | 
						|
			]);
 | 
						|
		}
 | 
						|
		// Give up if we have a connection error.
 | 
						|
		catch (PDOException $e)
 | 
						|
		{
 | 
						|
			http_response_code(503);
 | 
						|
			echo '<h2>Database Connection Problems</h2><p>Our software could not connect to the database. We apologise for any inconvenience and ask you to check back later.</p>';
 | 
						|
			exit;
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	public function getQueryCount()
 | 
						|
	{
 | 
						|
		return $this->query_count;
 | 
						|
	}
 | 
						|
 | 
						|
	public function getLoggedQueries()
 | 
						|
	{
 | 
						|
		return $this->logged_queries;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Fetches a row from a given statement/recordset, using field names as keys.
 | 
						|
	 */
 | 
						|
	public function fetchAssoc($stmt)
 | 
						|
	{
 | 
						|
		return $stmt->fetch(PDO::FETCH_ASSOC);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Fetches a row from a given statement/recordset, encapsulating into an object.
 | 
						|
	 */
 | 
						|
	public function fetchObject($stmt, $class)
 | 
						|
	{
 | 
						|
		return $stmt->fetchObject($class);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Fetches a row from a given statement/recordset, using numeric keys.
 | 
						|
	 */
 | 
						|
	public function fetchNum($stmt)
 | 
						|
	{
 | 
						|
		return $stmt->fetch(PDO::FETCH_NUM);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Destroys a given statement/recordset.
 | 
						|
	 */
 | 
						|
	public function free($stmt)
 | 
						|
	{
 | 
						|
		return $stmt->closeCursor();
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Returns the amount of rows in a given statement/recordset.
 | 
						|
	 */
 | 
						|
	public function rowCount($stmt)
 | 
						|
	{
 | 
						|
		return $stmt->rowCount();
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Returns the amount of fields in a given statement/recordset.
 | 
						|
	 */
 | 
						|
	public function columnCount($stmt)
 | 
						|
	{
 | 
						|
		return $stmt->columnCount();
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Returns the id of the row created by a previous query.
 | 
						|
	 */
 | 
						|
	public function insertId($name = null)
 | 
						|
	{
 | 
						|
		return $this->connection->lastInsertId($name);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Start a transaction.
 | 
						|
	 */
 | 
						|
	public function beginTransaction()
 | 
						|
	{
 | 
						|
		return $this->connection->beginTransaction();
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Rollback changes in a transaction.
 | 
						|
	 */
 | 
						|
	public function rollback()
 | 
						|
	{
 | 
						|
		return $this->connection->rollBack();
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Commit changes in a transaction.
 | 
						|
	 */
 | 
						|
	public function commit()
 | 
						|
	{
 | 
						|
		return $this->connection->commit();
 | 
						|
	}
 | 
						|
 | 
						|
	private function expandPlaceholders($db_string, array &$db_values)
 | 
						|
	{
 | 
						|
		foreach ($db_values as $key => &$value)
 | 
						|
		{
 | 
						|
			if (str_contains($db_string, ':' . $key))
 | 
						|
			{
 | 
						|
				if (is_array($value))
 | 
						|
				{
 | 
						|
					throw new UnexpectedValueException('Array ' . $key .
 | 
						|
						' is used as a scalar placeholder. Did you mean to use \'@\' instead?');
 | 
						|
				}
 | 
						|
 | 
						|
				// Prepare date/time values
 | 
						|
				if (is_a($value, 'DateTime'))
 | 
						|
				{
 | 
						|
					$value = $value->format('Y-m-d H:i:s');
 | 
						|
				}
 | 
						|
			}
 | 
						|
			elseif (str_contains($db_string, '@' . $key))
 | 
						|
			{
 | 
						|
				if (!is_array($value))
 | 
						|
				{
 | 
						|
					throw new UnexpectedValueException('Scalar value ' . $key .
 | 
						|
						' is used as an array placeholder. Did you mean to use \':\' instead?');
 | 
						|
				}
 | 
						|
 | 
						|
				// Create placeholders for all array elements
 | 
						|
				$placeholders = array_map(fn($num) => ':' . $key . $num, range(0, count($value) - 1));
 | 
						|
				$db_string = str_replace('@' . $key, implode(', ', $placeholders), $db_string);
 | 
						|
			}
 | 
						|
			else
 | 
						|
			{
 | 
						|
				// throw new Exception('Warning: unused key in query: ' . $key);
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		return $db_string;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Escapes and quotes a string using values passed, and executes the query.
 | 
						|
	 */
 | 
						|
	public function query($db_string, array $db_values = []): PDOStatement
 | 
						|
	{
 | 
						|
		// One more query...
 | 
						|
		$this->query_count++;
 | 
						|
 | 
						|
		// Error out if hardcoded strings are detected
 | 
						|
		if (strpos($db_string, '\'') !== false)
 | 
						|
			throw new UnexpectedValueException('Hack attempt: illegal character (\') used in query.');
 | 
						|
 | 
						|
		if (defined('DB_LOG_QUERIES') && DB_LOG_QUERIES)
 | 
						|
			$this->logged_queries[] = $db_string;
 | 
						|
 | 
						|
		try
 | 
						|
		{
 | 
						|
			// Preprocessing/checks: prepare any arrays for binding
 | 
						|
			$db_string = $this->expandPlaceholders($db_string, $db_values);
 | 
						|
 | 
						|
			// Prepare query for execution
 | 
						|
			$statement = $this->connection->prepare($db_string);
 | 
						|
 | 
						|
			// Bind parameters... the hard way, due to a limit/offset hack.
 | 
						|
			// NB: bindParam binds by reference, hence &$value here.
 | 
						|
			foreach ($db_values as $key => &$value)
 | 
						|
			{
 | 
						|
				// Assumption: both scalar and array values are preprocessed to use named ':' placeholders
 | 
						|
				if (!str_contains($db_string, ':' . $key))
 | 
						|
					continue;
 | 
						|
 | 
						|
				if (!is_array($value))
 | 
						|
				{
 | 
						|
					$statement->bindParam(':' . $key, $value);
 | 
						|
					continue;
 | 
						|
				}
 | 
						|
 | 
						|
				foreach (array_values($value) as $num => &$element)
 | 
						|
				{
 | 
						|
					$statement->bindParam(':' . $key . $num, $element);
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			$statement->execute();
 | 
						|
			return $statement;
 | 
						|
		}
 | 
						|
		catch (PDOException $e)
 | 
						|
		{
 | 
						|
			ob_start();
 | 
						|
 | 
						|
			$debug = ob_get_clean();
 | 
						|
 | 
						|
			throw new Exception($e->getMessage() . "\n" . var_export($e->errorInfo, true) . "\n" . var_export($db_values, true));
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Executes a query, returning an object of the row it returns.
 | 
						|
	 */
 | 
						|
	public function queryObject($class, $db_string, $db_values = [])
 | 
						|
	{
 | 
						|
		$res = $this->query($db_string, $db_values);
 | 
						|
 | 
						|
		if (!$res || $this->rowCount($res) === 0)
 | 
						|
			return null;
 | 
						|
 | 
						|
		$object = $this->fetchObject($res, $class);
 | 
						|
		$this->free($res);
 | 
						|
 | 
						|
		return $object;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Executes a query, returning an array of objects of all the rows returns.
 | 
						|
	 */
 | 
						|
	public function queryObjects($class, $db_string, $db_values = [])
 | 
						|
	{
 | 
						|
		$res = $this->query($db_string, $db_values);
 | 
						|
 | 
						|
		if (!$res || $this->rowCount($res) === 0)
 | 
						|
			return [];
 | 
						|
 | 
						|
		$rows = [];
 | 
						|
		while ($object = $this->fetchObject($res, $class))
 | 
						|
			$rows[] = $object;
 | 
						|
 | 
						|
		$this->free($res);
 | 
						|
 | 
						|
		return $rows;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Executes a query, returning an array of all the rows it returns.
 | 
						|
	 */
 | 
						|
	public function queryRow($db_string, array $db_values = [])
 | 
						|
	{
 | 
						|
		$res = $this->query($db_string, $db_values);
 | 
						|
 | 
						|
		if ($this->rowCount($res) === 0)
 | 
						|
			return [];
 | 
						|
 | 
						|
		$row = $this->fetchNum($res);
 | 
						|
		$this->free($res);
 | 
						|
 | 
						|
		return $row;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Executes a query, returning an array of all the rows it returns.
 | 
						|
	 */
 | 
						|
	public function queryRows($db_string, array $db_values = [])
 | 
						|
	{
 | 
						|
		$res = $this->query($db_string, $db_values);
 | 
						|
 | 
						|
		if ($this->rowCount($res) === 0)
 | 
						|
			return [];
 | 
						|
 | 
						|
		$rows = [];
 | 
						|
		while ($row = $this->fetchNum($res))
 | 
						|
			$rows[] = $row;
 | 
						|
 | 
						|
		$this->free($res);
 | 
						|
 | 
						|
		return $rows;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Executes a query, returning an array of all the rows it returns.
 | 
						|
	 */
 | 
						|
	public function queryPair($db_string, array $db_values = [])
 | 
						|
	{
 | 
						|
		$res = $this->query($db_string, $db_values);
 | 
						|
 | 
						|
		if ($this->rowCount($res) === 0)
 | 
						|
			return [];
 | 
						|
 | 
						|
		$rows = [];
 | 
						|
		while ($row = $this->fetchNum($res))
 | 
						|
			$rows[$row[0]] = $row[1];
 | 
						|
 | 
						|
		$this->free($res);
 | 
						|
 | 
						|
		return $rows;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Executes a query, returning an array of all the rows it returns.
 | 
						|
	 */
 | 
						|
	public function queryPairs($db_string, $db_values = array())
 | 
						|
	{
 | 
						|
		$res = $this->query($db_string, $db_values);
 | 
						|
 | 
						|
		if (!$res || $this->rowCount($res) === 0)
 | 
						|
			return [];
 | 
						|
 | 
						|
		$rows = [];
 | 
						|
		while ($row = $this->fetchAssoc($res))
 | 
						|
		{
 | 
						|
			$key_value = reset($row);
 | 
						|
			$rows[$key_value] = $row;
 | 
						|
		}
 | 
						|
 | 
						|
		$this->free($res);
 | 
						|
 | 
						|
		return $rows;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Executes a query, returning an associative array of all the rows it returns.
 | 
						|
	 */
 | 
						|
	public function queryAssoc($db_string, array $db_values = [])
 | 
						|
	{
 | 
						|
		$res = $this->query($db_string, $db_values);
 | 
						|
 | 
						|
		if ($this->rowCount($res) === 0)
 | 
						|
			return [];
 | 
						|
 | 
						|
		$row = $this->fetchAssoc($res);
 | 
						|
		$this->free($res);
 | 
						|
 | 
						|
		return $row;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Executes a query, returning an associative array of all the rows it returns.
 | 
						|
	 */
 | 
						|
	public function queryAssocs($db_string, array $db_values = [])
 | 
						|
	{
 | 
						|
		$res = $this->query($db_string, $db_values);
 | 
						|
 | 
						|
		if ($this->rowCount($res) === 0)
 | 
						|
			return [];
 | 
						|
 | 
						|
		$rows = [];
 | 
						|
		while ($row = $this->fetchAssoc($res))
 | 
						|
			$rows[] = $row;
 | 
						|
 | 
						|
		$this->free($res);
 | 
						|
 | 
						|
		return $rows;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Executes a query, returning the first value of the first row.
 | 
						|
	 */
 | 
						|
	public function queryValue($db_string, array $db_values = [])
 | 
						|
	{
 | 
						|
		$res = $this->query($db_string, $db_values);
 | 
						|
 | 
						|
		// If this happens, you're doing it wrong.
 | 
						|
		if ($this->rowCount($res) === 0)
 | 
						|
			return null;
 | 
						|
 | 
						|
		list($value) = $this->fetchNum($res);
 | 
						|
		$this->free($res);
 | 
						|
 | 
						|
		return $value;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Executes a query, returning an array of the first value of each row.
 | 
						|
	 */
 | 
						|
	public function queryValues($db_string, array $db_values = [])
 | 
						|
	{
 | 
						|
		$res = $this->query($db_string, $db_values);
 | 
						|
 | 
						|
		if ($this->rowCount($res) === 0)
 | 
						|
			return [];
 | 
						|
 | 
						|
		$rows = [];
 | 
						|
		while ($row = $this->fetchNum($res))
 | 
						|
			$rows[] = $row[0];
 | 
						|
 | 
						|
		$this->free($res);
 | 
						|
 | 
						|
		return $rows;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * This function can be used to insert data into the database in a secure way.
 | 
						|
	 */
 | 
						|
	public function insert($method, $table, $columns, $data)
 | 
						|
	{
 | 
						|
		// With nothing to insert, simply return.
 | 
						|
		if (empty($data))
 | 
						|
			return;
 | 
						|
 | 
						|
		// Inserting data as a single row can be done as a single array.
 | 
						|
		if (!is_array($data[array_rand($data)]))
 | 
						|
			$data = [$data];
 | 
						|
 | 
						|
		// Determine the method of insertion.
 | 
						|
		$method = $method == 'replace' ? 'REPLACE' : ($method == 'ignore' ? 'INSERT IGNORE' : 'INSERT');
 | 
						|
 | 
						|
		// What columns are we inserting?
 | 
						|
		$columns = array_keys($data[0]);
 | 
						|
 | 
						|
		// Start building the query.
 | 
						|
		$db_string = $method . ' INTO ' . $table . ' (' . implode(',', $columns) . ') VALUES ';
 | 
						|
 | 
						|
		// Create the mold for a single row insert.
 | 
						|
		$placeholders = '(' . substr(str_repeat('?, ', count($columns)), 0, -2) . '), ';
 | 
						|
 | 
						|
		// Append it for every row we're to insert.
 | 
						|
		$values = [];
 | 
						|
		foreach ($data as $row)
 | 
						|
		{
 | 
						|
			$values = array_merge($values, array_values($row));
 | 
						|
			$db_string .= $placeholders;
 | 
						|
		}
 | 
						|
 | 
						|
		// Get rid of the tailing comma.
 | 
						|
		$db_string = substr($db_string, 0, -2);
 | 
						|
 | 
						|
		// Prepare for your impending demise!
 | 
						|
		$statement = $this->connection->prepare($db_string);
 | 
						|
 | 
						|
		// Bind parameters... the hard way, due to a limit/offset hack.
 | 
						|
		foreach ($values as $key => $value)
 | 
						|
			$statement->bindValue($key + 1, $values[$key]);
 | 
						|
 | 
						|
		// Handle errors.
 | 
						|
		try
 | 
						|
		{
 | 
						|
			$statement->execute();
 | 
						|
			return $statement;
 | 
						|
		}
 | 
						|
		catch (PDOException $e)
 | 
						|
		{
 | 
						|
			throw new Exception($e->getMessage() . '<br><br>' . $db_string . '<br><br>' . print_r($values, true));
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 |