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.
118 lines
4.1 KiB
PHP
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 (" becoming &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.
|
|
}
|
|
} |