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.
This commit is contained in:
+40
-39
@@ -31,6 +31,40 @@ class db_statement extends PDOStatement {
|
||||
$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
|
||||
@@ -49,46 +83,13 @@ class db_statement extends PDOStatement {
|
||||
// null is preserved as-is so PDO can bind NULL columns correctly.
|
||||
$args = array_map(fn($v) => is_null($v) ? null : (string)$v, $args);
|
||||
|
||||
// escaping array
|
||||
// prevent store XSS
|
||||
// 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;
|
||||
|
||||
// decode the JSON data
|
||||
// set second parameter boolean TRUE for associative array output.
|
||||
$result = json_decode($item);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
// encode html for json
|
||||
$tmp = json_decode($item,true);
|
||||
foreach((array)$tmp as &$ii){
|
||||
|
||||
// Inner values may be arrays (nested JSON objects) — cast to string
|
||||
if (!is_string($ii)) {
|
||||
$ii = json_encode($ii);
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = json_decode($ii);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
// json inside json
|
||||
$tmpp = json_decode($ii,true);
|
||||
foreach ((array)$tmpp as &$iii) {
|
||||
$iii = htmlspecialchars($ii, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
$ii = json_encode($tmpp);
|
||||
}else{
|
||||
// string inside json
|
||||
$ii = htmlspecialchars($ii, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
}
|
||||
$item = json_encode($tmp);
|
||||
}else{
|
||||
// encode html for string
|
||||
$item = htmlspecialchars($item, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
$item = self::escapeValue($item);
|
||||
}
|
||||
unset($item);
|
||||
}
|
||||
return parent::execute($args);
|
||||
}
|
||||
@@ -96,11 +97,11 @@ class db_statement extends PDOStatement {
|
||||
}
|
||||
|
||||
//..................... PDO1 .....................//
|
||||
$pdo1 = new database($db_type.':host='.$db_server.';dbname='.$db_database.';charset=utf8', $db_user, $db_pass);
|
||||
$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=utf8', $db_user2, $db_pass2);
|
||||
$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
|
||||
|
||||
Reference in New Issue
Block a user