. */ namespace SP\Http; use Exception; use Klein\DataCollection\DataCollection; use Klein\DataCollection\HeaderDataCollection; use SP\Bootstrap; use SP\Core\Crypt\CryptPKI; use SP\Core\Crypt\Hash; use SP\Core\Exceptions\SPException; use SP\Util\Filter; use SP\Util\Util; /** * Clase Request para la gestión de peticiones HTTP * * @package SP */ final class Request { /** * @var array Directorios seguros para include */ const SECURE_DIRS = ['css', 'js']; /** * @var HeaderDataCollection */ private $headers; /** * @var \Klein\Request */ private $request; /** * @var DataCollection */ private $params; /** * @var string */ private $method; /** * @var bool */ private $https; /** * Request constructor. * * @param \Klein\Request $request */ public function __construct(\Klein\Request $request) { $this->request = $request; $this->headers = $this->request->headers(); $this->params = $this->getParamsByMethod(); $this->detectHttps(); } /** * @return DataCollection */ private function getParamsByMethod() { if ($this->request->method('GET')) { $this->method = 'GET'; return $this->request->paramsGet(); } else { $this->method = 'POST'; return $this->request->paramsPost(); } } /** * Detects if the connection is done through HTTPS */ private function detectHttps() { $this->https = Util::boolval($this->request->server()->get('HTTPS', 'off')) || $this->request->server()->get('SERVER_PORT', 0) === 443; } /** * Devuelve un nombre de archivo seguro * * @param string $file * @param string $base * * @return string */ public static function getSecureAppFile(string $file, string $base = null) { return basename(self::getSecureAppPath($file, $base)); } /** * Devolver una ruta segura para * * @param string $path * @param string $base * * @return string */ public static function getSecureAppPath(string $path, string $base = null) { if ($base === null) { $base = APP_ROOT; } elseif (!in_array(basename($base), self::SECURE_DIRS, true)) { return ''; } $realPath = realpath($base . DIRECTORY_SEPARATOR . $path); if ($realPath === false || strpos($realPath, $base) !== 0 ) { return ''; } return $realPath; } /** * @param bool $fullForwarded * * @return array|string */ public function getClientAddress(bool $fullForwarded = false) { if (APP_MODULE === 'tests') { return '127.0.0.1'; } $forwarded = $this->getForwardedFor(); if ($forwarded !== null) { return $fullForwarded ? implode(',', $forwarded) : $forwarded[0]; } return $this->request->server()->get('REMOTE_ADDR', ''); } /** * @return string[]|null */ public function getForwardedFor() { // eg: Forwarded: by=; for=; host=; proto= $forwarded = $this->headers->get('HTTP_FORWARDED'); if ($forwarded !== null && preg_match_all( '/(?:for=([\w.:]+))|(?:for="\[([\w.:]+)\]")/i', $forwarded, $matches) ) { return array_filter(array_merge($matches[1], $matches[2]), function ($value) { return !empty($value); }); } // eg: X-Forwarded-For: 192.0.2.43, 2001:db8:cafe::17 $xForwarded = $this->headers->get('HTTP_X_FORWARDED_FOR'); if ($xForwarded !== null) { $matches = preg_split('/(?<=[\w])+,\s?/i', $xForwarded, -1, PREG_SPLIT_NO_EMPTY); if (count($matches) > 0) { return $matches; } } return null; } /** * Comprobar si se realiza una recarga de la página * * @return bool */ public function checkReload() { return $this->headers->get('Cache-Control') === 'max-age=0'; } /** * @param string $param * @param string $default * * @return string|null */ public function analyzeEmail(string $param, string $default = null) { if (!$this->params->exists($param)) { return $default; } return Filter::getEmail($this->params->get($param)); } /** * Analizar un valor encriptado y devolverlo desencriptado * * @param string $param * * @return string */ public function analyzeEncrypted(string $param) { $encryptedData = $this->analyzeString($param); if ($encryptedData === null) { return ''; } try { // Desencriptar con la clave RSA $clearData = Bootstrap::getContainer()->get(CryptPKI::class) ->decryptRSA(base64_decode($encryptedData)); // Desencriptar con la clave RSA if ($clearData === false) { logger('No RSA encrypted data from request'); return $encryptedData; } return $clearData; } catch (Exception $e) { processException($e); return $encryptedData; } } /** * @param $param * @param $default * * @return string|null */ public function analyzeString(string $param, string $default = null) { if (!$this->params->exists($param)) { return $default; } return Filter::getString($this->params->get($param)); } /** * @param $param * @param $default * * @return string|null */ public function analyzeUnsafeString(string $param, string $default = null) { if (!$this->params->exists($param)) { return $default; } return Filter::getRaw($this->params->get($param)); } /** * @param string $param * @param callable|null $mapper * @param mixed $default * * @return array|null */ public function analyzeArray(string $param, callable $mapper = null, $default = null) { $requestValue = $this->params->get($param); if ($requestValue !== null && is_array($requestValue) ) { if (is_callable($mapper)) { return $mapper($requestValue); } return array_map(function ($value) { return is_numeric($value) ? Filter::getInt($value) : Filter::getString($value); }, $requestValue); } return $default; } /** * Comprobar si la petición es en formato JSON * * @return bool */ public function isJson() { return strpos($this->headers->get('Accept'), 'application/json') !== false; } /** * Comprobar si la petición es Ajax * * @return bool */ public function isAjax() { return $this->headers->get('X-Requested-With') === 'XMLHttpRequest' || $this->analyzeInt('isAjax', 0) === 1; } /** * @param string $param * @param int $default * * @return int */ public function analyzeInt(string $param, int $default = null) { if (!$this->params->exists($param)) { return $default; } return Filter::getInt($this->params->get($param)); } /** * @param string $file * * @return array|null */ public function getFile(string $file) { return $this->request->files()->get($file); } /** * @param string $param * @param bool $default * * @return bool */ public function analyzeBool(string $param, bool $default = null) { if (!$this->params->exists($param)) { return (bool)$default; } return Util::boolval($this->params->get($param)); } /** * @param string $key * @param string $param Checks the signature only for the given param * * @throws SPException */ public function verifySignature(string $key, string $param = null) { $result = false; $hash = $this->params->get('h'); if ($hash !== null) { // Strips out the hash param from the URI to get the // route which will be checked against the computed HMAC if ($param === null) { $uri = str_replace('&h=' . $hash, '', $this->request->uri()); $uri = substr($uri, strpos($uri, '?') + 1); } else { $uri = $this->params->get($param, ''); } $result = Hash::checkMessage($uri, $key, $hash); } if ($result === false) { throw new SPException( 'URI string altered', SPException::ERROR, null, 1 ); } } /** * Returns the URI used by the browser and checks for the protocol used * * @see https://tools.ietf.org/html/rfc7239#section-7.5 * @return string */ public function getHttpHost(): string { $forwarded = $this->getForwardedData(); // Check in style of RFC 7239 if (null !== $forwarded) { return strtolower($forwarded['proto'] . '://' . $forwarded['host']); } $xForward = $this->getXForwardedData(); // Check (deprecated) de facto standard if (null !== $xForward) { return strtolower($xForward['proto'] . '://' . $xForward['host']); } $protocol = 'http://'; // We got called directly if ($this->https) { $protocol = 'https://'; } return $protocol . $this->request->server()->get('HTTP_HOST'); } /** * Devolver datos de forward RFC 7239 * * @see https://tools.ietf.org/html/rfc7239#section-7.5 * @return array|null */ public function getForwardedData() { $forwarded = $this->getHeader('HTTP_FORWARDED'); // Check in style of RFC 7239 if (!empty($forwarded) && preg_match('/proto=(\w+);/i', $forwarded, $matchesProto) && preg_match('/host=(\w+);/i', $forwarded, $matchesHost) ) { $data = [ 'host ' => $matchesHost[0], 'proto' => $matchesProto[0], 'for' => $this->getForwardedFor() ]; // Check if protocol and host are not empty if (!empty($data['proto']) && !empty($data['host'])) { return $data; } } return null; } /** * @param string $header * * @return string */ public function getHeader(string $header): string { return $this->headers->get($header, ''); } /** * Devolver datos de x-forward * * @return array|null */ public function getXForwardedData() { $forwardedHost = $this->getHeader('HTTP_X_FORWARDED_HOST'); $forwardedProto = $this->getHeader('HTTP_X_FORWARDED_PROTO'); // Check (deprecated) de facto standard if (!empty($forwardedHost) && !empty($forwardedProto)) { $data = [ 'host' => trim(str_replace('"', '', $forwardedHost)), 'proto' => trim(str_replace('"', '', $forwardedProto)), 'for' => $this->getForwardedFor() ]; // Check if protocol and host are not empty if (!empty($data['host']) && !empty($data['proto'])) { return $data; } } return null; } /** * @return string */ public function getMethod(): string { return $this->method; } /** * @return bool */ public function isHttps(): bool { return $this->https; } /** * @return int */ public function getServerPort(): int { return (int)$this->request->server()->get('SERVER_PORT', 80); } /** * @return \Klein\Request */ public function getRequest(): \Klein\Request { return $this->request; } /** * @param string $key * * @return string */ public function getServer(string $key): string { return (string)$this->request->server()->get($key, ''); } }