add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks - Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists - Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services - Add Microsoft OIDC SSO, API token management, and user lifecycle features - Add swagger-ui vendor integration and OpenAPI spec - Add production Docker setup and bin/ scripts - Update composer dependencies, config, templates, and frontend assets throughout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,18 +10,13 @@ use MintyPHP\Router;
|
||||
*/
|
||||
class AccessControl
|
||||
{
|
||||
/** Paths that are always public (no auth required) */
|
||||
private const ALWAYS_PUBLIC_PATHS = [
|
||||
'login',
|
||||
'register',
|
||||
'verify-email',
|
||||
];
|
||||
|
||||
/** Prefixes that are always public */
|
||||
private const ALWAYS_PUBLIC_PREFIXES = [
|
||||
'password/',
|
||||
'branding/',
|
||||
'auth/tenant-avatar-file',
|
||||
'flash/',
|
||||
'auth/microsoft/',
|
||||
'api/',
|
||||
];
|
||||
|
||||
private array $configuredPublicPaths;
|
||||
@@ -56,11 +51,6 @@ class AccessControl
|
||||
}
|
||||
}
|
||||
|
||||
// Check always-public paths
|
||||
if (in_array($normalizedPath, self::ALWAYS_PUBLIC_PATHS, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check always-public prefixes
|
||||
foreach (self::ALWAYS_PUBLIC_PREFIXES as $prefix) {
|
||||
if (str_starts_with($normalizedPath, $prefix)) {
|
||||
|
||||
181
lib/Http/ApiAuth.php
Normal file
181
lib/Http/ApiAuth.php
Normal file
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Http;
|
||||
|
||||
use MintyPHP\Repository\Access\RolePermissionRepository;
|
||||
use MintyPHP\Repository\Access\UserRoleRepository;
|
||||
use MintyPHP\Service\Auth\ApiTokenService;
|
||||
use MintyPHP\Service\Tenant\TenantScopeService;
|
||||
use MintyPHP\Service\User\UserService;
|
||||
|
||||
class ApiAuth
|
||||
{
|
||||
private static ?array $currentUser = null;
|
||||
private static ?array $currentPermissions = null;
|
||||
private static ?int $currentTenantId = null;
|
||||
private static ?int $tokenTenantId = null;
|
||||
private static ?array $currentTokenRecord = null;
|
||||
private static bool $authenticated = false;
|
||||
|
||||
/**
|
||||
* Extract the full Bearer token from the Authorization header.
|
||||
*/
|
||||
public static function extractBearerToken(): string
|
||||
{
|
||||
$header = trim((string) ($_SERVER['HTTP_AUTHORIZATION'] ?? ''));
|
||||
if ($header === '') {
|
||||
$header = trim((string) ($_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? ''));
|
||||
}
|
||||
if ($header === '' || stripos($header, 'Bearer ') !== 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return trim(substr($header, 7));
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to authenticate from Authorization: Bearer header.
|
||||
*/
|
||||
public static function authenticate(): bool
|
||||
{
|
||||
self::$currentUser = null;
|
||||
self::$currentPermissions = null;
|
||||
self::$currentTenantId = null;
|
||||
self::$tokenTenantId = null;
|
||||
self::$currentTokenRecord = null;
|
||||
self::$authenticated = false;
|
||||
|
||||
$bearerToken = self::extractBearerToken();
|
||||
if ($bearerToken === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = ApiTokenService::validate($bearerToken);
|
||||
if ($result === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$user = $result['user'];
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
|
||||
// Load permissions directly (bypass session cache)
|
||||
$roleIds = UserRoleRepository::listRoleIdsByUserId($userId);
|
||||
$permissions = RolePermissionRepository::listPermissionKeysByRoleIds($roleIds);
|
||||
|
||||
// Resolve tenant context
|
||||
$tokenTenantId = $result['tenant_id'];
|
||||
if ($tokenTenantId !== null) {
|
||||
$userTenantIds = TenantScopeService::getUserTenantIds($userId);
|
||||
if (!in_array($tokenTenantId, $userTenantIds, true)) {
|
||||
return false;
|
||||
}
|
||||
$currentTenantId = $tokenTenantId;
|
||||
self::$tokenTenantId = $tokenTenantId;
|
||||
} else {
|
||||
$currentTenantId = UserService::getCurrentTenantId($userId);
|
||||
self::$tokenTenantId = null;
|
||||
}
|
||||
|
||||
self::$currentUser = $user;
|
||||
self::$currentPermissions = $permissions;
|
||||
self::$currentTenantId = $currentTenantId;
|
||||
self::$currentTokenRecord = $result['token_record'];
|
||||
self::$authenticated = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function isAuthenticated(): bool
|
||||
{
|
||||
return self::$authenticated;
|
||||
}
|
||||
|
||||
public static function user(): ?array
|
||||
{
|
||||
return self::$currentUser;
|
||||
}
|
||||
|
||||
public static function userId(): int
|
||||
{
|
||||
return (int) (self::$currentUser['id'] ?? 0);
|
||||
}
|
||||
|
||||
public static function permissions(): array
|
||||
{
|
||||
return self::$currentPermissions ?? [];
|
||||
}
|
||||
|
||||
public static function hasPermission(string $key): bool
|
||||
{
|
||||
return in_array($key, self::$currentPermissions ?? [], true);
|
||||
}
|
||||
|
||||
public static function tenantId(): ?int
|
||||
{
|
||||
return self::$currentTenantId;
|
||||
}
|
||||
|
||||
public static function scopedTenantId(): ?int
|
||||
{
|
||||
return self::$tokenTenantId;
|
||||
}
|
||||
|
||||
public static function tokenRecord(): ?array
|
||||
{
|
||||
return self::$currentTokenRecord;
|
||||
}
|
||||
|
||||
public static function tokenId(): ?int
|
||||
{
|
||||
$id = (int) (self::$currentTokenRecord['id'] ?? 0);
|
||||
return $id > 0 ? $id : null;
|
||||
}
|
||||
|
||||
public static function isTenantScopedToken(): bool
|
||||
{
|
||||
return (self::$tokenTenantId ?? 0) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Require both tenant-scope and token-tenant access for a resource.
|
||||
*/
|
||||
public static function requireResourceAccess(string $resource, int $resourceId): void
|
||||
{
|
||||
if (!TenantScopeService::canAccess($resource, $resourceId, self::userId())) {
|
||||
ApiResponse::notFound();
|
||||
}
|
||||
self::requireTokenTenantAccess($resource, $resourceId);
|
||||
}
|
||||
|
||||
public static function requireTokenTenantAccess(string $resource, int $resourceId): void
|
||||
{
|
||||
if (!self::isTenantScopedToken()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tenantId = (int) (self::$tokenTenantId ?? 0);
|
||||
if ($tenantId <= 0) {
|
||||
ApiResponse::forbidden();
|
||||
}
|
||||
|
||||
if (!TenantScopeService::resourceBelongsToTenant($resource, $resourceId, $tenantId)) {
|
||||
ApiResponse::notFound();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user can self-manage API tokens.
|
||||
*/
|
||||
public static function canSelfManageTokens(): bool
|
||||
{
|
||||
return self::hasPermission(\MintyPHP\Service\Access\PermissionService::USERS_SELF_UPDATE)
|
||||
|| self::hasPermission(\MintyPHP\Service\Access\PermissionService::API_TOKENS_MANAGE);
|
||||
}
|
||||
|
||||
public static function requireSelfManageTokens(): void
|
||||
{
|
||||
if (!self::canSelfManageTokens()) {
|
||||
ApiResponse::forbidden('api_tokens_self_manage_forbidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
201
lib/Http/ApiBootstrap.php
Normal file
201
lib/Http/ApiBootstrap.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Http;
|
||||
|
||||
use MintyPHP\Service\Audit\ApiAuditService;
|
||||
use MintyPHP\Service\Security\RateLimiterService;
|
||||
use MintyPHP\Service\Settings\SettingService;
|
||||
|
||||
class ApiBootstrap
|
||||
{
|
||||
private const API_RATE_SCOPE_TOKEN = 'api.token_ip';
|
||||
private const API_RATE_SCOPE_IP = 'api.ip';
|
||||
private const API_RATE_LIMIT_TOKEN = 300;
|
||||
private const API_RATE_LIMIT_IP = 120;
|
||||
private const API_RATE_WINDOW_SECONDS = 60;
|
||||
private const API_RATE_BLOCK_SECONDS = 120;
|
||||
|
||||
private static bool $initialized = false;
|
||||
private static bool $shutdownRegistered = false;
|
||||
|
||||
/**
|
||||
* Initialize the API request context.
|
||||
*
|
||||
* Call this at the top of every API page action.
|
||||
* Sets MINTY_ALLOW_OUTPUT, CORS headers, authenticates via Bearer token.
|
||||
*/
|
||||
public static function init(): void
|
||||
{
|
||||
if (self::$initialized) {
|
||||
return;
|
||||
}
|
||||
self::$initialized = true;
|
||||
|
||||
if (!defined('MINTY_ALLOW_OUTPUT')) {
|
||||
define('MINTY_ALLOW_OUTPUT', true);
|
||||
}
|
||||
|
||||
self::registerShutdownHandler();
|
||||
ApiAuditService::startRequestContext();
|
||||
self::setCorsHeaders();
|
||||
|
||||
if (strtoupper($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') {
|
||||
http_response_code(204);
|
||||
die();
|
||||
}
|
||||
|
||||
self::enforceRateLimit();
|
||||
|
||||
$authenticated = ApiAuth::authenticate();
|
||||
if (!$authenticated) {
|
||||
ApiResponse::unauthorized();
|
||||
}
|
||||
}
|
||||
|
||||
private static function setCorsHeaders(): void
|
||||
{
|
||||
$rawOrigin = trim((string) ($_SERVER['HTTP_ORIGIN'] ?? ''));
|
||||
if ($rawOrigin === '') {
|
||||
return;
|
||||
}
|
||||
$origin = self::normalizeOrigin($rawOrigin);
|
||||
if ($origin === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$allowedOrigins = array_fill_keys(SettingService::getApiCorsAllowedOrigins(), true);
|
||||
if (!isset($allowedOrigins[$origin])) {
|
||||
return;
|
||||
}
|
||||
|
||||
header('Vary: Origin');
|
||||
header('Access-Control-Allow-Origin: ' . $origin);
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Authorization, Content-Type, Accept');
|
||||
header('Access-Control-Max-Age: 86400');
|
||||
}
|
||||
|
||||
private static function registerShutdownHandler(): void
|
||||
{
|
||||
if (self::$shutdownRegistered) {
|
||||
return;
|
||||
}
|
||||
self::$shutdownRegistered = true;
|
||||
|
||||
register_shutdown_function(static function (): void {
|
||||
$error = error_get_last();
|
||||
if (!is_array($error)) {
|
||||
$statusCode = (int) http_response_code();
|
||||
if ($statusCode <= 0) {
|
||||
$statusCode = 200;
|
||||
}
|
||||
ApiAuditService::finish($statusCode);
|
||||
return;
|
||||
}
|
||||
|
||||
$type = (int) $error['type'];
|
||||
$fatalTypes = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR];
|
||||
if (!in_array($type, $fatalTypes, true)) {
|
||||
$statusCode = (int) http_response_code();
|
||||
if ($statusCode <= 0) {
|
||||
$statusCode = 200;
|
||||
}
|
||||
ApiAuditService::finish($statusCode);
|
||||
return;
|
||||
}
|
||||
|
||||
$statusCode = (int) http_response_code();
|
||||
if ($statusCode < 400) {
|
||||
$statusCode = 500;
|
||||
}
|
||||
ApiAuditService::finish($statusCode, 'fatal_error');
|
||||
});
|
||||
}
|
||||
|
||||
private static function normalizeOrigin(string $origin): ?string
|
||||
{
|
||||
$origin = rtrim(trim($origin), '/');
|
||||
if ($origin === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parsed = parse_url($origin);
|
||||
if (!is_array($parsed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$scheme = strtolower((string) ($parsed['scheme'] ?? ''));
|
||||
$host = strtolower((string) ($parsed['host'] ?? ''));
|
||||
$port = isset($parsed['port']) ? (int) $parsed['port'] : null;
|
||||
|
||||
if (!in_array($scheme, ['http', 'https'], true) || $host === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
isset($parsed['path'])
|
||||
|| isset($parsed['query'])
|
||||
|| isset($parsed['fragment'])
|
||||
|| isset($parsed['user'])
|
||||
|| isset($parsed['pass'])
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$defaultPort = ($scheme === 'https') ? 443 : 80;
|
||||
$portSuffix = ($port !== null && $port !== $defaultPort) ? ':' . $port : '';
|
||||
return $scheme . '://' . $host . $portSuffix;
|
||||
}
|
||||
|
||||
private static function enforceRateLimit(): void
|
||||
{
|
||||
$ip = trim((string) ($_SERVER['REMOTE_ADDR'] ?? ''));
|
||||
if ($ip === '') {
|
||||
$ip = 'unknown';
|
||||
}
|
||||
|
||||
$ipResult = RateLimiterService::hit(
|
||||
self::API_RATE_SCOPE_IP,
|
||||
$ip,
|
||||
self::API_RATE_LIMIT_IP,
|
||||
self::API_RATE_WINDOW_SECONDS,
|
||||
self::API_RATE_BLOCK_SECONDS
|
||||
);
|
||||
if (!($ipResult['allowed'] ?? true)) {
|
||||
ApiResponse::tooManyRequests(max(1, (int) ($ipResult['retry_after'] ?? self::API_RATE_BLOCK_SECONDS)));
|
||||
}
|
||||
|
||||
$selector = self::extractBearerSelector();
|
||||
if ($selector === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$tokenResult = RateLimiterService::hit(
|
||||
self::API_RATE_SCOPE_TOKEN,
|
||||
strtolower($selector) . '|' . $ip,
|
||||
self::API_RATE_LIMIT_TOKEN,
|
||||
self::API_RATE_WINDOW_SECONDS,
|
||||
self::API_RATE_BLOCK_SECONDS
|
||||
);
|
||||
|
||||
if (!($tokenResult['allowed'] ?? true)) {
|
||||
ApiResponse::tooManyRequests(max(1, (int) ($tokenResult['retry_after'] ?? self::API_RATE_BLOCK_SECONDS)));
|
||||
}
|
||||
}
|
||||
|
||||
private static function extractBearerSelector(): string
|
||||
{
|
||||
$token = ApiAuth::extractBearerToken();
|
||||
if ($token === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$parts = explode(':', $token, 2);
|
||||
$selector = trim((string) ($parts[0] ?? ''));
|
||||
if ($selector === '' || !preg_match('/^[a-f0-9]{24}$/i', $selector)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $selector;
|
||||
}
|
||||
}
|
||||
158
lib/Http/ApiResponse.php
Normal file
158
lib/Http/ApiResponse.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Http;
|
||||
|
||||
use MintyPHP\Service\Audit\ApiAuditService;
|
||||
|
||||
class ApiResponse
|
||||
{
|
||||
public static function success(array $data = [], int $status = 200): never
|
||||
{
|
||||
self::send($data, $status);
|
||||
}
|
||||
|
||||
public static function created(array $data = []): never
|
||||
{
|
||||
self::success($data, 201);
|
||||
}
|
||||
|
||||
public static function noContent(): never
|
||||
{
|
||||
self::send(null, 204);
|
||||
}
|
||||
|
||||
public static function error(string $error, int $status = 400, array $extra = []): never
|
||||
{
|
||||
$body = array_merge(['error' => $error], $extra);
|
||||
self::send($body, $status, $error);
|
||||
}
|
||||
|
||||
public static function unauthorized(): never
|
||||
{
|
||||
self::error('unauthorized', 401);
|
||||
}
|
||||
|
||||
public static function forbidden(string $detail = 'forbidden'): never
|
||||
{
|
||||
self::error($detail, 403);
|
||||
}
|
||||
|
||||
public static function notFound(string $detail = 'not_found'): never
|
||||
{
|
||||
self::error($detail, 404);
|
||||
}
|
||||
|
||||
public static function methodNotAllowed(): never
|
||||
{
|
||||
self::error('method_not_allowed', 405);
|
||||
}
|
||||
|
||||
public static function validationError(array $errors): never
|
||||
{
|
||||
self::error('validation_error', 422, ['errors' => $errors]);
|
||||
}
|
||||
|
||||
public static function tooManyRequests(int $retryAfter = 60): never
|
||||
{
|
||||
self::send(
|
||||
['error' => 'rate_limit_exceeded'],
|
||||
429,
|
||||
'rate_limit_exceeded',
|
||||
['Retry-After: ' . $retryAfter]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a service result (['ok' => bool, ...] pattern) into an API response.
|
||||
*/
|
||||
public static function fromServiceResult(array $result, int $successStatus = 200): never
|
||||
{
|
||||
if ($result['ok'] ?? false) {
|
||||
$data = $result;
|
||||
unset($data['ok']);
|
||||
self::success($data, $successStatus);
|
||||
}
|
||||
|
||||
$status = (int) ($result['status'] ?? 0);
|
||||
if ($status < 400) {
|
||||
$status = 422;
|
||||
}
|
||||
$error = (string) ($result['error'] ?? 'operation_failed');
|
||||
$errors = $result['errors'] ?? [];
|
||||
$extra = [];
|
||||
if ($errors) {
|
||||
$extra['errors'] = $errors;
|
||||
}
|
||||
self::error($error, $status, $extra);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read JSON request body into an array.
|
||||
*/
|
||||
public static function readJsonBody(): array
|
||||
{
|
||||
$raw = file_get_contents('php://input');
|
||||
if ($raw === false || $raw === '') {
|
||||
return [];
|
||||
}
|
||||
$data = json_decode($raw, true);
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Require specific HTTP method(s).
|
||||
*/
|
||||
public static function requireMethod(string ...$methods): void
|
||||
{
|
||||
$method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'));
|
||||
if (!in_array($method, $methods, true)) {
|
||||
self::methodNotAllowed();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Require API authentication.
|
||||
*/
|
||||
public static function requireAuth(): void
|
||||
{
|
||||
if (!ApiAuth::isAuthenticated()) {
|
||||
self::unauthorized();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Require a specific permission key.
|
||||
*/
|
||||
public static function requirePermission(string $key): void
|
||||
{
|
||||
self::requireAuth();
|
||||
if (!ApiAuth::hasPermission($key)) {
|
||||
self::forbidden();
|
||||
}
|
||||
}
|
||||
|
||||
private static function send(?array $body, int $status, ?string $errorCode = null, array $headers = []): never
|
||||
{
|
||||
http_response_code($status);
|
||||
foreach ($headers as $headerLine) {
|
||||
header($headerLine);
|
||||
}
|
||||
|
||||
if ($body === null) {
|
||||
ApiAuditService::finish($status, $errorCode);
|
||||
die();
|
||||
}
|
||||
|
||||
$json = json_encode($body, JSON_UNESCAPED_UNICODE);
|
||||
if (!is_string($json)) {
|
||||
$status = 500;
|
||||
http_response_code($status);
|
||||
$errorCode = $errorCode ?: 'serialization_error';
|
||||
$json = json_encode(['error' => 'serialization_error'], JSON_UNESCAPED_UNICODE) ?: '{"error":"serialization_error"}';
|
||||
}
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
ApiAuditService::finish($status, $errorCode);
|
||||
die($json);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace MintyPHP\Http;
|
||||
|
||||
use MintyPHP\I18n;
|
||||
use MintyPHP\Router;
|
||||
|
||||
/**
|
||||
* Handles locale detection and URL manipulation for multi-language support.
|
||||
@@ -18,7 +17,7 @@ class LocaleResolver
|
||||
{
|
||||
$this->defaultLocale = $defaultLocale ?? I18n::$defaultLocale;
|
||||
$this->availableLocales = $availableLocales ?? (defined('APP_LOCALES') ? APP_LOCALES : [$this->defaultLocale]);
|
||||
$this->basePath = trim(parse_url(Router::getBaseUrl(), PHP_URL_PATH) ?: '/', '/');
|
||||
$this->basePath = trim(parse_url(\MintyPHP\Router::getBaseUrl(), PHP_URL_PATH) ?: '/', '/');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,7 +36,7 @@ class LocaleResolver
|
||||
$path = parse_url($uri, PHP_URL_PATH) ?: '';
|
||||
$query = parse_url($uri, PHP_URL_QUERY) ?: '';
|
||||
|
||||
$relativePath = $this->stripBasePath($path);
|
||||
$relativePath = Request::stripBasePath($path);
|
||||
$segments = $relativePath === '' ? [] : explode('/', $relativePath);
|
||||
|
||||
$localeCandidate = $segments[0] ?? '';
|
||||
@@ -120,21 +119,6 @@ class LocaleResolver
|
||||
return $this->defaultLocale;
|
||||
}
|
||||
|
||||
private function stripBasePath(string $path): string
|
||||
{
|
||||
$relativePath = ltrim($path, '/');
|
||||
|
||||
if ($this->basePath !== '' && strpos($relativePath, $this->basePath . '/') === 0) {
|
||||
return substr($relativePath, strlen($this->basePath) + 1);
|
||||
}
|
||||
|
||||
if ($this->basePath !== '' && $relativePath === $this->basePath) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $relativePath;
|
||||
}
|
||||
|
||||
private function looksLikeLocale(string $candidate): bool
|
||||
{
|
||||
return preg_match('/^[a-z]{2}(?:-[a-z]{2})?$/i', $candidate) === 1;
|
||||
|
||||
@@ -7,20 +7,27 @@ use MintyPHP\I18n;
|
||||
|
||||
class Request
|
||||
{
|
||||
public static function stripBasePath(string $path): string
|
||||
{
|
||||
$base = trim(parse_url(Router::getBaseUrl(), PHP_URL_PATH) ?: '/', '/');
|
||||
$path = ltrim($path, '/');
|
||||
|
||||
if ($base !== '' && strpos($path, $base . '/') === 0) {
|
||||
return substr($path, strlen($base) + 1);
|
||||
}
|
||||
if ($base !== '' && $path === $base) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
public static function path(): string
|
||||
{
|
||||
$uri = $_SERVER['REQUEST_URI'] ?? '';
|
||||
$path = parse_url($uri, PHP_URL_PATH) ?: '';
|
||||
$base = trim(Router::getBaseUrl(), '/');
|
||||
$path = ltrim($path, '/');
|
||||
|
||||
if ($base !== '' && strpos($path, $base . '/') === 0) {
|
||||
$path = substr($path, strlen($base) + 1);
|
||||
} elseif ($base !== '' && $path === $base) {
|
||||
$path = '';
|
||||
}
|
||||
|
||||
return $path;
|
||||
return self::stripBasePath($path);
|
||||
}
|
||||
|
||||
public static function safeReturnTarget(string $returnParam = ''): string
|
||||
@@ -28,15 +35,8 @@ class Request
|
||||
if ($returnParam !== '') {
|
||||
$parts = parse_url($returnParam);
|
||||
if (($parts['scheme'] ?? '') === '' && ($parts['host'] ?? '') === '') {
|
||||
$path = $parts['path'] ?? '';
|
||||
$path = self::stripBasePath($parts['path'] ?? '');
|
||||
$query = isset($parts['query']) ? '?' . $parts['query'] : '';
|
||||
$base = trim(Router::getBaseUrl(), '/');
|
||||
$path = ltrim($path, '/');
|
||||
if ($base !== '' && strpos($path, $base . '/') === 0) {
|
||||
$path = substr($path, strlen($base) + 1);
|
||||
} elseif ($base !== '' && $path === $base) {
|
||||
$path = '';
|
||||
}
|
||||
return $path . $query;
|
||||
}
|
||||
}
|
||||
@@ -47,15 +47,8 @@ class Request
|
||||
$host = $parts['host'] ?? '';
|
||||
$currentHost = $_SERVER['HTTP_HOST'] ?? '';
|
||||
if ($host === '' || $host === $currentHost) {
|
||||
$path = $parts['path'] ?? '';
|
||||
$path = self::stripBasePath($parts['path'] ?? '');
|
||||
$query = isset($parts['query']) ? '?' . $parts['query'] : '';
|
||||
$base = trim(Router::getBaseUrl(), '/');
|
||||
$path = ltrim($path, '/');
|
||||
if ($base !== '' && strpos($path, $base . '/') === 0) {
|
||||
$path = substr($path, strlen($base) + 1);
|
||||
} elseif ($base !== '' && $path === $base) {
|
||||
$path = '';
|
||||
}
|
||||
return $path . $query;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user