Centralizes url canonicalization and normalization
parent
b88b2ce8a5
commit
8dc0b526ab
|
|
@ -0,0 +1,252 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared URL canonicalization utilities.
|
||||||
|
*
|
||||||
|
* Provides query normalization/compare helpers plus a policy-based builder so
|
||||||
|
* controllers can define canonical query contracts declaratively.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize query payload recursively for stable comparisons.
|
||||||
|
*
|
||||||
|
* - Sort associative keys.
|
||||||
|
* - Re-index list arrays.
|
||||||
|
* - Drop null values.
|
||||||
|
* - Convert scalar values to strings so GET payloads and canonical payloads
|
||||||
|
* compare by value rather than PHP type.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $query
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
function app_url_normalize_query(array $query): array
|
||||||
|
{
|
||||||
|
$normalized = [];
|
||||||
|
|
||||||
|
foreach ($query as $key => $value) {
|
||||||
|
if ($value === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($value)) {
|
||||||
|
$child = app_url_normalize_query($value);
|
||||||
|
$normalized[$key] = array_is_list($value)
|
||||||
|
? array_values($child)
|
||||||
|
: $child;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_bool($value)) {
|
||||||
|
$normalized[$key] = $value ? '1' : '0';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized[$key] = (string)$value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!array_is_list($normalized)) {
|
||||||
|
ksort($normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two query arrays after canonical normalization.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $left
|
||||||
|
* @param array<string,mixed> $right
|
||||||
|
*/
|
||||||
|
function app_url_queries_match(array $left, array $right): bool
|
||||||
|
{
|
||||||
|
return app_url_normalize_query($left) === app_url_normalize_query($right);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an internal app URL from a canonical query payload.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $query
|
||||||
|
*/
|
||||||
|
function app_url_build_internal(string $appRoot, array $query): string
|
||||||
|
{
|
||||||
|
return rtrim($appRoot !== '' ? $appRoot : '/', '/?&') . '/?' . http_build_query($query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to canonical query URL when current and canonical payloads differ.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $currentQuery
|
||||||
|
* @param array<string,mixed> $canonicalQuery
|
||||||
|
*/
|
||||||
|
function app_url_redirect_to_canonical_query(string $appRoot, array $currentQuery, array $canonicalQuery): void
|
||||||
|
{
|
||||||
|
if (!app_url_queries_match($currentQuery, $canonicalQuery)) {
|
||||||
|
header('Location: ' . app_url_build_internal($appRoot, $canonicalQuery));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve one canonical value from a policy rule.
|
||||||
|
*
|
||||||
|
* Supported rule types:
|
||||||
|
* - literal: fixed `value`
|
||||||
|
* - string: trimmed scalar string
|
||||||
|
* - int: validated integer with optional `min` / `max`
|
||||||
|
* - enum: trimmed scalar string constrained by `allowed`
|
||||||
|
* - bool_flag: includes `value_true` only when request value is truthy
|
||||||
|
* - string_list: trims and filters array items
|
||||||
|
*
|
||||||
|
* @param string $targetKey
|
||||||
|
* @param array<string,mixed> $rule
|
||||||
|
* @param array<string,mixed> $sourceQuery
|
||||||
|
* @return mixed|null
|
||||||
|
*/
|
||||||
|
function app_url_policy_value(string $targetKey, array $rule, array $sourceQuery)
|
||||||
|
{
|
||||||
|
$type = (string)($rule['type'] ?? 'string');
|
||||||
|
$sourceKey = (string)($rule['source'] ?? $targetKey);
|
||||||
|
|
||||||
|
if ($type === 'literal') {
|
||||||
|
return $rule['value'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasSourceValue = array_key_exists($sourceKey, $sourceQuery);
|
||||||
|
$rawValue = $hasSourceValue ? $sourceQuery[$sourceKey] : null;
|
||||||
|
|
||||||
|
if (!$hasSourceValue) {
|
||||||
|
return $rule['default'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'string') {
|
||||||
|
if (is_array($rawValue)) {
|
||||||
|
return $rule['default'] ?? null;
|
||||||
|
}
|
||||||
|
$value = (string)$rawValue;
|
||||||
|
if (($rule['trim'] ?? true) === true) {
|
||||||
|
$value = trim($value);
|
||||||
|
}
|
||||||
|
if ($value === '' && !($rule['allow_empty'] ?? false)) {
|
||||||
|
return $rule['default'] ?? null;
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'int') {
|
||||||
|
if (is_array($rawValue)) {
|
||||||
|
return $rule['default'] ?? null;
|
||||||
|
}
|
||||||
|
$candidate = trim((string)$rawValue);
|
||||||
|
if ($candidate === '' || filter_var($candidate, FILTER_VALIDATE_INT) === false) {
|
||||||
|
return $rule['default'] ?? null;
|
||||||
|
}
|
||||||
|
$value = (int)$candidate;
|
||||||
|
if (isset($rule['min']) && $value < (int)$rule['min']) {
|
||||||
|
return $rule['default'] ?? null;
|
||||||
|
}
|
||||||
|
if (isset($rule['max']) && $value > (int)$rule['max']) {
|
||||||
|
return $rule['default'] ?? null;
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'enum') {
|
||||||
|
if (is_array($rawValue)) {
|
||||||
|
return $rule['default'] ?? null;
|
||||||
|
}
|
||||||
|
$value = trim((string)$rawValue);
|
||||||
|
if ($value === '') {
|
||||||
|
return $rule['default'] ?? null;
|
||||||
|
}
|
||||||
|
$allowed = is_array($rule['allowed'] ?? null) ? $rule['allowed'] : [];
|
||||||
|
if (!in_array($value, $allowed, true)) {
|
||||||
|
return $rule['default'] ?? null;
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'bool_flag') {
|
||||||
|
if (is_array($rawValue)) {
|
||||||
|
return $rule['default'] ?? null;
|
||||||
|
}
|
||||||
|
$truthyValues = is_array($rule['truthy_values'] ?? null)
|
||||||
|
? $rule['truthy_values']
|
||||||
|
: ['1', 'true', 'yes', 'on'];
|
||||||
|
$candidate = strtolower(trim((string)$rawValue));
|
||||||
|
if (in_array($candidate, $truthyValues, true)) {
|
||||||
|
return $rule['value_true'] ?? '1';
|
||||||
|
}
|
||||||
|
return $rule['default'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'string_list') {
|
||||||
|
if (!is_array($rawValue)) {
|
||||||
|
return $rule['default'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize list payloads into deterministic arrays for canonical URLs.
|
||||||
|
$items = array_map(static function ($item): string {
|
||||||
|
return trim((string)$item);
|
||||||
|
}, $rawValue);
|
||||||
|
$items = array_values(array_filter($items, static function ($item): bool {
|
||||||
|
return $item !== '';
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!empty($rule['unique'])) {
|
||||||
|
$items = array_values(array_unique($items));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($items) && !($rule['allow_empty'] ?? false)) {
|
||||||
|
return $rule['default'] ?? null;
|
||||||
|
}
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rule['default'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build canonical query payload from declarative policy rules.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $sourceQuery
|
||||||
|
* @param array<string,array<string,mixed>> $policy
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
function app_url_build_query_from_policy(array $sourceQuery, array $policy): array
|
||||||
|
{
|
||||||
|
$canonical = [];
|
||||||
|
|
||||||
|
foreach ($policy as $targetKey => $rule) {
|
||||||
|
if (!is_array($rule)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($rule['include_if']) && is_callable($rule['include_if']) && !$rule['include_if']($sourceQuery, $canonical)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = app_url_policy_value((string)$targetKey, $rule, $sourceQuery);
|
||||||
|
if ($value === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($rule['transform']) && is_callable($rule['transform'])) {
|
||||||
|
$value = $rule['transform']($value, $sourceQuery, $canonical);
|
||||||
|
if ($value === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($rule['validator']) && is_callable($rule['validator']) && !$rule['validator']($value, $sourceQuery, $canonical)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('omit_if', $rule) && $value === $rule['omit_if']) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$canonical[(string)$targetKey] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $canonical;
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,8 @@
|
||||||
* - `password`: Change password
|
* - `password`: Change password
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
require_once '../app/helpers/url_canonicalizer.php';
|
||||||
|
|
||||||
// Initialize user object
|
// Initialize user object
|
||||||
$userObject = new User($db);
|
$userObject = new User($db);
|
||||||
|
|
||||||
|
|
@ -21,6 +23,24 @@ $userObject = new User($db);
|
||||||
$action = $_REQUEST['action'] ?? '';
|
$action = $_REQUEST['action'] ?? '';
|
||||||
$item = $_REQUEST['item'] ?? '';
|
$item = $_REQUEST['item'] ?? '';
|
||||||
|
|
||||||
|
$isGetRequest = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'GET';
|
||||||
|
if ($isGetRequest) {
|
||||||
|
$canonicalPolicy = [
|
||||||
|
'page' => [
|
||||||
|
'type' => 'literal',
|
||||||
|
'value' => 'credentials',
|
||||||
|
],
|
||||||
|
'action' => [
|
||||||
|
'type' => 'enum',
|
||||||
|
'allowed' => ['setup', 'verify'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$canonicalQuery = app_url_build_query_from_policy($_GET, $canonicalPolicy);
|
||||||
|
|
||||||
|
// Restrict credentials URLs to valid setup/verify screen states.
|
||||||
|
app_url_redirect_to_canonical_query((string)$app_root, $_GET, $canonicalQuery);
|
||||||
|
}
|
||||||
|
|
||||||
// if a form is submitted
|
// if a form is submitted
|
||||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
// Ensure security helper is available
|
// Ensure security helper is available
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@
|
||||||
// clear the global error var before login
|
// clear the global error var before login
|
||||||
unset($error);
|
unset($error);
|
||||||
|
|
||||||
|
require_once '../app/helpers/url_canonicalizer.php';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// connect to database
|
// connect to database
|
||||||
$db = connectDB($config);
|
$db = connectDB($config);
|
||||||
|
|
@ -31,6 +33,39 @@ try {
|
||||||
|
|
||||||
$action = $_REQUEST['action'] ?? '';
|
$action = $_REQUEST['action'] ?? '';
|
||||||
|
|
||||||
|
$isGetRequest = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'GET';
|
||||||
|
if ($isGetRequest) {
|
||||||
|
$canonicalPolicy = [
|
||||||
|
'page' => [
|
||||||
|
'type' => 'literal',
|
||||||
|
'value' => 'login',
|
||||||
|
],
|
||||||
|
'action' => [
|
||||||
|
'type' => 'enum',
|
||||||
|
'allowed' => ['verify', 'forgot', 'reset'],
|
||||||
|
],
|
||||||
|
'token' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'validator' => static function ($value): bool {
|
||||||
|
return $value !== '';
|
||||||
|
},
|
||||||
|
'include_if' => static function (array $sourceQuery): bool {
|
||||||
|
return (($sourceQuery['action'] ?? '') === 'reset');
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'redirect' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'validator' => static function ($value): bool {
|
||||||
|
return (strpos($value, '/') === 0 || strpos($value, '?') === 0);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$canonicalQuery = app_url_build_query_from_policy($_GET, $canonicalPolicy);
|
||||||
|
|
||||||
|
// Keep login URLs constrained to supported route states and safe redirect inputs.
|
||||||
|
app_url_redirect_to_canonical_query((string)$app_root, $_GET, $canonicalQuery);
|
||||||
|
}
|
||||||
|
|
||||||
if ($action === 'verify' && isset($_SESSION['2fa_pending_user_id'])) {
|
if ($action === 'verify' && isset($_SESSION['2fa_pending_user_id'])) {
|
||||||
// Handle 2FA verification
|
// Handle 2FA verification
|
||||||
$code = $_POST['code'] ?? '';
|
$code = $_POST['code'] ?? '';
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,29 @@
|
||||||
* - `edit`: Edit user profile details, rights, or avatar.
|
* - `edit`: Edit user profile details, rights, or avatar.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
require_once '../app/helpers/url_canonicalizer.php';
|
||||||
|
|
||||||
$action = $_REQUEST['action'] ?? '';
|
$action = $_REQUEST['action'] ?? '';
|
||||||
$item = $_REQUEST['item'] ?? '';
|
$item = $_REQUEST['item'] ?? '';
|
||||||
|
|
||||||
|
$isGetRequest = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'GET';
|
||||||
|
if ($isGetRequest) {
|
||||||
|
$canonicalPolicy = [
|
||||||
|
'page' => [
|
||||||
|
'type' => 'literal',
|
||||||
|
'value' => 'profile',
|
||||||
|
],
|
||||||
|
'action' => [
|
||||||
|
'type' => 'enum',
|
||||||
|
'allowed' => ['edit'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$canonicalQuery = app_url_build_query_from_policy($_GET, $canonicalPolicy);
|
||||||
|
|
||||||
|
// Keep profile URLs constrained to supported view states only.
|
||||||
|
app_url_redirect_to_canonical_query((string)$app_root, $_GET, $canonicalQuery);
|
||||||
|
}
|
||||||
|
|
||||||
// pass the user details to the profile hooks
|
// pass the user details to the profile hooks
|
||||||
$profileHooksContext = [
|
$profileHooksContext = [
|
||||||
'userId' => $userId ?? null,
|
'userId' => $userId ?? null,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
require_once APP_PATH . 'helpers/url_canonicalizer.php';
|
||||||
|
|
||||||
// Check if user has any of the required rights
|
// Check if user has any of the required rights
|
||||||
if (!($userObject->hasRight($userId, 'superuser') ||
|
if (!($userObject->hasRight($userId, 'superuser') ||
|
||||||
$userObject->hasRight($userId, 'edit whitelist') ||
|
$userObject->hasRight($userId, 'edit whitelist') ||
|
||||||
|
|
@ -11,6 +13,29 @@ if (!($userObject->hasRight($userId, 'superuser') ||
|
||||||
|
|
||||||
// Get current section
|
// Get current section
|
||||||
$section = isset($_POST['section']) ? $_POST['section'] : (isset($_GET['section']) ? $_GET['section'] : 'whitelist');
|
$section = isset($_POST['section']) ? $_POST['section'] : (isset($_GET['section']) ? $_GET['section'] : 'whitelist');
|
||||||
|
$allowedSections = ['whitelist', 'blacklist', 'ratelimit'];
|
||||||
|
if (!in_array($section, $allowedSections, true)) {
|
||||||
|
$section = 'whitelist';
|
||||||
|
}
|
||||||
|
|
||||||
|
$isGetRequest = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'GET';
|
||||||
|
if ($isGetRequest) {
|
||||||
|
$canonicalPolicy = [
|
||||||
|
'page' => [
|
||||||
|
'type' => 'literal',
|
||||||
|
'value' => 'security',
|
||||||
|
],
|
||||||
|
'section' => [
|
||||||
|
'type' => 'literal',
|
||||||
|
'value' => $section,
|
||||||
|
'omit_if' => 'whitelist',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$canonicalQuery = app_url_build_query_from_policy($_GET, $canonicalPolicy);
|
||||||
|
|
||||||
|
// Keep security page URLs stable by removing unknown GET parameters.
|
||||||
|
app_url_redirect_to_canonical_query((string)$app_root, $_GET, $canonicalQuery);
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize RateLimiter
|
// Initialize RateLimiter
|
||||||
require_once '../app/classes/ratelimiter.php';
|
require_once '../app/classes/ratelimiter.php';
|
||||||
|
|
@ -153,8 +178,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
Feedback::flash('ERROR', $e->getMessage());
|
Feedback::flash('ERROR', $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect back to the appropriate section
|
// Redirect back to the appropriate section using canonical query formatting.
|
||||||
header("Location: $app_root?page=security§ion=" . urlencode($section));
|
$redirectPolicy = [
|
||||||
|
'page' => [
|
||||||
|
'type' => 'literal',
|
||||||
|
'value' => 'security',
|
||||||
|
],
|
||||||
|
'section' => [
|
||||||
|
'type' => 'literal',
|
||||||
|
'value' => $section,
|
||||||
|
'omit_if' => 'whitelist',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$redirectQuery = app_url_build_query_from_policy([], $redirectPolicy);
|
||||||
|
header('Location: ' . app_url_build_internal((string)$app_root, $redirectQuery));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
// Initialize security
|
// Initialize security
|
||||||
require_once '../app/helpers/security.php';
|
require_once '../app/helpers/security.php';
|
||||||
|
require_once '../app/helpers/url_canonicalizer.php';
|
||||||
$security = SecurityHelper::getInstance();
|
$security = SecurityHelper::getInstance();
|
||||||
|
|
||||||
// Only allow access to logged-in users
|
// Only allow access to logged-in users
|
||||||
|
|
@ -22,6 +23,29 @@ if (!Session::isValidSession()) {
|
||||||
// Get any old feedback messages
|
// Get any old feedback messages
|
||||||
include_once '../app/helpers/feedback.php';
|
include_once '../app/helpers/feedback.php';
|
||||||
|
|
||||||
|
$isGetRequest = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'GET';
|
||||||
|
if ($isGetRequest) {
|
||||||
|
$canonicalPolicy = [
|
||||||
|
'page' => [
|
||||||
|
'type' => 'literal',
|
||||||
|
'value' => 'theme',
|
||||||
|
],
|
||||||
|
'switch_to' => [
|
||||||
|
'type' => 'string',
|
||||||
|
],
|
||||||
|
'csrf_token' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'include_if' => static function (array $sourceQuery): bool {
|
||||||
|
return trim((string)($sourceQuery['switch_to'] ?? '')) !== '';
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$canonicalQuery = app_url_build_query_from_policy($_GET, $canonicalPolicy);
|
||||||
|
|
||||||
|
// Keep theme page URLs deterministic while preserving switch action inputs.
|
||||||
|
app_url_redirect_to_canonical_query((string)$app_root, $_GET, $canonicalQuery);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle theme switching
|
// Handle theme switching
|
||||||
if (isset($_GET['switch_to'])) {
|
if (isset($_GET['switch_to'])) {
|
||||||
$themeName = $_GET['switch_to'];
|
$themeName = $_GET['switch_to'];
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ require_once PLUGIN_LOGS_PATH . 'models/Log.php';
|
||||||
require_once PLUGIN_LOGS_PATH . 'models/LoggerFactory.php';
|
require_once PLUGIN_LOGS_PATH . 'models/LoggerFactory.php';
|
||||||
require_once APP_PATH . 'classes/user.php';
|
require_once APP_PATH . 'classes/user.php';
|
||||||
require_once APP_PATH . 'helpers/theme.php';
|
require_once APP_PATH . 'helpers/theme.php';
|
||||||
|
require_once APP_PATH . 'helpers/url_canonicalizer.php';
|
||||||
|
|
||||||
function logs_plugin_handle(string $action, array $context = []): bool {
|
function logs_plugin_handle(string $action, array $context = []): bool {
|
||||||
$validSession = (bool)($context['valid_session'] ?? false);
|
$validSession = (bool)($context['valid_session'] ?? false);
|
||||||
|
|
@ -31,6 +32,60 @@ function logs_plugin_handle(string $action, array $context = []): bool {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$isGetRequest = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'GET';
|
||||||
|
if ($isGetRequest) {
|
||||||
|
$canonicalPolicy = [
|
||||||
|
'page' => [
|
||||||
|
'type' => 'literal',
|
||||||
|
'value' => 'logs',
|
||||||
|
],
|
||||||
|
'action' => [
|
||||||
|
'type' => 'enum',
|
||||||
|
'allowed' => ['list'],
|
||||||
|
'omit_if' => 'list',
|
||||||
|
],
|
||||||
|
'tab' => [
|
||||||
|
'type' => 'enum',
|
||||||
|
'allowed' => ['user', 'system'],
|
||||||
|
'omit_if' => 'user',
|
||||||
|
],
|
||||||
|
'page_num' => [
|
||||||
|
'type' => 'int',
|
||||||
|
'min' => 1,
|
||||||
|
'omit_if' => 1,
|
||||||
|
],
|
||||||
|
'from_time' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'validator' => static function ($value): bool {
|
||||||
|
return trim((string)$value) !== '';
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'until_time' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'validator' => static function ($value): bool {
|
||||||
|
return trim((string)$value) !== '';
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'message' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'validator' => static function ($value): bool {
|
||||||
|
return trim((string)$value) !== '';
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'id' => [
|
||||||
|
'type' => 'int',
|
||||||
|
'min' => 1,
|
||||||
|
'include_if' => static function (array $sourceQuery): bool {
|
||||||
|
return (($sourceQuery['tab'] ?? '') === 'system');
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$canonicalQuery = app_url_build_query_from_policy($_GET, $canonicalPolicy);
|
||||||
|
|
||||||
|
// Keep logs URLs constrained to supported list filters and pagination state.
|
||||||
|
app_url_redirect_to_canonical_query((string)$app_root, $_GET, $canonicalQuery);
|
||||||
|
}
|
||||||
|
|
||||||
switch ($action) {
|
switch ($action) {
|
||||||
case 'list':
|
case 'list':
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ require_once APP_PATH . 'classes/user.php';
|
||||||
require_once APP_PATH . 'classes/validator.php';
|
require_once APP_PATH . 'classes/validator.php';
|
||||||
require_once APP_PATH . 'helpers/security.php';
|
require_once APP_PATH . 'helpers/security.php';
|
||||||
require_once APP_PATH . 'helpers/theme.php';
|
require_once APP_PATH . 'helpers/theme.php';
|
||||||
|
require_once APP_PATH . 'helpers/url_canonicalizer.php';
|
||||||
require_once APP_PATH . 'includes/rate_limit_middleware.php';
|
require_once APP_PATH . 'includes/rate_limit_middleware.php';
|
||||||
require_once PLUGIN_REGISTER_PATH . 'models/register.php';
|
require_once PLUGIN_REGISTER_PATH . 'models/register.php';
|
||||||
|
|
||||||
|
|
@ -33,6 +34,20 @@ function register_plugin_handle_register(string $action, array $context = []): b
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$isGetRequest = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'GET';
|
||||||
|
if ($isGetRequest) {
|
||||||
|
$canonicalPolicy = [
|
||||||
|
'page' => [
|
||||||
|
'type' => 'literal',
|
||||||
|
'value' => 'register',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$canonicalQuery = app_url_build_query_from_policy($_GET, $canonicalPolicy);
|
||||||
|
|
||||||
|
// Keep register URLs constrained to the canonical public registration route.
|
||||||
|
app_url_redirect_to_canonical_query((string)$app_root, $_GET, $canonicalQuery);
|
||||||
|
}
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
register_plugin_handle_submission($validSession, $app_root, $db, $logger);
|
register_plugin_handle_submission($validSession, $app_root, $db, $logger);
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue