Files
wms-app/app/dbconn.php
T
Thanakorn c89b28da4c Fix QA review findings: server-side validation, notes encoding, dashboard totals
Validate document lines on the server and recompute their totals, store notes with quotes/markup/emoji (utf8mb4, idempotent escaping, decode in form fields), exclude transfers from company-wide stock in/out, count revenue from confirmed orders only, one low-stock rule everywhere, list unapproved lots, natural bin sort, stable order/PO sort, status tiles that add up.
2026-09-19 10:58:42 +07:00

118 lines
4.1 KiB
PHP

<?php
// Apply the configured application timezone before anything formats or stores a
// date. config.php (loaded by the caller) supplies $time_zone.
require_once __DIR__ . '/assets/utils/timezone.php';
// db connection
/** overide native PDO function */
class database extends PDO {
protected $query;
public function __construct($dsn, $username = '', $password = '', $driver_options = array()) {
parent::__construct($dsn, $username, $password, $driver_options);
$this->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 (&quot; becoming &amp;quot;), 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.
}
}