Skip to content

Commit

Permalink
Redirect away (#87)
Browse files Browse the repository at this point in the history
  • Loading branch information
pascalbaljet authored Oct 23, 2022
1 parent 5a9f6dd commit 8598ac7
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 0 deletions.
2 changes: 2 additions & 0 deletions app/resources/views/navigation/nav.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<Link dusk="notFound" href="/navigation/notFound">notFound</Link>
<Link dusk="serverError" href="/navigation/serverError">serverError</Link>
<Link dusk="video" href="/navigation/video/one">Video</Link>
<Link dusk="redirectToTwo" href="/navigation/redirectToTwo">redirectToTwo</Link>
<Link dusk="away" href="/navigation/away">Away</Link>
<Link dusk="lazy" href="/lazy">Lazy</Link>

<Link confirm dusk="confirm" href="/navigation/two">Confirm to two</Link>
Expand Down
2 changes: 2 additions & 0 deletions app/routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@
Route::get('navigation/video/one', [NavigationController::class, 'videoOne'])->name('navigation.videoOne');
Route::get('navigation/video/two', [NavigationController::class, 'videoTwo'])->name('navigation.videoTwo');

Route::get('navigation/redirectToTwo', fn () => redirect()->route('navigation.two'))->name('navigation.redirectToTwo');
Route::get('navigation/away', fn () => redirect()->away('https://splade.dev/'))->name('navigation.away');
Route::get('navigation/notFound', fn () => abort(404))->name('navigation.notFound');
Route::get('navigation/serverError', fn () => throw new Exception('Whoops!'))->name('navigation.serverError');

Expand Down
26 changes: 26 additions & 0 deletions app/tests/Browser/NavigationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,32 @@ public function it_can_navigate_without_reloading_the_whole_page()
});
}

/** @test */
public function it_can_redirect_without_reloading_the_whole_page()
{
$this->browse(function (Browser $browser) {
$browser->visit('/navigation/one')
->waitForText('NavigationOne')
->tap(fn (Browser $browser) => $browser->script('document.body.classList.add("persistent")'))
->click('@redirectToTwo')
->waitForText('NavigationTwo')
->assertRouteIs('navigation.two')
->tap(fn (Browser $browser) => $this->assertStringContainsString('persistent', $browser->element('')->getAttribute('class')));
});
}

/** @test */
public function it_can_redirect_away_from_the_splade_spa()
{
$this->browse(function (Browser $browser) {
$browser->visit('/navigation/one')
->waitForText('NavigationOne')
->click('@away')
->waitUntilMissingText('NavigationOne')
->assertUrlIs('https://splade.dev/');
});
}

