leeds_backend/src/Libs/GuardLib.php
2026-01-29 18:28:39 -03:00

238 lines
5.2 KiB
PHP

<?php
declare(strict_types=1);
namespace Bass\Webclient\Libs;
class GuardLib
{
public static function requireJsonObject(mixed $data): array
{
// JSON precisa ser um objeto (array associativo)
if (!is_array($data)) {
return [
false,
[
'code' => 'INVALID_JSON',
'message' => 'JSON payload must be an object'
]
];
}
// Bloqueia array indexado []
if (array_is_list($data)) {
return [
false,
[
'code' => 'INVALID_JSON_STRUCTURE',
'message' => 'JSON payload must be an object, not a list'
]
];
}
// Payload vazio {}
if ($data === []) {
return [
false,
[
'code' => 'EMPTY_PAYLOAD',
'message' => 'JSON payload cannot be empty'
]
];
}
return [true, []];
}
public static function validateClientRequest(array $data): array
{
foreach (['name', 'email', 'company_name', 'sector', 'revenue'] as $field) {
if (!isset($data[$field]) || trim((string) $data[$field]) === '') {
return [
false,
[
'code' => 'MISSING_FIELD',
'field' => $field,
'message' => "Field {$field} is required"
]
];
}
}
if (!filter_var((string) $data['email'], FILTER_VALIDATE_EMAIL)) {
return [
false,
[
'code' => 'INVALID_EMAIL',
'message' => 'Invalid email'
]
];
}
if ((int) ($data['number_of_employees'] ?? 0) < 1) {
return [
false,
[
'code' => 'INVALID_EMPLOYEES',
'message' => 'Invalid number_of_employees'
]
];
}
return [true, []];
}
public static function allowOnlyFields(array $data, array $allowed): array
{
$extra = array_diff(array_keys($data), $allowed);
if (!empty($extra)) {
return [
false,
[
'code' => 'UNEXPECTED_FIELDS',
'fields' => array_values($extra),
'message' => 'Payload contains unexpected fields'
]
];
}
return [true, []];
}
public static function maxPayloadFields(array $data, int $limit = 15): array
{
if (count($data) > $limit) {
return [
false,
[
'code' => 'PAYLOAD_TOO_LARGE',
'message' => 'Too many fields in payload'
]
];
}
return [true, []];
}
public static function blockDangerousPatterns(array $data): array
{
$patterns = [
'/\$\(/', // command substitution
'/`/', // backticks
'/\|/', // pipe
'/&&|\|\|/', // logical chaining
'/--/', // SQL comment
'/;/' // statement break
];
foreach ($data as $key => $value) {
if (!is_scalar($value)) {
continue;
}
foreach ($patterns as $pattern) {
if (preg_match($pattern, (string) $value)) {
return [
false,
[
'code' => 'DANGEROUS_INPUT',
'field' => $key,
'message' => 'Suspicious input detected'
]
];
}
}
}
return [true, []];
}
public static function requiredBySchema(array $data, array $schema): array
{
foreach ($schema as $field => $rules) {
if (($rules['required'] ?? false) === true) {
if (!isset($data[$field]) || trim((string) $data[$field]) === '') {
return [
false,
[
'code' => 'MISSING_FIELD',
'field' => $field,
'message' => "Field {$field} is required"
]
];
}
}
}
return [true, []];
}
public static function validateBySchema(array $data, array $schema): array
{
foreach ($schema as $field => $rules) {
if (!isset($data[$field])) {
continue;
}
$value = $data[$field];
switch ($rules['type']) {
case 'string':
if (!is_scalar($value)) {
return self::typeError($field);
}
if (isset($rules['max']) && mb_strlen((string) $value) > $rules['max']) {
return self::limitError($field);
}
break;
case 'int':
if (!is_numeric($value)) {
return self::typeError($field);
}
if (isset($rules['min']) && (int) $value < $rules['min']) {
return self::limitError($field);
}
break;
case 'email':
if (!filter_var((string) $value, FILTER_VALIDATE_EMAIL)) {
return [
false,
[
'code' => 'INVALID_EMAIL',
'field' => $field,
'message' => 'Invalid email'
]
];
}
break;
}
}
return [true, []];
}
private static function typeError(string $field): array
{
return [
false,
[
'code' => 'INVALID_TYPE',
'field' => $field,
'message' => "Invalid type for {$field}"
]
];
}
private static function limitError(string $field): array
{
return [
false,
[
'code' => 'INVALID_VALUE',
'field' => $field,
'message' => "Invalid value for {$field}"
]
];
}
}