driver = $driver; try { if ($driver === 'sqlite') { $this->connection = new PDO("sqlite:" . $options['file'], null, null, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]); $this->connection->exec('PRAGMA journal_mode=WAL'); $this->registerSQLiteFunctions(); } else { $this->connection = new PDO( "mysql:host={$options['host']};dbname={$options['name']};charset=utf8mb4", $options['user'], $options['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 '

Database Connection Problems

Our software could not connect to the database. We apologise for any inconvenience and ask you to check back later.

'; exit; } } private function registerSQLiteFunctions() { $pdo = $this->connection; $pdo->sqliteCreateFunction('CONCAT', function () { return implode('', func_get_args()); }, -1); $pdo->sqliteCreateFunction('IF', function ($cond, $t, $f) { return $cond ? $t : $f; }, 3); $pdo->sqliteCreateFunction('FROM_UNIXTIME', function ($ts) { return date('Y-m-d H:i:s', $ts); }, 1); $pdo->sqliteCreateFunction('UNIX_TIMESTAMP', function () { return time(); }, 0); $pdo->sqliteCreateFunction('CURRENT_TIMESTAMP', function () { return date('Y-m-d H:i:s'); }, 0); } public function getDriver() { return $this->driver; } private function rewriteForSQLite($sql) { // REPLACE INTO → INSERT OR REPLACE INTO $sql = preg_replace('/\bREPLACE\s+INTO\b/i', 'INSERT OR REPLACE INTO', $sql); // INSERT IGNORE INTO → INSERT OR IGNORE INTO $sql = preg_replace('/\bINSERT\s+IGNORE\s+INTO\b/i', 'INSERT OR IGNORE INTO', $sql); // LIMIT :offset, :limit → LIMIT :limit OFFSET :offset $sql = preg_replace( '/\bLIMIT\s+:offset\s*,\s*:limit\b/i', 'LIMIT :limit OFFSET :offset', $sql ); return $sql; } 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); // SQLite query rewriting if ($this->driver === 'sqlite') $db_string = $this->rewriteForSQLite($db_string); // 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); $object = $this->fetchObject($res, $class); $this->free($res); return $object ?: null; } /** * 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); $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); $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); $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); $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); $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); $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); $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); $row = $this->fetchNum($res); $this->free($res); if (!$row) return null; return $row[0]; } /** * 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); $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. if ($this->driver === 'sqlite') $method = $method == 'replace' ? 'INSERT OR REPLACE' : ($method == 'ignore' ? 'INSERT OR IGNORE' : 'INSERT'); else $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() . '

' . $db_string . '

' . print_r($values, true)); } } }