[test] contact to setting modules

This commit is contained in:
Thanakorn S
2026-05-06 08:58:09 +07:00
parent 414f1cd81d
commit 6501d326ca
43 changed files with 4295 additions and 581 deletions
+39 -51
View File
@@ -16,17 +16,7 @@
* Balance summary → getWarehouseBalanceSummary
*
* Security: All SQL uses PDO prepared statements with bound parameters.
* Dynamic table names (td_stock_<warehouse>) are derived from DB-sourced
* warehouse names sanitised with preg_replace('/[^a-zA-Z0-9_]/', '', ...)
* before interpolation. Integer parameters (warehouse_id, company_id, limit)
* are explicitly cast to (int) before use in any SQL string fragment.
*
* Security fixes applied vs. previous version:
* - getRecentActivity: warehouse_name in SQL now uses $safe (was unescaped)
* - getExpiredStock: warehouse_id cast to (int) before SQL fragment injection;
* warehouse_name in UNION now uses $safe (was unescaped)
* - getRackLog: cross-DB join uses $mainDb injected at construction time
* - getRackOccupancy: (already safe — no dynamic identifiers)
* Dynamic stock table names are derived only from md_warehouse.id.
*/
class ReportManager
{
@@ -45,26 +35,26 @@ class ReportManager
// Private helpers
// ─────────────────────────────────────────────────────────────
/**
* Resolve the td_stock_<wh> table name for a warehouse, requiring active status.
*
* Returns null if the warehouse is not found or inactive.
* The warehouse_name is sanitised before use as a table suffix.
*
* @param int $warehouse_id The md_warehouse.id to resolve.
* @return string|null Sanitised table name, or null if not active/found.
*/
private function stockTableNameFromWarehouseId(int $warehouse_id): string
{
if ($warehouse_id <= 0) {
throw new Exception("Invalid warehouse id.");
}
return 'td_stock_' . $warehouse_id;
}
private function resolveWarehouseTable(int $warehouse_id): ?string
{
$sth = $this->pdo->prepare(
"SELECT warehouse_name FROM md_warehouse
"SELECT id FROM md_warehouse
WHERE company_id = :company_id AND id = :id AND status = 1"
);
$sth->execute([':company_id' => $this->companyId, ':id' => $warehouse_id]);
$name = $sth->fetchColumn();
if (!$name) return null;
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $name);
return "td_stock_{$safe}";
$id = $sth->fetchColumn();
if (!$id) return null;
return $this->stockTableNameFromWarehouseId((int)$id);
}
/**
@@ -569,9 +559,6 @@ class ReportManager
* Builds a UNION ALL across all td_stock_* tables to produce a unified
* activity feed ordered by date DESC. Used by the dashboard "recent activity" widget.
*
* Security fix: warehouse_name in UNION SQL now uses $safe (sanitised with
* preg_replace) instead of the raw warehouse_name string.
*
* @param int $limit Maximum number of transactions to return (default 10).
* @return array Activity rows with product_name, product_sku, warehouse_name,
* direction ('in'|'out'), qty, type, date.
@@ -587,18 +574,17 @@ class ReportManager
if (empty($warehouses)) return [];
// Security: use $safe (sanitised name) for both the table identifier
// and the literal warehouse_name string in the SELECT — never raw $wh['warehouse_name'].
$unions = implode("\nUNION ALL\n", array_map(
function ($wh) {
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $wh['warehouse_name']);
$cid = (int) $this->companyId;
$table = $this->stockTableNameFromWarehouseId((int)$wh['id']);
$cid = (int) $this->companyId;
$warehouse_name = $this->pdo->quote($wh['warehouse_name']);
return "SELECT s.date, s.product_sku, s.type,
ROUND(COALESCE(s.`in`, 0), 2) AS stock_in,
ROUND(COALESCE(s.`out`, 0), 2) AS stock_out,
p.product_name,
'{$safe}' AS warehouse_name
FROM `td_stock_{$safe}` s
{$warehouse_name} AS warehouse_name
FROM `{$table}` s
LEFT JOIN md_product p
ON p.company_id = s.company_id
AND p.sku = s.product_sku
@@ -721,7 +707,7 @@ class ReportManager
/**
* Return monthly stock_in and stock_out totals for a warehouse over the last 12 months.
*
* Queries the per-warehouse td_stock_<wh> table directly (not warehouse_balance)
* Queries the per-warehouse td_stock_<warehouse_id> table directly (not warehouse_balance)
* for per-warehouse granularity. Produces chart-ready arrays with month labels.
*
* @param int $warehouse_id The warehouse to query.
@@ -899,8 +885,6 @@ class ReportManager
*
* Security fix: $warehouse_id is now cast to (int) and the WHERE condition
* uses a bound parameter (:warehouse_id) instead of raw string interpolation.
* warehouse_name in UNION now uses $safe variable (sanitised) not raw $wh['warehouse_name'].
*
* @param int $warehouse_id Warehouse filter (0 = all warehouses).
* @return array Expiry-status stock items with product, lot, location, and days_remaining.
*/
@@ -928,10 +912,9 @@ class ReportManager
$cid = (int) $this->companyId; // cast before interpolation
// Security: use $safe (sanitised) for table identifier and literal warehouse name string
$unions = implode("\nUNION ALL\n", array_map(
function ($wh) use ($cid) {
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $wh['warehouse_name']);
$table = $this->stockTableNameFromWarehouseId((int)$wh['id']);
return "SELECT
s.id,
s.product_sku,
@@ -941,7 +924,7 @@ class ReportManager
s.rack,
ROUND(s.`in`, 2) AS quantity,
{$wh['id']} AS warehouse_id
FROM `td_stock_{$safe}` s
FROM `{$table}` s
WHERE s.company_id = {$cid}
AND s.type = 'in'
AND s.lot_number IS NOT NULL
@@ -1044,8 +1027,8 @@ class ReportManager
$rows = [];
foreach ($warehouses as $wh) {
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $wh['warehouse_name']);
$table = "td_stock_{$safe}";
$table = $this->stockTableNameFromWarehouseId((int)$wh['id']);
$warehouse_name = $this->pdo->quote($wh['warehouse_name']);
$sth = $this->pdo->prepare(
"SELECT
@@ -1059,7 +1042,7 @@ class ReportManager
ROUND(COALESCE(s.`out`, 0), 2) AS stock_out,
s.serial_number,
s.description,
'{$safe}' AS warehouse_name
{$warehouse_name} AS warehouse_name
FROM `{$table}` s
WHERE s.company_id = :company_id
AND s.product_sku = :product_sku
@@ -1109,8 +1092,7 @@ class ReportManager
$cid = (int) $this->companyId;
foreach ($wh_list as $wh) {
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $wh['warehouse_name']);
$table = "td_stock_{$safe}";
$table = $this->stockTableNameFromWarehouseId((int)$wh['id']);
$sth = $this->pdo->query(
"SELECT lot_number,
@@ -1195,7 +1177,10 @@ class ReportManager
{
if (!$rack_id) return [];
$main_db = preg_replace('/[^a-zA-Z0-9_]/', '', $this->mainDb);
$main_db = $this->mainDb;
if ($main_db === '' || !ctype_alnum(str_replace('_', '', $main_db))) {
throw new Exception("Invalid main database name.");
}
$sth = $this->pdo->prepare(
"SELECT
@@ -1287,7 +1272,7 @@ class ReportManager
}
/**
* Return paginated raw stock movement transactions from td_stock_<warehouse>
* Return paginated raw stock movement transactions from td_stock_<warehouse_id>
* within a date range (by month), with a balance brought forward.
*
* Each row is one individual transaction — no grouping. Full datetime is
@@ -1412,13 +1397,15 @@ class ReportManager
$bf_out = (float)($bf['bf_out'] ?? 0);
$bf_bal = round($bf_in - $bf_out, 2);
// Product name
// Product name + UOM
$sth = $this->pdo->prepare(
"SELECT product_name FROM md_product
"SELECT product_name, uom FROM md_product
WHERE company_id = :company_id AND sku = :sku LIMIT 1"
);
$sth->execute([':company_id' => $this->companyId, ':sku' => $product_sku]);
$product_name = $sth->fetchColumn() ?: $product_sku;
$product_row = $sth->fetch(PDO::FETCH_ASSOC) ?: [];
$product_name = $product_row['product_name'] ?: $product_sku;
$product_uom = $product_row['uom'] ?? '';
// All transactions for this SKU in range, sorted by date then id
$sth = $this->pdo->prepare(
@@ -1460,6 +1447,7 @@ class ReportManager
return [
'sku' => $product_sku,
'product_name' => $product_name,
'uom' => $product_uom,
'brought_forward' => [
'in' => $bf_in,
'out' => $bf_out,
@@ -1468,4 +1456,4 @@ class ReportManager
'rows' => $rows,
];
}
}
}