<?php

declare(strict_types=1);

/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

namespace TYPO3\CMS\Core\Http;

use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Http\Message\UriInterface;
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
use TYPO3\CMS\Core\Core\Environment;

/**
 * Class to create ServerRequest objects
 *
 * Highly inspired by https://github.com/phly/http/
 *
 * @internal Note that this is not public API yet.
 */
#[AsAlias(ServerRequestFactoryInterface::class, public: true)]
readonly class ServerRequestFactory implements ServerRequestFactoryInterface
{
    /**
     * Create a new server request.
     *
     * Note that server-params are taken precisely as given - no parsing/processing
     * of the given values is performed, and, in particular, no attempt is made to
     * determine the HTTP method or URI, which must be provided explicitly.
     *
     * @param string $method The HTTP method associated with the request.
     * @param UriInterface|string $uri The URI associated with the request.
     * @param array $serverParams Array of SAPI parameters with which to seed the generated request instance.
     */
    public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
    {
        return new ServerRequest($uri, $method, null, [], $serverParams);
    }

    /**
     * Create a request from the original superglobal variables.
     *
     * @return ServerRequest
     * @throws \InvalidArgumentException when invalid file values given
     * @internal Note that this is not public API yet.
     */
    public static function fromGlobals()
    {
        $serverParameters = $_SERVER;
        $headers = static::prepareHeaders($serverParameters);

        $method = $serverParameters['REQUEST_METHOD'] ?? 'GET';
        try {
            // Note an early middleware creates NormalizedParams again to attach it as request attribute.
            // Doing this twice per request is considered ok for now since NormalizedParams is constructed
            // quite quickly with just a few string operations in it. We also may not want ot attach NormalizedParams
            // as request attribute here already to not 'pollute' ServerRequest early. Another option is to
            // hand over NormalizedParams to this method and have callers handle the decision if the attribute
            // is attached as request attribute on their own.
            $uri = new Uri(NormalizedParams::createFromServerParams($serverParameters)->getRequestUrl());
        } catch (\InvalidArgumentException $e) {
            if (Environment::isCli()) {
                throw new InvalidRequestUrlOnCliException(
                    'Usage of ' . __METHOD__ . ' on CLI is discouraged. In case you rely on the method, you have to fake a valid request URL using $_SERVER.',
                    1701105725,
                    $e,
                );
            }
            throw $e;
        }

        $request = new ServerRequest(
            $uri,
            $method,
            'php://input',
            $headers,
            $serverParameters,
            static::normalizeUploadedFiles($_FILES)
        );
        if (!empty($_COOKIE)) {
            $request = $request->withCookieParams($_COOKIE);
        }
        if (!empty($_GET)) {
            $request = $request->withQueryParams($_GET);
        }
        $parsedBody = $_POST;
        if (empty($parsedBody) && in_array($method, ['PUT', 'PATCH', 'DELETE'])) {
            parse_str((string)file_get_contents('php://input'), $parsedBody);
        }
        if (!empty($parsedBody)) {
            $request = $request->withParsedBody($parsedBody);
        }
        return $request;
    }

    /**
     * Fetch headers from $_SERVER variables
     * which are only the ones starting with HTTP_* and CONTENT_*
     *
     * @return array
     */
    protected static function prepareHeaders(array $server)
    {
        $headers = [];
        foreach ($server as $key => $value) {
            if (!is_string($key)) {
                continue;
            }
            if (str_starts_with($key, 'HTTP_COOKIE')) {
                // Cookies are handled using the $_COOKIE superglobal
                continue;
            }
            if ($value !== '') {
                if (str_starts_with($key, 'HTTP_')) {
                    $name = str_replace('_', ' ', substr($key, 5));
                    $name = str_replace(' ', '-', ucwords(strtolower($name)));
                    $name = strtolower($name);
                    $headers[$name] = $value;
                } elseif (str_starts_with($key, 'CONTENT_')) {
                    $name = substr($key, 8); // Content-
                    $name = 'Content-' . (($name === 'MD5') ? $name : ucfirst(strtolower($name)));
                    $name = strtolower($name);
                    $headers[$name] = $value;
                }
            }
        }
        return $headers;
    }

    /**
     * Normalize uploaded files
     *
     * Transforms each value into an UploadedFileInterface instance, and ensures that nested arrays are normalized.
     *
     * @param array $files
     * @return array
     * @throws \InvalidArgumentException for unrecognized values
     */
    protected static function normalizeUploadedFiles(array $files)
    {
        $normalizedFileUploads = [];
        foreach ($files as $key => $value) {
            if ($value instanceof UploadedFileInterface) {
                $normalizedFileUploads[$key] = $value;
            } elseif (is_array($value)) {
                if (isset($value['tmp_name'])) {
                    $uploadedFiles = self::createUploadedFile($value);
                    if ($uploadedFiles) {
                        $normalizedFileUploads[$key] = $uploadedFiles;
                    }
                } else {
                    $normalizedFileUploads[$key] = self::normalizeUploadedFiles($value);
                }
            } else {
                throw new \InvalidArgumentException('Invalid value in files specification.', 1436717282);
            }
        }
        return $normalizedFileUploads;
    }

    /**
     * Create and return an UploadedFile instance from a $_FILES specification.
     *
     * If the specification represents an array of values, this method will
     * recursively resolve uploaded files.
     *
     * @param array $value $_FILES structure
     * @return UploadedFileInterface[]|UploadedFileInterface|null
     */
    protected static function createUploadedFile(array $value)
    {
        if (is_array($value['tmp_name'])) {
            $files = [];
            foreach (array_keys($value['tmp_name']) as $key) {
                $data = [
                    'tmp_name' => $value['tmp_name'][$key],
                    'error'    => $value['error'][$key],
                    'name'     => $value['name'][$key],
                    'type'     => $value['type'][$key],
                ];
                if (isset($value['size'][$key])) {
                    $data['size'] = $value['size'][$key];
                }
                $result = self::createUploadedFile($data);
                if ($result) {
                    $files[$key] = $result;
                }
            }
            return $files;
        }
        if (!empty($value['tmp_name'])) {
            return new UploadedFile($value['tmp_name'], $value['size'] ?? 0, $value['error'], $value['name'], $value['type']);
        }
        return null;
    }
}
