350 lines
9.7 KiB
PHP
350 lines
9.7 KiB
PHP
<?php
|
|
|
|
namespace Leantime\Core\Http;
|
|
|
|
use Illuminate\Contracts\Container\BindingResolutionException;
|
|
use Leantime\Core\Http\RequestTypes\RequestTypeDetector;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
|
|
/**
|
|
* Incoming Request information
|
|
*/
|
|
class IncomingRequest extends \Illuminate\Http\Request
|
|
{
|
|
/**
|
|
* The decoded JSON content for the request.
|
|
*
|
|
* @var \Symfony\Component\HttpFoundation\InputBag|null
|
|
*/
|
|
protected $json;
|
|
|
|
protected $pageUrl;
|
|
|
|
protected $currentRoute;
|
|
|
|
private $basePathCalculated = false;
|
|
|
|
private $pathInfoCalculated = false;
|
|
|
|
public const HEADER_FORWARDED = parent::HEADER_FORWARDED; // When using RFC 7239
|
|
|
|
public const HEADER_X_FORWARDED_FOR = parent::HEADER_X_FORWARDED_FOR;
|
|
|
|
public const HEADER_X_FORWARDED_HOST = parent::HEADER_X_FORWARDED_HOST;
|
|
|
|
public const HEADER_X_FORWARDED_PROTO = parent::HEADER_X_FORWARDED_PROTO;
|
|
|
|
public const HEADER_X_FORWARDED_PORT = parent::HEADER_X_FORWARDED_PORT;
|
|
|
|
public const HEADER_X_FORWARDED_PREFIX = parent::HEADER_X_FORWARDED_PREFIX;
|
|
|
|
public const HEADER_X_FORWARDED_AWS_ELB = parent::HEADER_X_FORWARDED_AWS_ELB; // AWS ELB doesn't send X-Forwarded-Host
|
|
|
|
public const HEADER_X_FORWARDED_TRAEFIK = parent::HEADER_X_FORWARDED_TRAEFIK; // All "X-Forwarded-*"
|
|
|
|
// List of valid api endpoint urls
|
|
public array $apiEndpoints = [
|
|
'/api/jsonrpc',
|
|
'/mcp',
|
|
'/api',
|
|
];
|
|
|
|
public static function createFromGlobals(): static
|
|
{
|
|
return parent::createFromBase(parent::createFromGlobals());
|
|
}
|
|
|
|
public static function capture(): IncomingRequest
|
|
{
|
|
parent::enableHttpMethodParameterOverride();
|
|
|
|
$request = self::createFromGlobals();
|
|
$requestClass = RequestTypeDetector::detect($request);
|
|
|
|
return $requestClass::createFromBase($request);
|
|
}
|
|
|
|
/**
|
|
* Gets the full URL including request uri and protocol
|
|
*/
|
|
public function getFullUrl(): string
|
|
{
|
|
return $this->getSchemeAndHttpHost().$this->getBasePath().$this->getPathInfo();
|
|
}
|
|
|
|
public function getBasePath(): string
|
|
{
|
|
|
|
// Early in the stack we may not have BASE_URL yet.
|
|
// Let's have symfony deal with it
|
|
if (! defined('BASE_URL')) {
|
|
return $this->prepareBasePath();
|
|
}
|
|
|
|
// Will always only return the domain portion
|
|
if (! $this->basePathCalculated) {
|
|
$schemeHost = $this->getSchemeAndHttpHost();
|
|
$baseUrl = rtrim(BASE_URL, '/');
|
|
|
|
// Extract potential subfolder from BASE_URL
|
|
if ($baseUrl !== $schemeHost) {
|
|
$this->basePath = substr($baseUrl, strlen($schemeHost));
|
|
} else {
|
|
$this->basePath = $this->prepareBasePath();
|
|
}
|
|
|
|
$this->basePathCalculated = true;
|
|
}
|
|
|
|
return $this->basePath;
|
|
}
|
|
|
|
public function getPathInfo(): string
|
|
{
|
|
if (! $this->pathInfoCalculated) {
|
|
$pathInfo = $this->preparePathInfo();
|
|
$basePath = $this->getBasePath();
|
|
|
|
// Only strip basePath if it exists at the start of pathInfo
|
|
if ($basePath && strpos($pathInfo, $basePath) === 0) {
|
|
$this->pathInfo = substr($pathInfo, strlen($basePath));
|
|
} else {
|
|
$this->pathInfo = $pathInfo;
|
|
}
|
|
|
|
$this->pathInfoCalculated = true;
|
|
}
|
|
|
|
return $this->pathInfo;
|
|
}
|
|
|
|
/**
|
|
* Gets the request URI (path behind domain name)
|
|
* Will adjust for subfolder installations
|
|
*/
|
|
public function getRequestUri(): string
|
|
{
|
|
|
|
if ($this->requestUri === null) {
|
|
$requestUri = parent::getRequestUri();
|
|
$basePath = $this->getBasePath();
|
|
|
|
// If we have a basePath (subfolder installation)
|
|
// and it exists at the start of the requestUri,
|
|
// strip it out to get the correct relative path
|
|
if ($basePath && str_starts_with($requestUri, $basePath)) {
|
|
$requestUri = substr($requestUri, strlen($basePath));
|
|
}
|
|
|
|
// Ensure requestUri starts with a forward slash
|
|
if (! str_starts_with($requestUri, '/')) {
|
|
$requestUri = '/'.$requestUri;
|
|
}
|
|
|
|
$this->requestUri = $requestUri;
|
|
}
|
|
|
|
return $this->requestUri;
|
|
}
|
|
|
|
/**
|
|
* Gets the request params
|
|
*/
|
|
public function getRequestParams(?string $method = null): array
|
|
{
|
|
$method ??= $this->method();
|
|
$method = strtoupper($method);
|
|
$patch_vars = [];
|
|
|
|
if ($method === 'PATCH') {
|
|
parse_str($this->getContent(), $patch_vars);
|
|
}
|
|
|
|
$params = $this->query->all();
|
|
|
|
// Merge query vars with post or patch vars
|
|
return match ($method) {
|
|
'PATCH' => array_merge($params, $patch_vars),
|
|
'POST' => array_merge($this->request->all(), $params),
|
|
default => $params
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get the full URL of the current request.
|
|
* Wrapper for Laravel
|
|
*
|
|
* @return string The full URL of the current request.
|
|
*
|
|
* @Override
|
|
*/
|
|
public function fullUrl(): string
|
|
{
|
|
return $this->getFullUrl();
|
|
}
|
|
|
|
/**
|
|
* Determines whether the current request is an API or Cron request.
|
|
*
|
|
* @return bool Returns true if the request is an API or Cron request, false otherwise.
|
|
*/
|
|
public function isApiOrCronRequest(): bool
|
|
{
|
|
$requestUri = $this->getRequestUri();
|
|
|
|
return str_starts_with(strtolower($requestUri), '/api/jsonrpc') || str_starts_with($requestUri, '/cron');
|
|
}
|
|
|
|
/**
|
|
* Determines whether the current request targets the MCP server endpoint.
|
|
*
|
|
* @return bool Returns true if the request is an MCP request, false otherwise.
|
|
*/
|
|
public function isMcpRequest(): bool
|
|
{
|
|
$requestUri = strtolower($this->getRequestUri());
|
|
|
|
return $requestUri === '/mcp'
|
|
|| str_starts_with($requestUri, '/mcp/')
|
|
|| str_starts_with($requestUri, '/mcp?');
|
|
}
|
|
|
|
/**
|
|
* Determines whether the current request is an Htmx request.
|
|
*
|
|
* @return bool Returns true if the request is an Htmx request, false otherwise.
|
|
*/
|
|
public function isHtmxRequest(): bool
|
|
{
|
|
return ! empty($this->headers->get('Hx-Request'));
|
|
}
|
|
|
|
/**
|
|
* Determines whether the current request is a boosted htmx request.
|
|
*
|
|
* @return bool Returns true if the request is a boosted htmx request, false otherwise.
|
|
*/
|
|
public function isBoostedHtmxRequest(): bool
|
|
{
|
|
return $this->isHtmxRequest() &&
|
|
$this->headers->get('Hx-Boost') === 'true';
|
|
}
|
|
|
|
/**
|
|
* Determines whether the current request is an unboosted HTMX request.
|
|
*
|
|
* @return bool Returns true if the request is an unboosted HTMX request, false otherwise.
|
|
*/
|
|
public function isUnboostedHtmxRequest(): bool
|
|
{
|
|
return $this->isHtmxRequest() &&
|
|
empty($this->headers->get('Hx-Boost'));
|
|
}
|
|
|
|
public function getCurrentRoute()
|
|
{
|
|
if ($this->currentRoute === null) {
|
|
$path = $this->getPathInfo();
|
|
$path = trim($path, '/');
|
|
|
|
if (empty($path)) {
|
|
return '';
|
|
}
|
|
|
|
$route = str_replace('/', '.', $path);
|
|
|
|
$this->currentRoute = $route;
|
|
}
|
|
|
|
return $this->currentRoute;
|
|
}
|
|
|
|
public function segments(): array
|
|
{
|
|
$segments = explode('/', $this->decodedPath());
|
|
|
|
return array_values(array_filter($segments, static function ($value) {
|
|
return $value !== '';
|
|
}));
|
|
}
|
|
|
|
public function decodedPath(): string
|
|
{
|
|
return rawurldecode($this->path());
|
|
}
|
|
|
|
public function path(): string
|
|
{
|
|
$pattern = trim($this->getPathInfo(), '/');
|
|
|
|
return $pattern === '' ? '/' : $pattern;
|
|
}
|
|
|
|
public function setCurrentRoute($route): void
|
|
{
|
|
$this->currentRoute = $route;
|
|
}
|
|
|
|
/**
|
|
* Gets the module name from the given complete name or the current route.
|
|
*
|
|
* @param string|null $completeName The complete name from which to extract the module name. If not provided, the current route will be used.
|
|
* @return string The module name.
|
|
*
|
|
* @deprecated
|
|
*/
|
|
public function getModuleName(?string $completeName = null): string
|
|
{
|
|
$completeName ??= $this->getCurrentRoute();
|
|
$actionParts = explode('.', empty($completeName) ? $this->currentRoute : $completeName);
|
|
|
|
if (is_array($actionParts)) {
|
|
return $actionParts[0];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* getActionName - split string to get actionName
|
|
*
|
|
* @throws BindingResolutionException
|
|
*
|
|
* @deprecated
|
|
*/
|
|
public function getActionName(?string $completeName = null): string
|
|
{
|
|
$completeName ??= $this->getCurrentRoute();
|
|
$actionParts = explode('.', empty($completeName) ? $this->currentRoute : $completeName);
|
|
$actionName = '';
|
|
|
|
// If no action name was given, call index controller
|
|
if (is_array($actionParts) && count($actionParts) === 1) {
|
|
$actionName = 'index';
|
|
}
|
|
|
|
if (is_array($actionParts) && count($actionParts) === 2) {
|
|
$actionName = $actionParts[1];
|
|
}
|
|
|
|
return $actionName;
|
|
}
|
|
|
|
/**
|
|
* Checks if the current request is an API request.
|
|
*
|
|
* @return bool Returns true if the current request is an API request, false otherwise.
|
|
*/
|
|
public function isApiRequest(): bool
|
|
{
|
|
$requestUri = strtolower($this->getRequestUri());
|
|
|
|
// Check the endpoint
|
|
foreach ($this->apiEndpoints as $apiEndpoint) {
|
|
if (str_starts_with($requestUri, $apiEndpoint)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|