|
7 | 7 | use Amp\ByteStream\ReadableResourceStream; |
8 | 8 | use Amp\Http\Cookie\RequestCookie; |
9 | 9 | use Amp\Http\Server\DefaultErrorHandler; |
10 | | -use Amp\Http\Server\FormParser\BufferedFile; |
11 | | -use Amp\Http\Server\FormParser\Form; |
12 | 10 | use Amp\Http\Server\HttpServer as AmpHttpServer; |
13 | 11 | use Amp\Http\Server\HttpServerStatus; |
14 | 12 | use Amp\Http\Server\Request as AmpRequest; |
|
19 | 17 | use Illuminate\Contracts\Http\Kernel as HttpKernel; |
20 | 18 | use Illuminate\Foundation\Testing\Concerns\WithoutExceptionHandlingHandler; |
21 | 19 | use Illuminate\Http\Request; |
22 | | -use Illuminate\Http\UploadedFile; |
23 | 20 | use Illuminate\Routing\UrlGenerator; |
24 | 21 | use Illuminate\Support\Uri; |
25 | 22 | use Pest\Browser\Contracts\HttpServer; |
26 | 23 | use Pest\Browser\Exceptions\ServerNotFoundException; |
27 | 24 | use Pest\Browser\Execution; |
28 | 25 | use Pest\Browser\GlobalState; |
| 26 | +use Pest\Browser\Http\ExtendedFormParser; |
29 | 27 | use Pest\Browser\Playwright\Playwright; |
30 | 28 | use Psr\Log\NullLogger; |
31 | 29 | use Symfony\Component\Mime\MimeTypes; |
@@ -53,14 +51,27 @@ final class LaravelHttpServer implements HttpServer |
53 | 51 | */ |
54 | 52 | private ?Throwable $lastThrowable = null; |
55 | 53 |
|
| 54 | + /** |
| 55 | + * The multipart parser wrapper with upload validation behavior. |
| 56 | + */ |
| 57 | + private ExtendedFormParser $extendedFormParser; |
| 58 | + |
56 | 59 | /** |
57 | 60 | * Creates a new laravel http server instance. |
58 | 61 | */ |
59 | 62 | public function __construct( |
60 | 63 | public readonly string $host, |
61 | 64 | public readonly int $port, |
62 | 65 | ) { |
63 | | - // |
| 66 | + $this->extendedFormParser = ExtendedFormParser::fromIni(); |
| 67 | + } |
| 68 | + |
| 69 | + /** |
| 70 | + * Overrides the multipart parser instance. |
| 71 | + */ |
| 72 | + public function setExtendedFormParser(ExtendedFormParser $extendedFormParser): void |
| 73 | + { |
| 74 | + $this->extendedFormParser = $extendedFormParser; |
64 | 75 | } |
65 | 76 |
|
66 | 77 | /** |
@@ -383,170 +394,6 @@ private function rewriteAssetUrl(string $content): string |
383 | 394 | */ |
384 | 395 | private function parseMultipartFormData(AmpRequest $request): array |
385 | 396 | { |
386 | | - $form = Form::fromRequest($request); |
387 | | - |
388 | | - $values = $form->getValues(); |
389 | | - $files = $form->getFiles(); |
390 | | - |
391 | | - return [ |
392 | | - $this->normalizeMultipartParameters($values), |
393 | | - $this->normalizeMultipartFiles($files), |
394 | | - ]; |
395 | | - } |
396 | | - |
397 | | - /** |
398 | | - * Normalize multipart field values to a Symfony request-compatible array. |
399 | | - * |
400 | | - * @param array<string, list<string>> $fields |
401 | | - * @return array<int|string, mixed> |
402 | | - */ |
403 | | - private function normalizeMultipartParameters(array $fields): array |
404 | | - { |
405 | | - $normalized = []; |
406 | | - |
407 | | - foreach ($fields as $field => $values) { |
408 | | - foreach ($values as $value) { |
409 | | - $this->setFieldValue($normalized, $field, $value); |
410 | | - } |
411 | | - } |
412 | | - |
413 | | - return $normalized; |
414 | | - } |
415 | | - |
416 | | - /** |
417 | | - * Normalize multipart files to a Symfony request-compatible files array. |
418 | | - * |
419 | | - * @param array<string, list<BufferedFile>> $files |
420 | | - * @return array<int|string, mixed> |
421 | | - */ |
422 | | - private function normalizeMultipartFiles(array $files): array |
423 | | - { |
424 | | - $normalized = []; |
425 | | - |
426 | | - foreach ($files as $field => $fileEntries) { |
427 | | - foreach ($fileEntries as $fileEntry) { |
428 | | - $this->setFieldValue($normalized, $field, $this->createUploadedFile($fileEntry)); |
429 | | - } |
430 | | - } |
431 | | - |
432 | | - return $normalized; |
433 | | - } |
434 | | - |
435 | | - /** |
436 | | - * @param array<int|string, mixed> $target |
437 | | - */ |
438 | | - private function setFieldValue(array &$target, string $field, string|UploadedFile $value): void |
439 | | - { |
440 | | - $segments = $this->fieldSegments($field); |
441 | | - |
442 | | - if ($segments === []) { |
443 | | - return; |
444 | | - } |
445 | | - |
446 | | - $this->setNestedFieldValue($target, $segments, $value); |
447 | | - } |
448 | | - |
449 | | - /** |
450 | | - * @return list<string> |
451 | | - */ |
452 | | - private function fieldSegments(string $field): array |
453 | | - { |
454 | | - if (! str_contains($field, '[')) { |
455 | | - return [$field]; |
456 | | - } |
457 | | - |
458 | | - $segments = []; |
459 | | - $head = mb_strstr($field, '[', true); |
460 | | - |
461 | | - if ($head !== false && $head !== '') { |
462 | | - $segments[] = $head; |
463 | | - } |
464 | | - |
465 | | - preg_match_all('/\[([^\]]*)\]/', $field, $matches); |
466 | | - |
467 | | - foreach ($matches[1] as $segment) { |
468 | | - $segments[] = $segment; |
469 | | - } |
470 | | - |
471 | | - return $segments; |
472 | | - } |
473 | | - |
474 | | - /** |
475 | | - * @param array<int|string, mixed> $target |
476 | | - * @param list<string> $segments |
477 | | - */ |
478 | | - private function setNestedFieldValue(array &$target, array $segments, string|UploadedFile $value): void |
479 | | - { |
480 | | - $segment = array_shift($segments); |
481 | | - |
482 | | - if ($segment === null) { |
483 | | - return; |
484 | | - } |
485 | | - |
486 | | - if ($segments === []) { |
487 | | - if ($segment === '') { |
488 | | - $target[] = $value; |
489 | | - |
490 | | - return; |
491 | | - } |
492 | | - |
493 | | - if (! array_key_exists($segment, $target)) { |
494 | | - $target[$segment] = $value; |
495 | | - |
496 | | - return; |
497 | | - } |
498 | | - |
499 | | - if (! is_array($target[$segment])) { |
500 | | - $target[$segment] = [$target[$segment]]; |
501 | | - } |
502 | | - |
503 | | - $target[$segment][] = $value; |
504 | | - |
505 | | - return; |
506 | | - } |
507 | | - |
508 | | - if ($segment === '') { |
509 | | - $target[] = []; |
510 | | - |
511 | | - $lastKey = array_key_last($target); |
512 | | - if (! is_array($target[$lastKey])) { |
513 | | - return; |
514 | | - } |
515 | | - |
516 | | - $this->setNestedFieldValue($target[$lastKey], $segments, $value); |
517 | | - |
518 | | - return; |
519 | | - } |
520 | | - |
521 | | - if (! isset($target[$segment]) || ! is_array($target[$segment])) { |
522 | | - $target[$segment] = []; |
523 | | - } |
524 | | - |
525 | | - $this->setNestedFieldValue($target[$segment], $segments, $value); |
526 | | - } |
527 | | - |
528 | | - private function createUploadedFile(object $fileEntry): UploadedFile |
529 | | - { |
530 | | - $tempPath = tempnam(sys_get_temp_dir(), 'pest-browser-upload-'); |
531 | | - assert($tempPath !== false, 'Failed to create temporary upload file.'); |
532 | | - |
533 | | - $contents = method_exists($fileEntry, 'getContents') ? $fileEntry->getContents() : ''; |
534 | | - $contents = is_string($contents) ? $contents : ''; |
535 | | - |
536 | | - file_put_contents($tempPath, $contents); |
537 | | - |
538 | | - $clientFilename = method_exists($fileEntry, 'getName') ? $fileEntry->getName() : 'upload'; |
539 | | - $clientFilename = is_string($clientFilename) && $clientFilename !== '' ? $clientFilename : 'upload'; |
540 | | - |
541 | | - $mimeType = method_exists($fileEntry, 'getMimeType') ? $fileEntry->getMimeType() : 'application/octet-stream'; |
542 | | - $mimeType = is_string($mimeType) && $mimeType !== '' ? $mimeType : 'application/octet-stream'; |
543 | | - |
544 | | - return new UploadedFile( |
545 | | - $tempPath, |
546 | | - $clientFilename, |
547 | | - $mimeType, |
548 | | - UPLOAD_ERR_OK, |
549 | | - true, |
550 | | - ); |
| 397 | + return $this->extendedFormParser->parseMultipart($request); |
551 | 398 | } |
552 | 399 | } |
0 commit comments