Enhance input validation, unicode handling, and sanitization across department goal criteria
- Add `validateAndSanitize` method to `goals_criteria` for input validation and defaulting incorrect values. - Ensure JSON encoding in `response` and database interactions respects Unicode (`JSON_UNESCAPED_UNICODE`). - Sanitize and validate input for `department_goals_o::add` and criteria usage. - Fix header character encoding in response (`Content-Type: application/json; charset=utf-8`). - Update criteria `label` sanitization with trimming, length limits, and safe character handling.
This commit is contained in:
@@ -23,7 +23,7 @@ class response implements response_i
|
||||
#[NoReturn] public function response(bool $success, mixed $data, int $status = null): void
|
||||
{
|
||||
global $DEBUG;
|
||||
header('Content-Type: application/json');
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
if ($status) {
|
||||
http_response_code($status);
|
||||
} else {
|
||||
@@ -52,7 +52,7 @@ class response implements response_i
|
||||
'data' => $data,
|
||||
'meta' => $this->meta,
|
||||
'includes' => $this->includes
|
||||
]);
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace goals\classes;
|
||||
|
||||
use classes\db;
|
||||
use goals\helpers\goals_criteria_type;
|
||||
use goals\helpers\goals_criteria_progress_alert_frequency;
|
||||
use goals\interfaces\goals_criteria_i;
|
||||
@@ -230,6 +231,7 @@ class goals_criteria implements goals_criteria_i
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
global /** @var db $db */ $db;
|
||||
// Normalize lists to identifiers only
|
||||
$users = $this->users?->listCustomerNumbers() ?? [];
|
||||
$departments = $this->departments?->listIDs() ?? [];
|
||||
@@ -243,7 +245,7 @@ class goals_criteria implements goals_criteria_i
|
||||
return [
|
||||
'type' => $this->type?->name ?? goals_criteria_type::NONE->name,
|
||||
'target' => $this->target ?? 0,
|
||||
'label' => $this->label,
|
||||
'label' => (string)$this->label,
|
||||
'start' => ($this->start instanceof \DateTimeInterface) ? $this->start->format(DATE_ATOM) : null,
|
||||
'end' => ($this->end instanceof \DateTimeInterface) ? $this->end->format(DATE_ATOM) : null,
|
||||
'users' => $users,
|
||||
@@ -335,4 +337,40 @@ class goals_criteria implements goals_criteria_i
|
||||
{
|
||||
return $this->getProgress();
|
||||
}
|
||||
|
||||
public function validateAndSanitize(): void
|
||||
{
|
||||
// Ensure target is non-negative
|
||||
if ($this->target < 0) {
|
||||
$this->target = 0;
|
||||
}
|
||||
|
||||
// Ensure timeframe is valid
|
||||
if (($this->start instanceof \DateTimeInterface) && ($this->end instanceof \DateTimeInterface)) {
|
||||
if ($this->end < $this->start) {
|
||||
$tmp = $this->start;
|
||||
$this->start = $this->end;
|
||||
$this->end = $tmp;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure unknown enum values are reset to defaults
|
||||
if (!in_array($this->type, goals_criteria_type::cases())) {
|
||||
$this->type = goals_criteria_type::NONE;
|
||||
}
|
||||
if (!in_array($this->progress_alert_frequency, goals_criteria_progress_alert_frequency::cases())) {
|
||||
$this->progress_alert_frequency = goals_criteria_progress_alert_frequency::NONE;
|
||||
}
|
||||
|
||||
// Sanitize label
|
||||
if (is_string($this->label)) {
|
||||
$label = trim($this->label);
|
||||
$label = strip_tags($label);
|
||||
$label = preg_replace('/\s+/', ' ', $label);
|
||||
if ($label !== null) {
|
||||
$label = mb_substr($label, 0, 255);
|
||||
$this->label = $label;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,11 +36,18 @@ class department_goals_o extends db
|
||||
*/
|
||||
public function add(users_o $created_by, array $departments, goals_criteria $criteria): department_goals_o
|
||||
{
|
||||
// Sanitize the input
|
||||
$created_by->requireSelected();
|
||||
foreach ($departments as $dept) {
|
||||
$dept->requireSelected();
|
||||
}
|
||||
// Validate and sanitize criteria object
|
||||
$criteria->validateAndSanitize();
|
||||
// Add the object
|
||||
$tmp_id = self::add_object([
|
||||
'created_by' => (int)$created_by->id,
|
||||
'departments' => array_map(fn($dept) => (int)$dept->id, $departments),
|
||||
'criteria' => $criteria->toArray(),
|
||||
'criteria' => (array)$criteria->toArray(),
|
||||
]);
|
||||
$this->id = $tmp_id;
|
||||
self::getObjectProperties();
|
||||
|
||||
@@ -116,10 +116,8 @@ class departmentGoalsRoute
|
||||
|
||||
// Parse criteria (preserve unicode characters like æ, ø, å)
|
||||
$criteria = goals_criteria::fromJson(json_encode($criteriaInput, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
// Create
|
||||
// Create the goal
|
||||
$goal = (new department_goals_o())->add($user, $departments, $criteria);
|
||||
|
||||
$response->success($goal->asArray(), 201);
|
||||
}, [
|
||||
'goals_department_create' => 'Create a new department goal'
|
||||
|
||||
@@ -1101,7 +1101,7 @@ trait db_object_t
|
||||
foreach ( $data as $key => $value ) {
|
||||
// JSON encode objects and arrays
|
||||
if (is_object($value) || is_array($value)) {
|
||||
$value = json_encode($value);
|
||||
$value = json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
if ($value === false) {
|
||||
throw new Exception('Failed to encode value for key: ' . $key . ' - ' . json_last_error_msg());
|
||||
}
|
||||
@@ -1226,7 +1226,7 @@ trait db_object_t
|
||||
foreach ( $data as $key => $value ) {
|
||||
// JSON encode objects and arrays
|
||||
if (is_object($value) || is_array($value)) {
|
||||
$value = json_encode($value);
|
||||
$value = json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
if ($value === false) {
|
||||
throw new Exception('Failed to encode value for key: ' . $key . ' - ' . json_last_error_msg());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user