Enhance goals_criteria::fromJson with input sanitization, deduplication, and validation
- Add robust validation for JSON input, including error handling for malformed or invalid data. - Sanitize and validate all fields (`type`, `target`, `label`, `start`, `end`) with additional constraints like trimming, length limits, and proper date handling. - Implement deduplication for `users`, `departments`, and `products` to ensure unique and valid entries. - Ensure `end` date does not precede `start` by swapping when necessary.
This commit is contained in:
@@ -55,26 +55,68 @@ class goals_criteria implements goals_criteria_i
|
||||
public static function fromJson(string $json): goals_criteria
|
||||
{
|
||||
$data = json_decode($json, true);
|
||||
|
||||
if (!is_array($data) || json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new \InvalidArgumentException('Invalid JSON for goals criteria');
|
||||
}
|
||||
|
||||
$criteria = new goals_criteria();
|
||||
|
||||
// Sanitize type (ignore unknowns)
|
||||
if (isset($data['type'])) {
|
||||
$criteria->type = goals_criteria_type::tryFrom($data['type']);
|
||||
$type = goals_criteria_type::tryFrom((string)$data['type']);
|
||||
if ($type !== null) {
|
||||
$criteria->type = $type;
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize target: numeric and non-negative
|
||||
if (isset($data['target'])) {
|
||||
$criteria->target = $data['target'];
|
||||
if (is_numeric($data['target'])) {
|
||||
$target = (float)$data['target'];
|
||||
$criteria->target = max(0, $target);
|
||||
}
|
||||
}
|
||||
if (isset($data['label'])) {
|
||||
$criteria->label = is_string($data['label']) ? $data['label'] : null;
|
||||
|
||||
// Sanitize label: trim, strip tags, collapse whitespace, max length 255
|
||||
if (isset($data['label']) && is_string($data['label'])) {
|
||||
$label = trim($data['label']);
|
||||
$label = strip_tags($label);
|
||||
$label = preg_replace('/\s+/', ' ', $label);
|
||||
if ($label !== null) {
|
||||
$label = mb_substr($label, 0, 255);
|
||||
}
|
||||
$criteria->label = ($label === '') ? null : $label;
|
||||
}
|
||||
if (isset($data['start'])) {
|
||||
$criteria->start = new \DateTime($data['start']);
|
||||
|
||||
// Parse timeframe with validation
|
||||
if (isset($data['start']) && is_string($data['start'])) {
|
||||
try {
|
||||
$criteria->start = new \DateTime($data['start']);
|
||||
} catch (\Exception) {
|
||||
// keep default
|
||||
}
|
||||
}
|
||||
if (isset($data['end'])) {
|
||||
$criteria->end = new \DateTime($data['end']);
|
||||
if (isset($data['end']) && is_string($data['end'])) {
|
||||
try {
|
||||
$criteria->end = new \DateTime($data['end']);
|
||||
} catch (\Exception) {
|
||||
// keep default
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure end is not before start (swap if needed)
|
||||
if (($criteria->start instanceof \DateTimeInterface) && ($criteria->end instanceof \DateTimeInterface)) {
|
||||
if ($criteria->end < $criteria->start) {
|
||||
$tmp = $criteria->start;
|
||||
$criteria->start = $criteria->end;
|
||||
$criteria->end = $tmp;
|
||||
}
|
||||
}
|
||||
// Parse users (expects array of customer_numbers or objects with customer_number)
|
||||
if (isset($data['users']) && is_array($data['users'])) {
|
||||
$users = array_map(function ($item) {
|
||||
$seen = [];
|
||||
$users = array_map(function ($item) use (&$seen) {
|
||||
$customerNumber = null;
|
||||
if (is_array($item)) {
|
||||
if (isset($item['customer_number'])) {
|
||||
@@ -85,9 +127,13 @@ class goals_criteria implements goals_criteria_i
|
||||
} elseif (is_numeric($item)) {
|
||||
$customerNumber = (int)$item;
|
||||
}
|
||||
if ($customerNumber === null) {
|
||||
if ($customerNumber === null || $customerNumber <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (isset($seen[$customerNumber])) {
|
||||
return null; // dedupe early
|
||||
}
|
||||
$seen[$customerNumber] = true;
|
||||
return (new \objects\users_o())->getUserByCustomerNumber($customerNumber);
|
||||
}, $data['users']);
|
||||
// Filter out nulls in case of malformed entries
|
||||
@@ -97,7 +143,8 @@ class goals_criteria implements goals_criteria_i
|
||||
|
||||
// Parse departments (expects array of IDs or objects with id)
|
||||
if (isset($data['departments']) && is_array($data['departments'])) {
|
||||
$departments = array_map(function ($item) {
|
||||
$seenDept = [];
|
||||
$departments = array_map(function ($item) use (&$seenDept) {
|
||||
$id = null;
|
||||
if (is_array($item)) {
|
||||
if (isset($item['id'])) {
|
||||
@@ -106,9 +153,13 @@ class goals_criteria implements goals_criteria_i
|
||||
} elseif (is_numeric($item)) {
|
||||
$id = (int)$item;
|
||||
}
|
||||
if ($id === null) {
|
||||
if ($id === null || $id <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (isset($seenDept[$id])) {
|
||||
return null;
|
||||
}
|
||||
$seenDept[$id] = true;
|
||||
return (new \objects\departments_o())->select($id);
|
||||
}, $data['departments']);
|
||||
$departments = array_values(array_filter($departments));
|
||||
@@ -117,7 +168,8 @@ class goals_criteria implements goals_criteria_i
|
||||
|
||||
// Parse products (expects array of IDs or objects with id)
|
||||
if (isset($data['products']) && is_array($data['products'])) {
|
||||
$products = array_map(function ($item) {
|
||||
$seenProd = [];
|
||||
$products = array_map(function ($item) use (&$seenProd) {
|
||||
$id = null;
|
||||
if (is_array($item)) {
|
||||
if (isset($item['id'])) {
|
||||
@@ -126,9 +178,13 @@ class goals_criteria implements goals_criteria_i
|
||||
} elseif (is_numeric($item)) {
|
||||
$id = (int)$item;
|
||||
}
|
||||
if ($id === null) {
|
||||
if ($id === null || $id <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (isset($seenProd[$id])) {
|
||||
return null;
|
||||
}
|
||||
$seenProd[$id] = true;
|
||||
return (new \objects\products_o())->select($id);
|
||||
}, $data['products']);
|
||||
$products = array_values(array_filter($products));
|
||||
|
||||
Reference in New Issue
Block a user