create(); return $requestInput; } function formErrors(): FormErrors { return new FormErrors(); } function formErrorsFrom(array|FormErrors $errors): FormErrors { return formErrors()->merge($errors); } function flashFormErrors(FormErrors $errors, ?string $scope = null, string $keyPrefix = 'form_error'): void { $index = 0; foreach ($errors->toFlatList() as $message) { $key = $keyPrefix . '_' . $index; Flash::error((string) $message, $scope, $key); $index++; } } /** * Enforce POST request method for mutation actions. * * Browser requests are redirected to $redirect. * Optional JSON mode returns 405 with a JSON error payload. */ function actionRequirePost( string $redirect, bool $jsonAware = false, string $jsonError = 'method_not_allowed' ): bool { if (requestInput()->isMethod('POST')) { return true; } if ($jsonAware && Request::wantsJson()) { header('Allow: POST'); http_response_code(405); Router::json(['error' => trim($jsonError) !== '' ? trim($jsonError) : 'method_not_allowed']); return false; } Router::redirect($redirect); return false; } /** * Enforce CSRF protection for POST actions. * * Returns false after responding/redirecting when token validation fails. */ function actionRequireCsrf( string $redirect, ?string $flashScope = null, string $flashKeyPrefix = 'csrf_expired', bool $jsonAware = false, string $jsonError = 'csrf', bool $flashOnFailure = true, bool $redirectOnFailure = true ): bool { if (Session::checkCsrfToken()) { return true; } if ($jsonAware && Request::wantsJson()) { http_response_code(400); Router::json(['error' => trim($jsonError) !== '' ? trim($jsonError) : 'csrf']); return false; } if ($flashOnFailure) { $errorBag = formErrors(); $errorBag->addGlobal(t('Form expired, please try again')); flashFormErrors($errorBag, $flashScope ?? $redirect, $flashKeyPrefix); } if ($redirectOnFailure) { Router::redirect($redirect); } return false; } /** * Handle authorization decisions consistently for JSON and browser requests. */ function actionAllowOrDeny( AuthorizationDecision $decision, string $redirect = 'error/forbidden', bool $jsonAware = true ): bool { if ($decision->isAllowed()) { return true; } if ($jsonAware && Request::wantsJson()) { http_response_code($decision->status()); Router::json(['error' => $decision->error()]); return false; } if ($redirect === 'error/forbidden') { Router::redirect('error/forbidden?url=' . urlencode(Request::pathWithQuery())); return false; } Router::redirect($redirect); return false; } /** * Consume and remove flash messages matching any provided scope. * * @param array $scopes * @return array> */ function flashConsumeForScopes(array $scopes): array { return Flash::consumeForScopes($scopes); } function requestExtractNestedReturnValue(mixed $value): string { if (is_string($value)) { return trim($value); } if (!is_array($value)) { return ''; } for ($i = count($value) - 1; $i >= 0; $i--) { $item = $value[$i] ?? null; if (is_string($item) && trim($item) !== '') { return trim($item); } } return ''; } function requestLooksLikeDetailPath(string $path): bool { $path = trim($path, '/'); if ($path === '') { return false; } $segments = array_values(array_filter(explode('/', $path), static fn (string $segment): bool => $segment !== '')); return in_array('edit', $segments, true) || in_array('create', $segments, true); } function requestNormalizeSafeInternalTarget(string $target, int $maxDepth = 5): string { $target = trim($target); if ($target === '') { return ''; } $parts = parse_url($target); if ( !is_array($parts) || ($parts['scheme'] ?? '') !== '' || ($parts['host'] ?? '') !== '' ) { return ''; } $safeTarget = Request::safeReturnTarget($target); if ($safeTarget === '') { return ''; } $safeParts = parse_url($safeTarget); if (!is_array($safeParts)) { return ''; } $path = Request::stripLocale(Request::stripBasePath((string) ($safeParts['path'] ?? ''))); if ($path === '') { return ''; } $queryParams = []; parse_str((string) ($safeParts['query'] ?? ''), $queryParams); $nestedReturnTarget = requestExtractNestedReturnValue($queryParams['return'] ?? null); unset($queryParams['return']); $query = http_build_query($queryParams); $normalizedOuterTarget = $path . ($query !== '' ? '?' . $query : ''); if ($maxDepth > 0 && $nestedReturnTarget !== '' && requestLooksLikeDetailPath($path)) { $resolvedNestedTarget = requestNormalizeSafeInternalTarget($nestedReturnTarget, $maxDepth - 1); if ($resolvedNestedTarget !== '') { return $resolvedNestedTarget; } } return $normalizedOuterTarget; } function requestResolveReturnTargetFromInput(RequestInput $input, string $fallback = ''): string { $normalizedFallback = requestNormalizeSafeInternalTarget($fallback); $candidate = $input->body('return', ''); if (!is_string($candidate)) { $candidate = ''; } $candidate = trim($candidate); if ($candidate === '') { $candidate = $input->query('return', ''); if (!is_string($candidate)) { $candidate = ''; } $candidate = trim($candidate); } if ($candidate !== '') { $resolved = requestNormalizeSafeInternalTarget($candidate); if ($resolved !== '') { if ($normalizedFallback === '') { return $resolved; } $resolvedPath = (string) parse_url($resolved, PHP_URL_PATH); if ($resolvedPath !== '' && !requestLooksLikeDetailPath($resolvedPath)) { return $resolved; } } } return $normalizedFallback; } function requestResolveReturnTarget(string $fallback = ''): string { return requestResolveReturnTargetFromInput(requestInput(), $fallback); } function requestPathWithReturnTarget(string $path, string $returnTarget = ''): string { $resolvedPath = requestNormalizeSafeInternalTarget($path); if ($resolvedPath === '') { return ''; } $returnTarget = trim($returnTarget); if ($returnTarget === '') { return $resolvedPath; } $normalizedReturnTarget = requestNormalizeSafeInternalTarget($returnTarget); if ($normalizedReturnTarget === '') { return $resolvedPath; } $parts = parse_url($resolvedPath); if (!is_array($parts)) { return ''; } $pathValue = Request::stripLocale(Request::stripBasePath((string) ($parts['path'] ?? ''))); if ($pathValue === '') { return ''; } $queryParams = []; parse_str((string) ($parts['query'] ?? ''), $queryParams); unset($queryParams['return']); $queryParams['return'] = $normalizedReturnTarget; $query = http_build_query($queryParams); return $pathValue . ($query !== '' ? '?' . $query : ''); }