Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"ext-sockets": "*",
"amphp/amp": "^3.1.1",
"amphp/http-server": "^3.4.4",
"amphp/http-server-form-parser": "^2.0.0",
"amphp/websocket-client": "^2.0.2",
"pestphp/pest": "^4.3.2",
"pestphp/pest-plugin": "^4.0.0",
Expand Down
88 changes: 83 additions & 5 deletions src/Drivers/LaravelHttpServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Pest\Browser\Exceptions\ServerNotFoundException;
use Pest\Browser\Execution;
use Pest\Browser\GlobalState;
use Pest\Browser\Http\ExtendedFormParser;
use Pest\Browser\Playwright\Playwright;
use Psr\Log\NullLogger;
use Symfony\Component\Mime\MimeTypes;
Expand Down Expand Up @@ -50,14 +51,19 @@ final class LaravelHttpServer implements HttpServer
*/
private ?Throwable $lastThrowable = null;

/**
* The multipart parser wrapper with upload validation behavior.
*/
private ExtendedFormParser $extendedFormParser;

/**
* Creates a new laravel http server instance.
*/
public function __construct(
public readonly string $host,
public readonly int $port,
) {
//
$this->extendedFormParser = ExtendedFormParser::fromIni();
}

/**
Expand All @@ -69,6 +75,14 @@ public function __destruct()
// $this->stop();
}

/**
* Overrides the multipart parser instance.
*/
public function setExtendedFormParser(ExtendedFormParser $extendedFormParser): void
{
$this->extendedFormParser = $extendedFormParser;
}

/**
* Rewrite the given URL to match the server's host and port.
*/
Expand Down Expand Up @@ -239,22 +253,46 @@ private function handleRequest(AmpRequest $request): Response

$contentType = $request->getHeader('content-type') ?? '';
$method = mb_strtoupper($request->getMethod());
$rawBody = (string) $request->getBody();
$parameters = [];
if ($method !== 'GET' && str_starts_with(mb_strtolower($contentType), 'application/x-www-form-urlencoded')) {
parse_str($rawBody, $parameters);
$files = [];

if ($method !== 'GET' && str_starts_with(mb_strtolower($contentType), 'multipart/form-data')) {
[$parameters, $files] = $this->parseMultipartFormData($request);

$rawBody = '';
} else {
$rawBody = (string) $request->getBody();

if ($method !== 'GET' && str_starts_with(mb_strtolower($contentType), 'application/x-www-form-urlencoded')) {
parse_str($rawBody, $parameters);
}
}

$cookies = array_map(fn (RequestCookie $cookie): string => urldecode($cookie->getValue()), $request->getCookies());
$cookies = array_merge($cookies, test()->prepareCookiesForRequest()); // @phpstan-ignore-line
/** @var array<string, string> $serverVariables */
$serverVariables = test()->serverVariables(); // @phpstan-ignore-line

if ($contentType !== '') {
$serverVariables['CONTENT_TYPE'] = $contentType;
}

$contentLength = $request->getHeader('content-length');
if ($contentLength !== null && $contentLength !== '') {
$serverVariables['CONTENT_LENGTH'] = $contentLength;
}

$contentMd5 = $request->getHeader('content-md5');
if ($contentMd5 !== null && $contentMd5 !== '') {
$serverVariables['CONTENT_MD5'] = $contentMd5;
}

$symfonyRequest = Request::create(
$absoluteUrl,
$method,
$parameters,
$cookies,
[], // @TODO files...
$files,
$serverVariables,
$rawBody
);
Expand All @@ -271,6 +309,9 @@ private function handleRequest(AmpRequest $request): Response
$symfonyRequest->server->set('HTTP_HOST', $hostHeader);
}

$superglobalState = $this->captureRequestSuperglobals();
$symfonyRequest->overrideGlobals();

$debug = config('app.debug');

try {
Expand All @@ -283,6 +324,7 @@ private function handleRequest(AmpRequest $request): Response
throw $e;
} finally {
config(['app.debug' => $debug]);
$this->restoreRequestSuperglobals($superglobalState);
}

$kernel->terminate($laravelRequest, $response);
Expand Down Expand Up @@ -362,4 +404,40 @@ private function rewriteAssetUrl(string $content): string

return str_replace($this->originalAssetUrl, $this->url(), $content);
}

/**
* Parse multipart form data and return request parameters and files.
*
* @return array{array<int|string, mixed>, array<int|string, mixed>}
*/
private function parseMultipartFormData(AmpRequest $request): array
{
return $this->extendedFormParser->parseMultipart($request);
}

/**
* @return array{get: array<int|string, mixed>, post: array<int|string, mixed>, request: array<int|string, mixed>, server: array<int|string, mixed>, cookie: array<int|string, mixed>}
*/
private function captureRequestSuperglobals(): array
{
return [
'get' => $_GET,
'post' => $_POST,
'request' => $_REQUEST,
'server' => $_SERVER,
'cookie' => $_COOKIE,
];
}

/**
* @param array{get: array<int|string, mixed>, post: array<int|string, mixed>, request: array<int|string, mixed>, server: array<int|string, mixed>, cookie: array<int|string, mixed>} $superglobalState
*/
private function restoreRequestSuperglobals(array $superglobalState): void
{
$_GET = $superglobalState['get'];
$_POST = $superglobalState['post'];
$_REQUEST = $superglobalState['request'];
$_SERVER = $superglobalState['server'];
$_COOKIE = $superglobalState['cookie'];
}
}
Loading