[test] contact to setting modules
This commit is contained in:
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user