/** @test */
public function it_can_use_the_back_and_forward_buttons()
{
Expand Down
33 changes: 33 additions & 0 deletions app/tests/Unit/SpladeCoreTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Tests\Unit;

use Illuminate\Http\JsonResponse;
use ProtoneMedia\Splade\Facades\Splade;
use ProtoneMedia\Splade\SpladeCore;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Tests\TestCase;

class SpladeCoreTest extends TestCase
{
/** @test */
public function it_generates_a_regular_redirect_on_non_splade_requests()
{
$redirect = Splade::redirectAway('https://splade.dev');

$this->assertInstanceOf(RedirectResponse::class, $redirect);
$this->assertEquals('https://splade.dev', $redirect->getTargetUrl());
}

/** @test */
public function it_can_generate_a_409_response_with_a_target_url()
{
request()->headers->set(SpladeCore::HEADER_SPLADE, true);

$redirect = Splade::redirectAway('https://splade.dev');

$this->assertInstanceOf(JsonResponse::class, $redirect);
$this->assertEquals(409, $redirect->status());
$this->assertEquals('https://splade.dev', $redirect->headers->get(SpladeCore::HEADER_REDIRECT_AWAY));
}
}
28 changes: 28 additions & 0 deletions app/tests/Unit/SpladeMiddlewareTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Tests\Unit;

use ProtoneMedia\Splade\Http\SpladeMiddleware;
use Tests\TestCase;

class SpladeMiddlewareTest extends TestCase
{
/** @test */
public function it_can_format_a_url_into_a_host_and_port_with_defaults()
{
$this->assertEquals('splade.dev:80', SpladeMiddleware::urlToHostAndPort('http://splade.dev'));
$this->assertEquals('splade.dev:443', SpladeMiddleware::urlToHostAndPort('https://splade.dev'));
$this->assertEquals('splade.dev:8080', SpladeMiddleware::urlToHostAndPort('http://splade.dev:8080'));
$this->assertEquals('splade.dev:8080', SpladeMiddleware::urlToHostAndPort('https://splade.dev:8080'));

$this->assertEquals('splade.dev:80', SpladeMiddleware::urlToHostAndPort('http://splade.dev/path'));
$this->assertEquals('splade.dev:443', SpladeMiddleware::urlToHostAndPort('https://splade.dev/path'));
$this->assertEquals('splade.dev:8080', SpladeMiddleware::urlToHostAndPort('http://splade.dev:8080/path'));
$this->assertEquals('splade.dev:8080', SpladeMiddleware::urlToHostAndPort('https://splade.dev:8080/path'));

$this->assertEquals('splade.dev:80', SpladeMiddleware::urlToHostAndPort('splade.dev/path'));
$this->assertEquals('splade.dev:80', SpladeMiddleware::urlToHostAndPort('splade.dev:80/path'));
$this->assertEquals('splade.dev:443', SpladeMiddleware::urlToHostAndPort('splade.dev:443/path'));
$this->assertEquals('splade.dev:8080', SpladeMiddleware::urlToHostAndPort('splade.dev:8080/path'));
}
}
4 changes: 4 additions & 0 deletions lib/Splade.js
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,10 @@ function request(url, method, data, headers, replace) {
.catch((error) => {
fireEvent("request-error", { url, method, data, headers, replace, error });

if (error.response.status == 409 && error.response.headers["x-splade-redirect-away"]) {
return window.location = error.response.headers["x-splade-redirect-away"];
}

// Though the request has failed, it may still
// return Splade data with validation errors.
const spladeData = error.response.data.splade;
Expand Down
2 changes: 2 additions & 0 deletions src/Facades/Splade.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use ProtoneMedia\Splade\EventRedirectFactory;
use ProtoneMedia\Splade\EventRefresh;
use ProtoneMedia\Splade\SpladeToast;
use Symfony\Component\HttpFoundation\Response;

/**
* @method static array getShared()
Expand All @@ -31,6 +32,7 @@
* @method static SpladeToast toastOnEvent(string $message = '')
* @method static string getModalKey()
* @method static string modalType()
* @method static Response redirectAway(string $targetUrl)
*
* @see \ProtoneMedia\Splade\SpladeCore
*/
Expand Down
56 changes: 56 additions & 0 deletions src/Http/SpladeMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response as LaravelResponse;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
use ProtoneMedia\Splade\Facades\Splade;
use ProtoneMedia\Splade\SpladeCore;
use ProtoneMedia\Splade\Ssr;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;

Expand Down Expand Up @@ -57,6 +60,10 @@ public function handle(Request $request, Closure $next)
// Gather the required meta data for the app.
$spladeData = $this->spladeData($request->session());

if ($response instanceof RedirectResponse && $this->redirectsAway($response)) {
return Splade::redirectAway($response->getTargetUrl());
}

// If the response is a redirect, put the toasts into the session
// so they won't be lost in the next request.
if (in_array($response->getStatusCode(), [302, 303])) {
Expand Down Expand Up @@ -275,6 +282,55 @@ private function spladeData(Session $session): object
];
}

/**
* Returns a boolean whether the Target URL is outside of the app.
*
* @param \Illuminate\Http\RedirectResponse $redirect
* @return bool
*/
private function redirectsAway(RedirectResponse $redirect): bool
{
$targetUrl = $redirect->getTargetUrl();

if (Str::startsWith($targetUrl, '/')) {
return false;
}

/** @var UrlGenerator $urlGenerator */
$urlGenerator = app('url');

$appUrl = $urlGenerator->format(
$urlGenerator->formatRoot($urlGenerator->formatScheme()) ?: config('app.url'),
'/'
);

return $this->urlToHostAndPort($appUrl) !== $this->urlToHostAndPort($targetUrl);
}

/**
* Maps a full URL to a host:port formatted string.
*
* @param string $url
* @return string
*/
public static function urlToHostAndPort(string $url): string
{
if (!parse_url($url, PHP_URL_HOST)) {
$url = "http://{$url}";
}

$parsed = parse_url($url);

$host = $parsed['host'] ?? 'host';
$port = $parsed['port'] ?? null;

if (!$port) {
$port = $parsed['scheme'] === 'http' ? 80 : 443;
}

return "{$host}:{$port}";
}

/**
* Renders the Confirm and ToastWrapper components.
*
Expand Down
22 changes: 22 additions & 0 deletions src/SpladeCore.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

use Closure;
use Illuminate\Foundation\Exceptions\Handler;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Validation\ValidationException;
use ProtoneMedia\Splade\Http\ResolvableData;
use Symfony\Component\HttpFoundation\Response;
use Throwable;

class SpladeCore
Expand All @@ -20,6 +22,8 @@ class SpladeCore

const HEADER_LAZY = 'X-Splade-Lazy';

const HEADER_REDIRECT_AWAY = 'X-Splade-Redirect-Away';

const MODAL_TYPE_MODAL = 'modal';

const MODAL_TYPE_SLIDEOVER = 'slideover';
Expand Down Expand Up @@ -362,4 +366,22 @@ public function getModalType(): string
default => static::MODAL_TYPE_MODAL
};
}

/**
* Returns a JSON response that indicates that the Splade frontend
* should redirect to an external URL.
*
* @param string $targetUrl
* @return \Symfony\Component\HttpFoundation\Response
*/
public function redirectAway(string $targetUrl): Response
{
if (!$this->isSpladeRequest()) {
return redirect()->away($targetUrl);
}

return new JsonResponse(null, 409, [
static::HEADER_REDIRECT_AWAY => $targetUrl,
]);
}
}

0 comments on commit 8598ac7

Please sign in to comment.