setAttribute(PDO::ATTR_STATEMENT_CLASS, array('db_statement', array($this))); } public function last_query() { return $this->query; } } /** overide native PDO statement */ // for XSS protection class db_statement extends PDOStatement { protected $pdo; protected function __construct($pdo) { $this->pdo = $pdo; } // double_encode is off so text that is loaded and saved again is not escaped // a second time (" becoming "), and ENT_SUBSTITUTE keeps a value // with a broken byte sequence instead of silently storing an empty string. const ESCAPE_FLAGS = ENT_QUOTES | ENT_SUBSTITUTE; private static function escapeString(string $value): string { return htmlspecialchars($value, self::ESCAPE_FLAGS, 'UTF-8', false); } private static function escapeTree($node) { if (is_string($node)) return self::escapeString($node); if (!is_array($node)) return $node; $out = []; foreach ($node as $k => $v) { $out[is_string($k) ? self::escapeString($k) : $k] = self::escapeTree($v); } return $out; } public static function escapeValue(string $item): string { $first = $item[0] ?? ''; if ($first === '{' || $first === '[') { $tree = json_decode($item, true); if (is_array($tree)) { $flags = JSON_PRESERVE_ZERO_FRACTION; // An empty {} must not come back as []. if ($tree === [] ) return $item; $encoded = json_encode(self::escapeTree($tree), $flags); if ($encoded !== false) return $encoded; } } return self::escapeString($item); } // PDOStatement::execute() is declared ?array $params = null : bool. This // override deliberately accepts a looser signature so callers may pass // positional arguments (see func_get_args() below), so the tightened return // type is opted out of rather than the call sites being changed. #[\ReturnTypeWillChange] public function execute($args = null) { // Perform logging here. PDO object is accessible // from $this->pdo. if (!is_array($args)) { $args = func_get_args(); }else{ // Cast all values to string before XSS processing. // json_decode requires a string — integers, booleans, and nulls // passed as bound parameters would otherwise cause a TypeError. // null is preserved as-is so PDO can bind NULL columns correctly. $args = array_map(fn($v) => is_null($v) ? null : (string)$v, $args); // Escape on the way in, to prevent stored XSS. Values holding a JSON // object/array are escaped string by string so they stay valid JSON. foreach($args as &$item){ if (is_null($item)) continue; $item = self::escapeValue($item); } unset($item); } return parent::execute($args); } } //..................... PDO1 .....................// $pdo1 = new database($db_type.':host='.$db_server.';dbname='.$db_database.';charset=utf8mb4', $db_user, $db_pass); $pdo1->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); //..................... PDO2 .....................// $pdo2 = new database($db_type2.':host='.$db_server2.';dbname='.$db_database2.';charset=utf8mb4', $db_user2, $db_pass2); $pdo2->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // Pin both connections to the application timezone, so MySQL NOW() and PHP // date() agree no matter how the database server itself is configured. Queries // mix the two freely (rows written with NOW(), others with date()), and a // mismatch shows up as timestamps hours away from the real clock. foreach ([$pdo1, $pdo2] as $pdo_tz) { try { $pdo_tz->exec("SET time_zone = '" . APP_TIMEZONE_OFFSET . "'"); } catch (PDOException $e) { // A server that refuses the offset keeps its own zone — no worse than // before this call existed, and not a reason to fail the request. } }