Repository: livewire/livewire (php)

Issue:
X-Amz-Expires must be non-negative while file upload since 2.3.7
### Description
I've upgraded from Livewire 2.3.6 to 2.3.7 (also tried to 2.3.8, same result).
My file upload (to S3) suddenly stopped working. When I open de network tab in my browser I can this request with a 400 status:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<Error>
    <Code>AuthorizationQueryParametersError</Code>
    <Message>X-Amz-Expires must be non-negative</Message>
    <RequestId>784898D8C4E388F4</RequestId>
    <HostId>vPRsnfz9YMKe0FXpF4Nk513SP4VNff8hszxRdZArjhXrZM2k2p6QdyfXM1vT1t4Pwtfu8dKOEwY=</HostId>
</Error>
```
When I go back to Livewire 2.3.5 it works.

### Exact steps to reproduce
In my blade file I've stripped down all fancy styling stuff to a bare minimum:
```html
<input type="file" id="newImage" name="newImage" wire:model="newImage">
```
### Stripped-down, copy-pastable code snippets

**Context**
- Livewire version: 2.3.6
- Laravel version:8.24.0
- Alpine version: 2.3.5
- Browser: Safari & Chrome


Pull request:
Set max_upload_time fallback default if config doesn't exist
This fixes #2395 where temporary uploads to S3 were failing for apps that already had a published config and were missing the max_upload_time key introduced in #2347.

See other PR #2412 in relation to merging configs and why that won't work for this config option.

So instead I've had to set a default fallback if the config option is missing.

Hope this helps!

Patch:
diff --git a/composer.json b/composer.json
index 21f50bb1..4eff90b9 100644
--- a/composer.json
+++ b/composer.json
@@ -13,7 +13,8 @@
         "symfony/http-kernel": "^5.0",
         "illuminate/support": "^7.0|^8.0",
         "illuminate/database": "^7.0|^8.0",
-        "illuminate/validation": "^7.0|^8.0"
+        "illuminate/validation": "^7.0|^8.0",
+        "spatie/laravel-ray": "^1.9"
     },
     "require-dev": {
         "psy/psysh": "@stable",
diff --git a/src/Connection/ConnectionHandler.php b/src/Connection/ConnectionHandler.php
index 2653dcfe..a9dca87f 100644
--- a/src/Connection/ConnectionHandler.php
+++ b/src/Connection/ConnectionHandler.php
@@ -2,9 +2,10 @@
 
 namespace Livewire\Connection;
 
+use Illuminate\Routing\Controller;
 use Livewire\LifecycleManager;
 
-abstract class ConnectionHandler
+abstract class ConnectionHandler extends Controller
 {
     public function handle($payload)
     {
diff --git a/src/Controllers/HttpConnectionHandler.php b/src/Controllers/HttpConnectionHandler.php
index d0d6fafa..714be2ee 100644
--- a/src/Controllers/HttpConnectionHandler.php
+++ b/src/Controllers/HttpConnectionHandler.php
@@ -2,10 +2,27 @@
 
 namespace Livewire\Controllers;
 
+use Livewire\Livewire;
+use Illuminate\Support\Facades\Request;
 use Livewire\Connection\ConnectionHandler;
 
 class HttpConnectionHandler extends ConnectionHandler
 {
+    public function __construct()
+    {
+        $originalUrl = Livewire::originalUrl();
+
+        $route = app('router')->getRoutes()->match(
+            Request::create($originalUrl, 'GET')
+        );
+
+        $originalRequestMiddleware = $route->gatherMiddleware();
+
+        $configuredMiddlewareGroup = config('livewire.middleware_group', 'web');
+
+        $this->middleware([$configuredMiddlewareGroup, ...$originalRequestMiddleware]);
+    }
+
     public function __invoke()
     {
         return $this->handle(
diff --git a/src/FileUploadConfiguration.php b/src/FileUploadConfiguration.php
index 510daad9..8cd29d52 100644
--- a/src/FileUploadConfiguration.php
+++ b/src/FileUploadConfiguration.php
@@ -90,6 +90,6 @@ class FileUploadConfiguration
 
     public static function maxUploadTime()
     {
-        return config('livewire.temporary_file_upload.max_upload_time');
+        return config('livewire.temporary_file_upload.max_upload_time', 5);
     }
 }
diff --git a/src/LifecycleManager.php b/src/LifecycleManager.php
index d3d65434..9e31f669 100644
--- a/src/LifecycleManager.php
+++ b/src/LifecycleManager.php
@@ -28,7 +28,7 @@ class LifecycleManager
         return tap(new static, function ($instance) use ($name, $id) {
             $instance->instance = app('livewire')->getInstance($name, $id);
             $instance->request = new Request([
-                'fingerprint' => ['id' => $id, 'name' => $name, 'locale' => app()->getLocale()],
+                'fingerprint' => ['id' => $id, 'name' => $name, 'locale' => app()->getLocale(), 'url' => Livewire::originalUrl()],
                 'updates' => [],
                 'serverMemo' => [],
             ]);
@@ -42,7 +42,7 @@ class LifecycleManager
         return tap(new static, function ($instance) use ($component,  $name) {
             $instance->instance = $component;
             $instance->request = new Request([
-                'fingerprint' => ['id' => $component->id, 'name' => $name, 'locale' => app()->getLocale()],
+                'fingerprint' => ['id' => $component->id, 'name' => $name, 'locale' => app()->getLocale(), 'url' => Livewire::originalUrl()],
                 'updates' => [],
                 'serverMemo' => [],
             ]);
diff --git a/src/LivewireManager.php b/src/LivewireManager.php
index 63891e21..fbfca22f 100644
--- a/src/LivewireManager.php
+++ b/src/LivewireManager.php
@@ -2,6 +2,7 @@
 
 namespace Livewire;
 
+use Exception;
 use Livewire\Testing\TestableLivewire;
 use Illuminate\Contracts\Auth\Authenticatable;
 use Livewire\Exceptions\ComponentNotFoundException;
@@ -12,8 +13,6 @@ class LivewireManager
     protected $componentAliases = [];
     protected $queryParamsForTesting = [];
 
-    public static $isLivewireRequestTestingOverride;
-
     public function component($alias, $viewClass = null)
     {
         if (is_null($viewClass)) {
@@ -261,11 +260,28 @@ HTML;
 
     public function isLivewireRequest()
     {
-        if (static::$isLivewireRequestTestingOverride) {
-            return true;
+        $route = request()->route();
+
+        if (! $route && app()->runningUnitTests()) {
+            return false;
+        }
+
+        throw_unless(
+            $route,
+            new Exception('It\'s too early in the request to call Livewire::isLivewireRequest().')
+        );
+
+        return $route->named('livewire.message');
+
+    }
+
+    public function originalUrl()
+    {
+        if ($this->isLivewireRequest()) {
+            return request('fingerprint')['url'];
         }
 
-        return request()->hasHeader('X-Livewire');
+        return url()->current();
     }
 
     public function getRootElementTagName($dom)
diff --git a/src/LivewireServiceProvider.php b/src/LivewireServiceProvider.php
index 3461f30b..da9b6f18 100644
--- a/src/LivewireServiceProvider.php
+++ b/src/LivewireServiceProvider.php
@@ -3,8 +3,10 @@
 namespace Livewire;
 
 use Illuminate\View\View;
+use Illuminate\Testing\TestResponse;
 use Illuminate\Filesystem\Filesystem;
 use Illuminate\Support\Facades\Blade;
+use Illuminate\Support\Facades\Request;
 use Illuminate\Support\ServiceProvider;
 use Illuminate\View\ComponentAttributeBag;
 use Livewire\Controllers\FileUploadHandler;
@@ -13,7 +15,6 @@ use Livewire\Controllers\HttpConnectionHandler;
 use Livewire\Controllers\LivewireJavaScriptAssets;
 use Illuminate\Support\Facades\Route as RouteFacade;
 use Illuminate\Foundation\Http\Middleware\TrimStrings;
-use Illuminate\Testing\TestResponse;
 use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
 use Livewire\Commands\{
     CpCommand,
@@ -53,6 +54,7 @@ class LivewireServiceProvider extends ServiceProvider
         $this->registerTestMacros();
         $this->registerLivewireSingleton();
         $this->registerComponentAutoDiscovery();
+        $this->registerConfig();
     }
 
     public function boot()
@@ -106,6 +108,11 @@ class LivewireServiceProvider extends ServiceProvider
         });
     }
 
+    protected function registerConfig()
+    {
+        $this->mergeConfigFrom(__DIR__.'/../config/livewire.php', 'livewire');
+    }
+
     protected function registerViews()
     {
         // This is mainly for overriding Laravel's pagination views
@@ -118,19 +125,7 @@ class LivewireServiceProvider extends ServiceProvider
 
     protected function registerRoutes()
     {
-        if ($this->app->runningUnitTests()) {
-            RouteFacade::get('/livewire-dusk/{component}', function ($component) {
-                $class = urldecode($component);
-
-                return app()->call(new $class);
-            })->middleware('web');
-        }
-
-        RouteFacade::get('/livewire/livewire.js', [LivewireJavaScriptAssets::class, 'source']);
-        RouteFacade::get('/livewire/livewire.js.map', [LivewireJavaScriptAssets::class, 'maps']);
-
-        RouteFacade::post('/livewire/message/{name}', HttpConnectionHandler::class)
-            ->middleware(config('livewire.middleware_group', 'web'));
+        RouteFacade::post('/livewire/message/{name}', HttpConnectionHandler::class)->name('livewire.message');
 
         RouteFacade::post('/livewire/upload-file', [FileUploadHandler::class, 'handle'])
             ->middleware(config('livewire.middleware_group', 'web'))
@@ -139,6 +134,9 @@ class LivewireServiceProvider extends ServiceProvider
         RouteFacade::get('/livewire/preview-file/{filename}', [FilePreviewHandler::class, 'handle'])
             ->middleware(config('livewire.middleware_group', 'web'))
             ->name('livewire.preview-file');
+
+        RouteFacade::get('/livewire/livewire.js', [LivewireJavaScriptAssets::class, 'source']);
+        RouteFacade::get('/livewire/livewire.js.map', [LivewireJavaScriptAssets::class, 'maps']);
     }
 
     protected function registerCommands()
@@ -327,7 +325,7 @@ class LivewireServiceProvider extends ServiceProvider
 
     protected function bypassTheseMiddlewaresDuringLivewireRequests(array $middlewareToExclude)
     {
-        if (! $this->app['livewire']->isLivewireRequest()) return;
+        if (! request()->hasHeader('X-Livewire')) return;
 
         $kernel = $this->app->make(\Illuminate\Contracts\Http\Kernel::class);
 
diff --git a/src/RenameMe/SupportBrowserHistory.php b/src/RenameMe/SupportBrowserHistory.php
index f5d24044..bc3c0d21 100644
--- a/src/RenameMe/SupportBrowserHistory.php
+++ b/src/RenameMe/SupportBrowserHistory.php
@@ -19,8 +19,6 @@ class SupportBrowserHistory
 
     function __construct()
     {
-        $this->mergedQueryParamsFromDehydratedComponents = collect($this->getExistingQueryParams());
-
         Livewire::listen('component.hydrate.initial', function ($component) {
             if (! $properties = $this->getQueryParamsFromComponentProperties($component)->keys()) return;
 
@@ -138,6 +136,10 @@ class SupportBrowserHistory
 
     protected function mergeComponentPropertiesWithExistingQueryParamsFromOtherComponentsAndTheRequest($component)
     {
+        if (! $this->mergedQueryParamsFromDehydratedComponents) {
+            $this->mergedQueryParamsFromDehydratedComponents = collect($this->getExistingQueryParams());
+        }
+
         $excepts = $this->getExceptsFromComponent($component);
 
         $this->mergedQueryParamsFromDehydratedComponents = collect(request()->query())


Test patch:
diff --git a/tests/Browser/Security/Component.php b/tests/Browser/Security/Component.php
new file mode 100644
index 00000000..65d06296
--- /dev/null
+++ b/tests/Browser/Security/Component.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Tests\Browser\Security;
+
+use Livewire\Component as BaseComponent;
+
+class Component extends BaseComponent
+{
+    public $middleware = [];
+    public $showNested = false;
+
+    public function showNestedComponent()
+    {
+        $this->showNested = true;
+    }
+
+    public function render()
+    {
+        $this->middleware = app('router')->current()->gatherMiddleware();
+
+        return <<<'HTML'
+<div>
+    <span dusk="middleware">@json($middleware)</span>
+    <span dusk="url">{{ \Livewire\Livewire::isLivewireRequest() ? request('fingerprint')['url'] : '' }}</span>
+
+    <button wire:click="$refresh" dusk="refresh">Refresh</button>
+    <button wire:click="showNestedComponent" dusk="showNested">Show Nested</button>
+
+    @if ($showNested)
+        @livewire(\Tests\Browser\Security\NestedComponent::class)
+    @endif
+</div>
+HTML;
+    }
+}
diff --git a/tests/Browser/Security/NestedComponent.php b/tests/Browser/Security/NestedComponent.php
new file mode 100644
index 00000000..7531612b
--- /dev/null
+++ b/tests/Browser/Security/NestedComponent.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Tests\Browser\Security;
+
+use Livewire\Component as BaseComponent;
+
+class NestedComponent extends BaseComponent
+{
+    public $middleware = [];
+
+    public function render()
+    {
+        $this->middleware = app('router')->current()->gatherMiddleware();
+
+        return <<<'HTML'
+<div>
+    <span dusk="nested-middleware">@json($middleware)</span>
+    <span dusk="nested-url">{{ \Livewire\Livewire::isLivewireRequest() ? request('fingerprint')['url'] : '' }}</span>
+
+    <button wire:click="$refresh" dusk="refreshNested">Refresh</button>
+</div>
+HTML;
+    }
+}
diff --git a/tests/Browser/Security/Test.php b/tests/Browser/Security/Test.php
new file mode 100644
index 00000000..4a6349d8
--- /dev/null
+++ b/tests/Browser/Security/Test.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Tests\Browser\Security;
+
+use Laravel\Dusk\Browser;
+use Livewire\Livewire;
+use Tests\Browser\TestCase;
+
+class Test extends TestCase
+{
+    public function test()
+    {
+        $this->browse(function (Browser $browser) {
+            Livewire::visit($browser, Component::class)
+                // See middleware from original request.
+                ->assertSeeIn('@middleware', '["web","Tests\\\\Browser\\\\ExpectedDummyMiddleware"]')
+                ->assertDontSeeIn('@url', 'http://127.0.0.1:8001/livewire-dusk/Tests%5CBrowser%5CSecurity%5CComponent')
+
+                ->waitForLivewire()->click('@refresh')
+
+                // See that the original request middleware was re-applied.
+                ->assertSeeIn('@middleware', '["web","Tests\\\\Browser\\\\ExpectedDummyMiddleware"]')
+                ->assertSeeIn('@url', 'http://127.0.0.1:8001/livewire-dusk/Tests%5CBrowser%5CSecurity%5CComponent')
+
+                ->waitForLivewire()->click('@showNested')
+
+                // Even to nested components shown AFTER the first load.
+                ->assertSeeIn('@middleware', '["web","Tests\\\\Browser\\\\ExpectedDummyMiddleware"]')
+                ->assertSeeIn('@url', 'http://127.0.0.1:8001/livewire-dusk/Tests%5CBrowser%5CSecurity%5CComponent')
+                ->assertSeeIn('@nested-middleware', '["web","Tests\\\\Browser\\\\ExpectedDummyMiddleware"]')
+                ->assertSeeIn('@nested-url', 'http://127.0.0.1:8001/livewire-dusk/Tests%5CBrowser%5CSecurity%5CComponent')
+
+                ->waitForLivewire()->click('@refreshNested')
+
+                // Make sure they are still applied when stand-alone requests are made to that component.
+                ->assertSeeIn('@middleware', '["web","Tests\\\\Browser\\\\ExpectedDummyMiddleware"]')
+                ->assertSeeIn('@url', 'http://127.0.0.1:8001/livewire-dusk/Tests%5CBrowser%5CSecurity%5CComponent')
+                ->assertSeeIn('@nested-middleware', '["web","Tests\\\\Browser\\\\ExpectedDummyMiddleware"]')
+                ->assertSeeIn('@nested-url', 'http://127.0.0.1:8001/livewire-dusk/Tests%5CBrowser%5CSecurity%5CComponent')
+            ;
+        });
+    }
+}
diff --git a/tests/Browser/TestCase.php b/tests/Browser/TestCase.php
index 15ba70f3..b72aa6e3 100644
--- a/tests/Browser/TestCase.php
+++ b/tests/Browser/TestCase.php
@@ -78,6 +78,12 @@ class TestCase extends BaseTestCase
                 \Tests\Browser\SyncHistory\ComponentWithoutQueryString::class
             )->middleware('web')->name('sync-history-without-query-string');
 
+            Route::get('/livewire-dusk/{component}', function ($component) {
+                $class = urldecode($component);
+
+                return app()->call(new $class);
+            })->middleware('web', ExpectedDummyMiddleware::class);
+
             app('session')->put('_token', 'this-is-a-hack-because-something-about-validating-the-csrf-token-is-broken');
 
             app('config')->set('view.paths', [
@@ -211,3 +217,11 @@ class TestCase extends BaseTestCase
         return $sh->getScopeVariables(false);
     }
 }
+
+class ExpectedDummyMiddleware
+{
+    public function handle($request, $next)
+    {
+        return $next($request);
+    }
+}