Compare commits
No commits in common. "665d5bded91e0fdfef67dbd43561b0decc9ac84f" and "10460baad85b4914af20c2e66f74b8ff31efa77c" have entirely different histories.
665d5bded9
...
10460baad8
|
|
@ -39,9 +39,6 @@ class ApiResponse {
|
||||||
* @param int $status HTTP status code
|
* @param int $status HTTP status code
|
||||||
*/
|
*/
|
||||||
private static function send($data, $status) {
|
private static function send($data, $status) {
|
||||||
while (ob_get_level() > 0) {
|
|
||||||
ob_end_clean();
|
|
||||||
}
|
|
||||||
http_response_code($status);
|
http_response_code($status);
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
echo json_encode($data);
|
echo json_encode($data);
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ class Session {
|
||||||
}
|
}
|
||||||
private static $sessionOptions = [
|
private static $sessionOptions = [
|
||||||
'cookie_httponly' => 1,
|
'cookie_httponly' => 1,
|
||||||
'cookie_secure' => 0,
|
'cookie_secure' => 1,
|
||||||
'cookie_samesite' => 'Lax',
|
'cookie_samesite' => 'Strict',
|
||||||
'gc_maxlifetime' => 7200 // 2 hours
|
'gc_maxlifetime' => 7200 // 2 hours
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -52,13 +52,13 @@ class Session {
|
||||||
'domain' => $thisDomain,
|
'domain' => $thisDomain,
|
||||||
'secure' => $isSecure,
|
'secure' => $isSecure,
|
||||||
'httponly' => true,
|
'httponly' => true,
|
||||||
'samesite' => 'Lax'
|
'samesite' => 'Strict'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Align session start options dynamically with current transport
|
// Align session start options dynamically with current transport
|
||||||
self::$sessionOptions['cookie_secure'] = $isSecure ? 1 : 0;
|
self::$sessionOptions['cookie_secure'] = $isSecure ? 1 : 0;
|
||||||
self::$sessionOptions['cookie_samesite'] = 'Lax';
|
self::$sessionOptions['cookie_samesite'] = 'Strict';
|
||||||
|
|
||||||
self::$initialized = true;
|
self::$initialized = true;
|
||||||
}
|
}
|
||||||
|
|
@ -181,7 +181,7 @@ class Session {
|
||||||
'domain' => $config['domain'],
|
'domain' => $config['domain'],
|
||||||
'secure' => isset($_SERVER['HTTPS']),
|
'secure' => isset($_SERVER['HTTPS']),
|
||||||
'httponly' => true,
|
'httponly' => true,
|
||||||
'samesite' => 'Lax'
|
'samesite' => 'Strict'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,7 +219,7 @@ class Session {
|
||||||
'domain' => $config['domain'] ?? '',
|
'domain' => $config['domain'] ?? '',
|
||||||
'secure' => isset($_SERVER['HTTPS']),
|
'secure' => isset($_SERVER['HTTPS']),
|
||||||
'httponly' => true,
|
'httponly' => true,
|
||||||
'samesite' => 'Lax'
|
'samesite' => 'Strict'
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -230,7 +230,7 @@ class Session {
|
||||||
'domain' => $config['domain'] ?? '',
|
'domain' => $config['domain'] ?? '',
|
||||||
'secure' => isset($_SERVER['HTTPS']),
|
'secure' => isset($_SERVER['HTTPS']),
|
||||||
'httponly' => true,
|
'httponly' => true,
|
||||||
'samesite' => 'Lax'
|
'samesite' => 'Strict'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ class Validator {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'phone':
|
case 'phone':
|
||||||
if (!empty($value) && !preg_match('/^(\+?\d{1,4})?\s?(\d[\d\s]{6,})$/', $value)) {
|
if (!empty($value) && !preg_match('/^[+]?[\d\s-()]{7,}$/', $value)) {
|
||||||
$this->addError($field, "Invalid phone number format");
|
$this->addError($field, "Invalid phone number format");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -275,7 +275,7 @@ class PluginManager
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Purge plugin by dropping its migration-defined tables and removing settings.
|
* Purge plugin by dropping its tables and removing settings.
|
||||||
*/
|
*/
|
||||||
public static function purge(string $plugin): bool
|
public static function purge(string $plugin): bool
|
||||||
{
|
{
|
||||||
|
|
@ -290,8 +290,6 @@ class PluginManager
|
||||||
}
|
}
|
||||||
$pdo = ($db instanceof \PDO) ? $db : $db->getConnection();
|
$pdo = ($db instanceof \PDO) ? $db : $db->getConnection();
|
||||||
|
|
||||||
$foreignKeyChecksDisabled = false;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First disable the plugin
|
// First disable the plugin
|
||||||
self::setEnabled($plugin, false);
|
self::setEnabled($plugin, false);
|
||||||
|
|
@ -300,55 +298,34 @@ class PluginManager
|
||||||
$stmt = $pdo->prepare('DELETE FROM settings WHERE `key` LIKE :pattern');
|
$stmt = $pdo->prepare('DELETE FROM settings WHERE `key` LIKE :pattern');
|
||||||
$stmt->execute([':pattern' => 'plugin_enabled_' . $plugin]);
|
$stmt->execute([':pattern' => 'plugin_enabled_' . $plugin]);
|
||||||
|
|
||||||
$migrationDir = self::$catalog[$plugin]['path'] . '/migrations';
|
// Drop plugin-specific tables (user_pro_* tables for this plugin)
|
||||||
$migrationFiles = glob($migrationDir . '/*.sql') ?: [];
|
$stmt = $pdo->prepare('SHOW TABLES LIKE "user_pro_%"');
|
||||||
$tables = [];
|
$stmt->execute();
|
||||||
|
$tables = $stmt->fetchAll(\PDO::FETCH_COLUMN, 0);
|
||||||
foreach ($migrationFiles as $migrationFile) {
|
|
||||||
if (!is_string($migrationFile) || !file_exists($migrationFile)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$migrationContent = file_get_contents($migrationFile);
|
|
||||||
if (!is_string($migrationContent) || $migrationContent === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Derive table ownership from CREATE TABLE statements in plugin migrations.
|
|
||||||
preg_match_all('/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?([a-zA-Z0-9_]+)`?/i', $migrationContent, $matches);
|
|
||||||
$discoveredTables = $matches[1] ?? [];
|
|
||||||
foreach ($discoveredTables as $tableName) {
|
|
||||||
if (!is_string($tableName) || $tableName === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$tables[] = $tableName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$tables = array_values(array_unique($tables));
|
|
||||||
|
|
||||||
// Disable foreign key checks temporarily to allow table drops
|
// Disable foreign key checks temporarily to allow table drops
|
||||||
$pdo->exec('SET FOREIGN_KEY_CHECKS=0');
|
$pdo->exec('SET FOREIGN_KEY_CHECKS=0');
|
||||||
$foreignKeyChecksDisabled = true;
|
|
||||||
|
|
||||||
foreach ($tables as $table) {
|
foreach ($tables as $table) {
|
||||||
$pdo->exec("DROP TABLE IF EXISTS `$table`");
|
// Check if this table belongs to the plugin by checking its migration file
|
||||||
app_log('info', 'PluginManager::purge: Dropped table ' . $table . ' for plugin ' . $plugin, ['scope' => 'plugin']);
|
$migrationFile = self::$catalog[$plugin]['path'] . '/migrations/create_' . $plugin . '_tables.sql';
|
||||||
|
if (file_exists($migrationFile)) {
|
||||||
|
$migrationContent = file_get_contents($migrationFile);
|
||||||
|
if (strpos($migrationContent, $table) !== false) {
|
||||||
|
$pdo->exec("DROP TABLE IF EXISTS `$table`");
|
||||||
|
app_log('info', 'PluginManager::purge: Dropped table ' . $table . ' for plugin ' . $plugin, ['scope' => 'plugin']);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-enable foreign key checks
|
||||||
|
$pdo->exec('SET FOREIGN_KEY_CHECKS=1');
|
||||||
|
|
||||||
app_log('info', 'PluginManager::purge: Successfully purged plugin ' . $plugin, ['scope' => 'plugin']);
|
app_log('info', 'PluginManager::purge: Successfully purged plugin ' . $plugin, ['scope' => 'plugin']);
|
||||||
return true;
|
return true;
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
app_log('error', 'PluginManager::purge failed for ' . $plugin . ': ' . $e->getMessage(), ['scope' => 'plugin']);
|
app_log('error', 'PluginManager::purge failed for ' . $plugin . ': ' . $e->getMessage(), ['scope' => 'plugin']);
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
|
||||||
if ($foreignKeyChecksDisabled) {
|
|
||||||
try {
|
|
||||||
$pdo->exec('SET FOREIGN_KEY_CHECKS=1');
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
app_log('error', 'PluginManager::purge: Failed to restore FOREIGN_KEY_CHECKS for ' . $plugin . ': ' . $e->getMessage(), ['scope' => 'plugin']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,252 +0,0 @@
|
||||||
<?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,8 +14,6 @@
|
||||||
* - `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);
|
||||||
|
|
||||||
|
|
@ -23,24 +21,6 @@ $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,8 +17,6 @@
|
||||||
// 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);
|
||||||
|
|
@ -33,39 +31,6 @@ 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,29 +12,8 @@
|
||||||
* - `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,7 +1,5 @@
|
||||||
<?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') ||
|
||||||
|
|
@ -13,29 +11,6 @@ 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';
|
||||||
|
|
@ -178,20 +153,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
Feedback::flash('ERROR', $e->getMessage());
|
Feedback::flash('ERROR', $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect back to the appropriate section using canonical query formatting.
|
// Redirect back to the appropriate section
|
||||||
$redirectPolicy = [
|
header("Location: $app_root?page=security§ion=" . urlencode($section));
|
||||||
'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,7 +11,6 @@
|
||||||
|
|
||||||
// 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
|
||||||
|
|
@ -23,29 +22,6 @@ 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'];
|
||||||
|
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
# App Registry Helper
|
|
||||||
|
|
||||||
The `App\App` class provides a service locator to expose core services to
|
|
||||||
plugins without relying on globals or relative `require_once` paths.
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
|
|
||||||
1. Provide a stable API surface for plugins and core modules.
|
|
||||||
2. Allow gradual refactors away from `$GLOBALS`.
|
|
||||||
3. Keep legacy code working by falling back to existing globals when no service
|
|
||||||
has been registered yet.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```php
|
|
||||||
use App\App;
|
|
||||||
|
|
||||||
// Register services during bootstrap
|
|
||||||
App::set('config', $config);
|
|
||||||
App::set('db', $dbConnection);
|
|
||||||
App::set('logger', $logger);
|
|
||||||
|
|
||||||
// Use services anywhere later
|
|
||||||
$db = App::db();
|
|
||||||
$config = App::config();
|
|
||||||
$logger = App::get('logger');
|
|
||||||
```
|
|
||||||
|
|
||||||
### Convenience Helpers
|
|
||||||
|
|
||||||
The helper exposes shortcuts for the most common services:
|
|
||||||
|
|
||||||
- `App::db()` – database connection
|
|
||||||
- `App::config()` – configuration array
|
|
||||||
- `App::user()` – authenticated user object (if any)
|
|
||||||
|
|
||||||
All helper calls fall back to their legacy `$GLOBALS` equivalents so older code
|
|
||||||
can be migrated incrementally.
|
|
||||||
|
|
||||||
### Resetting (Tests)
|
|
||||||
|
|
||||||
Unit tests can call `App::reset()` (optionally with a service key) to clear the
|
|
||||||
registry and avoid old state bleed between test cases.
|
|
||||||
|
|
||||||
## Bootstrap Integration
|
|
||||||
|
|
||||||
`public_html/index.php` now registers runtime services as they are created:
|
|
||||||
|
|
||||||
```php
|
|
||||||
App::set('config', $config);
|
|
||||||
App::set('config_path', $configFile);
|
|
||||||
App::set('app_root', $appRoot);
|
|
||||||
App::set('db', $db);
|
|
||||||
App::set('logger', $logger);
|
|
||||||
```
|
|
||||||
|
|
||||||
Plugins should prefer `App` over accessing globals directly. This ensures future
|
|
||||||
moves (like relocating call logic into `plugins/calls/`) do not require path rewrites
|
|
||||||
or global variables.
|
|
||||||
100
doc/security.md
100
doc/security.md
|
|
@ -1,100 +0,0 @@
|
||||||
# Security Documentation
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document outlines the security features and practices implemented in the system.
|
|
||||||
|
|
||||||
## Authentication
|
|
||||||
|
|
||||||
Authentication is handled through the user accounts system. See `user-accounts.md` for details on:
|
|
||||||
- User registration
|
|
||||||
- Login/logout functionality
|
|
||||||
- Password requirements
|
|
||||||
- Session management
|
|
||||||
|
|
||||||
## Database Security
|
|
||||||
|
|
||||||
1. **SQL Injection Prevention**
|
|
||||||
- All database queries use prepared statements with parameterized queries
|
|
||||||
- Input validation and sanitization
|
|
||||||
- Use of PDO for database access
|
|
||||||
|
|
||||||
2. **Data Access Control**
|
|
||||||
- User ownership verification on all operations
|
|
||||||
- Permission checks before data access
|
|
||||||
- Proper error handling to prevent information leakage
|
|
||||||
|
|
||||||
## Database Tables
|
|
||||||
|
|
||||||
The security system uses the following tables:
|
|
||||||
|
|
||||||
1. **Rate Limits (`rate_limit`)**
|
|
||||||
- Tracks rate limiting for various operations
|
|
||||||
- User and IP tracking
|
|
||||||
- Operation type identification
|
|
||||||
- Timestamp tracking
|
|
||||||
- Attempt counting
|
|
||||||
|
|
||||||
2. **Security Events (`security_event`)**
|
|
||||||
- Records security-related events
|
|
||||||
- Event type and severity
|
|
||||||
- User and IP information
|
|
||||||
- Timestamp tracking
|
|
||||||
- Event details storage
|
|
||||||
|
|
||||||
3. **Blocked IPs (`blocked_ip`)**
|
|
||||||
- Manages IP blocking
|
|
||||||
- Block reason tracking
|
|
||||||
- Block duration
|
|
||||||
- Administrator notes
|
|
||||||
|
|
||||||
## Data Protection
|
|
||||||
|
|
||||||
1. **Passwords**
|
|
||||||
- Stored using secure hashing
|
|
||||||
- Never stored or transmitted in plain text
|
|
||||||
- Password reset functionality with secure tokens
|
|
||||||
|
|
||||||
2. **Session Security**
|
|
||||||
- Session tokens properly generated and managed
|
|
||||||
- Session timeout implementation
|
|
||||||
- Protection against session fixation
|
|
||||||
|
|
||||||
3. **Input Validation**
|
|
||||||
- Data validation on both client and server side
|
|
||||||
- Protection against XSS attacks
|
|
||||||
- Content type verification
|
|
||||||
- Size limits on inputs
|
|
||||||
|
|
||||||
## Access Control
|
|
||||||
|
|
||||||
1. **Resource Protection**
|
|
||||||
- User ownership verification for all resources
|
|
||||||
- Permission checks before operations
|
|
||||||
- Proper error handling for unauthorized access
|
|
||||||
|
|
||||||
2. **API Security**
|
|
||||||
- Authentication required for API access
|
|
||||||
- Rate limiting
|
|
||||||
- Input validation
|
|
||||||
- Error handling without information leakage
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Code Security**
|
|
||||||
- Use of prepared statements
|
|
||||||
- Input validation and sanitization
|
|
||||||
- Proper error handling
|
|
||||||
- Secure configuration management
|
|
||||||
|
|
||||||
2. **Data Security**
|
|
||||||
- User data protection
|
|
||||||
- Secure storage practices
|
|
||||||
- Access control implementation
|
|
||||||
- Error handling without leaks
|
|
||||||
|
|
||||||
3. **Infrastructure Security**
|
|
||||||
- Configuration security
|
|
||||||
- Environment separation
|
|
||||||
- Secure deployment practices
|
|
||||||
- Regular security updates
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
# URL canonicalization and normalization guide
|
|
||||||
|
|
||||||
This document defines the standard flow for query-string canonicalization in page/controllers.
|
|
||||||
Use it for all new route work and when touching existing page logic.
|
|
||||||
|
|
||||||
## Why this exists
|
|
||||||
|
|
||||||
Canonical URLs make route behavior predictable and secure by:
|
|
||||||
- removing unknown query parameters,
|
|
||||||
- normalizing known parameters to expected types,
|
|
||||||
- preventing duplicate URL variants for the same page state,
|
|
||||||
- reducing controller-specific ad-hoc redirect logic.
|
|
||||||
|
|
||||||
## Shared helper
|
|
||||||
|
|
||||||
All route canonicalization must use:
|
|
||||||
- `app/helpers/url_canonicalizer.php`
|
|
||||||
|
|
||||||
Core functions:
|
|
||||||
- `app_url_build_query_from_policy(array $sourceQuery, array $policy): array`
|
|
||||||
- `app_url_redirect_to_canonical_query(string $appRoot, array $currentQuery, array $canonicalQuery): void`
|
|
||||||
- `app_url_build_internal(string $appRoot, array $query): string`
|
|
||||||
- `app_url_policy_value(string $targetKey, array $rule, array $sourceQuery)`
|
|
||||||
|
|
||||||
## Standard controller flow
|
|
||||||
|
|
||||||
For GET routes, follow this order:
|
|
||||||
|
|
||||||
1. Resolve request context (`app_root`, user/session state, etc.).
|
|
||||||
2. Resolve a defensive GET guard (`$isGetRequest`) from `$_SERVER['REQUEST_METHOD']`.
|
|
||||||
3. Define canonical policy rules for the route.
|
|
||||||
4. Build canonical query from `$_GET`.
|
|
||||||
5. Redirect if current query differs from canonical query.
|
|
||||||
6. Continue regular page logic (rendering, DB loading, etc.).
|
|
||||||
|
|
||||||
Reference pattern:
|
|
||||||
|
|
||||||
```php
|
|
||||||
require_once APP_PATH . 'helpers/url_canonicalizer.php';
|
|
||||||
|
|
||||||
$isGetRequest = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'GET';
|
|
||||||
if ($isGetRequest) {
|
|
||||||
$canonicalPolicy = [
|
|
||||||
'page' => [
|
|
||||||
'type' => 'literal',
|
|
||||||
'value' => 'example',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
$canonicalQuery = app_url_build_query_from_policy($_GET, $canonicalPolicy);
|
|
||||||
|
|
||||||
// Keep example URLs constrained to supported route state.
|
|
||||||
app_url_redirect_to_canonical_query((string)$app_root, $_GET, $canonicalQuery);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Policy rule types
|
|
||||||
|
|
||||||
Supported rule `type` values:
|
|
||||||
- `literal`: fixed value from policy (`value`)
|
|
||||||
- `string`: trimmed scalar string
|
|
||||||
- `int`: integer with optional bounds (`min`, `max`)
|
|
||||||
- `enum`: string limited to `allowed` values
|
|
||||||
- `bool_flag`: emits `value_true` for truthy request inputs
|
|
||||||
- `string_list`: normalized list values (optionally `unique`)
|
|
||||||
|
|
||||||
Useful options:
|
|
||||||
- `source`: map canonical key from another source key
|
|
||||||
- `default`: fallback value
|
|
||||||
- `include_if`: callable gate to include rule conditionally
|
|
||||||
- `omit_if`: drop key when value equals sentinel
|
|
||||||
- `transform`: callable value transformer
|
|
||||||
- `validator`: callable final validator
|
|
||||||
|
|
||||||
## Route design rules
|
|
||||||
|
|
||||||
When adding canonicalization:
|
|
||||||
- Always include `page` as `literal`.
|
|
||||||
- Keep allowed query set minimal.
|
|
||||||
- Use `enum` for fixed states (`tab`, `action`, `status`, etc.).
|
|
||||||
- Use `int` with bounds for IDs and pagination.
|
|
||||||
- Use `omit_if` to avoid noisy defaults in URLs (for example `p=1`).
|
|
||||||
- Preserve only query keys that materially represent page state.
|
|
||||||
|
|
||||||
## What not to canonicalize as page URLs
|
|
||||||
|
|
||||||
Do not force page-style canonicalization on non-page endpoints that intentionally behave as API/callback streams, for example:
|
|
||||||
- JSON suggestion endpoints,
|
|
||||||
- payment webhook/callback handlers,
|
|
||||||
- binary/document output handlers,
|
|
||||||
- static asset streaming handlers.
|
|
||||||
|
|
||||||
For these endpoints, keep strict input validation and explicit allowlists as currently implemented.
|
|
||||||
|
|
||||||
## Redirect behavior
|
|
||||||
|
|
||||||
`app_url_redirect_to_canonical_query` compares normalized current and canonical queries.
|
|
||||||
If different, it sends a `Location` header and exits.
|
|
||||||
|
|
||||||
Implications:
|
|
||||||
- Logic after the call runs only for canonical request URLs.
|
|
||||||
- Downstream code may continue reading `$_GET`; values are already canonicalized by redirect gate.
|
|
||||||
- If custom redirect URL construction is needed after POST actions, use `app_url_build_internal` with a policy-built query.
|
|
||||||
|
|
||||||
## Update checklist for new/edited routes
|
|
||||||
|
|
||||||
When changing a route:
|
|
||||||
1. Add/confirm `require_once` for `url_canonicalizer.php`.
|
|
||||||
2. Use the standardized defensive guard:
|
|
||||||
`$isGetRequest = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'GET';`
|
|
||||||
3. Add/adjust GET canonical policy near route entry.
|
|
||||||
4. Keep existing business logic unchanged unless explicitly requested.
|
|
||||||
5. Add concise inline comment for non-trivial policy/condition blocks.
|
|
||||||
6. Update deployment-facing route documentation used in your environment.
|
|
||||||
7. Run syntax checks and PHPUnit as part of validation cadence.
|
|
||||||
|
|
||||||
## Deployment notes
|
|
||||||
|
|
||||||
Coverage is deployment-scoped.
|
|
||||||
|
|
||||||
When auditing a specific environment:
|
|
||||||
- verify enabled route entry points use policy-based canonicalization,
|
|
||||||
- keep non-page API/callback/document/asset endpoints on strict allowlist
|
|
||||||
validation,
|
|
||||||
- keep local operational/developer documentation updated according to the
|
|
||||||
documentation set available in that installation.
|
|
||||||
|
|
@ -10,7 +10,6 @@ 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);
|
||||||
|
|
@ -32,60 +31,6 @@ 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,7 +11,6 @@ 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';
|
||||||
|
|
||||||
|
|
@ -34,20 +33,6 @@ 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