Skip to content
Merged
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
137 changes: 135 additions & 2 deletions src/Phaseolies/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
use Phaseolies\Http\Request;
use Phaseolies\Http\Contracts\GatewayInterface;
use Phaseolies\Http\Exceptions\HttpException;
use Phaseolies\Http\Exceptions\HttpResponseException;
use Phaseolies\Session\Contracts\CommittableSession;
use Phaseolies\Error\ErrorHandler;
use Phaseolies\DI\Container;
use Phaseolies\Config\Config;
Expand Down Expand Up @@ -1210,7 +1212,7 @@ public function defer(callable $callback): self
if (!$scope instanceof DeferScope) {
throw new \LogicException(
'defer() was called outside an active execution scope. '
. 'Run the code through runInDeferScope(), an HTTP dispatch or a console command.'
. 'Run the code through runInDeferScope(), an HTTP dispatch or a console command.'
);
}

Expand Down Expand Up @@ -1547,15 +1549,19 @@ public function dispatch($request): DispatchResult

$response = $this->handle($request);

$this->commitSession($request, $response);

$response->prepare($request)->send();

$this->terminateSession($request);

$this->suspendDeferScope($deferScope);
$this->suspendContext($context);

return new DispatchResult($this, $request, $response, null, $deferScope, $context);
} catch (HttpException $e) {
try {
Response::dispatchHttpException($e);
$this->renderHttpException($request, $e);
} catch (\Throwable $renderFailure) {
$this->endDeferScope($deferScope, $renderFailure);
$this->endContext($context, $renderFailure);
Expand All @@ -1568,13 +1574,140 @@ public function dispatch($request): DispatchResult

return new DispatchResult($this, $request, null, $e, $deferScope, $context);
} catch (\Throwable $e) {
// Validation failures and abort() are thrown as exceptions that carry
// the response to send (usually a redirect with flashed errors and
// old input). The global error handler sends it, so the session
// must be saved onto that response here, before it is rethrown.
if ($e instanceof HttpResponseException && $e->hasResponse()) {
$this->commitSessionSafely($request, $e->getResponse());
} else {
$this->endSessionSafely($request);
}

$this->endDeferScope($deferScope, $e);
$this->endContext($context, $e);

throw $e;
}
}

/**
* Persist the session of the request
*
* @param Request $request
* @param Response|null $response
* @return void
*/
protected function commitSession(Request $request, ?Response $response = null): void
{
$session = $request->session();

if ($session instanceof CommittableSession) {
$session->commit($response);
}
}

/**
* Render the error page of an HTTP exception, then save the session.
*
* @param Request $request
* @param HttpException $exception
* @return void
*/
protected function renderHttpException(Request $request, HttpException $exception): void
{
$level = ob_get_level();

ob_start();

try {
Response::dispatchHttpException($exception);
} catch (\Throwable $failure) {
$this->commitSessionSafely($request);

while (ob_get_level() > $level) {
ob_end_clean();
}

throw $failure;
}

$this->commitSessionSafely($request);

while (ob_get_level() > $level) {
ob_end_flush();
}
}

/**
* Persist the session without ever replacing the exception that is already ending the request
*
* @param Request $request
* @param Response|null $response
* @return void
*/
protected function commitSessionSafely(Request $request, ?Response $response = null): void
{
try {
$this->commitSession($request, $response);
} catch (\Throwable $failure) {
$this->reportSessionFailure($failure);
}
}

/**
* Apply a logout of a request that is failing, ignoring everything else.
*
* @param Request $request
* @return void
*/
protected function endSessionSafely(Request $request): void
{
try {
$session = $request->session();

if ($session instanceof CommittableSession) {
$session->commitEnd();
}
} catch (\Throwable $failure) {
$this->reportSessionFailure($failure);
}
}

/**
* Log a session operation that failed after the request could no longer be told about it
*
* @param \Throwable $failure
* @return void
*/
protected function reportSessionFailure(\Throwable $failure): void
{
try {
error('[Doppar] A session operation failed and its changes were lost: ' . $failure->getMessage(), ['exception' => $failure]);
} catch (\Throwable) {
// Logging must never turn a lost save into a second failure.
}
}

/**
* Run the session housekeeping once the response has been sent
*
* @param Request $request
* @return void
*/
protected function terminateSession(Request $request): void
{
$session = $request->session();

if ($session instanceof CommittableSession) {
try {
$session->terminate();
} catch (\Throwable $failure) {
$this->reportSessionFailure($failure);
}
}
}

/**
* Describe a request for its execution context. Must never throw.
*
Expand Down
17 changes: 5 additions & 12 deletions src/Phaseolies/Console/Commands/ClearSessionCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Phaseolies\Console\Commands;

use Phaseolies\Console\Schedule\Command;
use Phaseolies\Session\SessionManager;

class ClearSessionCommand extends Command
{
Expand All @@ -14,11 +15,11 @@ class ClearSessionCommand extends Command
protected $name = 'session:clear';

/**
* The name of the console command.
* The description of the console command.
*
* @var string
*/
protected $description = 'Clear all sessions from the Applicaion.';
protected $description = 'Clear all sessions from the Application.';

/**
* Execute the console command.
Expand All @@ -27,17 +28,9 @@ class ClearSessionCommand extends Command
*/
public function handle(): int
{
return $this->withTiming(function() {
$sessionDir = base_path('storage/framework/sessions');

$files = glob($sessionDir . '/*');
foreach ($files as $file) {
if (is_file($file)) {
@unlink($file);
}
}
return $this->withTiming(function () {
SessionManager::store()->flush();

session()->flush();
return Command::SUCCESS;
}, 'Application sessions have been gracefully cleared.');
}
Expand Down
39 changes: 39 additions & 0 deletions src/Phaseolies/Console/Commands/SessionGcCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace Phaseolies\Console\Commands;

use Phaseolies\Console\Schedule\Command;
use Phaseolies\Session\SessionManager;

class SessionGcCommand extends Command
{
/**
* The name of the console command.
*
* @var string
*/
protected $name = 'session:gc';

/**
* The description of the console command.
*
* @var string
*/
protected $description = 'Remove every expired session from the session store.';

/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
return $this->withTiming(function () {
$removed = SessionManager::store()->gc(SessionManager::config()['lifetime'] * 60);

$this->info("Removed {$removed} expired session(s).");

return Command::SUCCESS;
}, 'Expired sessions have been removed.');
}
}
4 changes: 3 additions & 1 deletion src/Phaseolies/Error/WebErrorRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,10 @@ public function renderDebug(Throwable $exception): Response
'current_route_name' => \Phaseolies\Support\Facades\Route::currentRouteName(),
'current_route_action' => \Phaseolies\Support\Facades\Route::currentRouteAction(),
'current_route_params' => request()->getRouteParams()
]);
], true);

// Rendered to a string (not echoed) so the status code and headers of
// the response are still applied when it is sent.
return response($content, $this->resolveStatusCode($exception))
->setOriginal($content)
->setStatusCode($this->resolveStatusCode($exception));
Expand Down
2 changes: 1 addition & 1 deletion src/Phaseolies/Helpers/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ function session($key = null, $default = null): mixed
*/
function csrf_token(): ?string
{
return session('_token') ?? null;
return session()->token();
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/Phaseolies/Http/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Phaseolies\Utilities\IpUtils;
use Phaseolies\Support\Session;
use Phaseolies\Session\Contracts\CommittableSession;
use Phaseolies\Session\Contracts\SessionInterface;
use Phaseolies\Support\File;
use Phaseolies\Support\Facades\Str;
Expand Down Expand Up @@ -164,6 +165,10 @@ public function __construct(
$this->session = $container->has(SessionInterface::class)
? $container->get(SessionInterface::class)
: new Session();

if ($this->session instanceof CommittableSession) {
$this->session->attach($this);
}
$this->files = $files;
$this->content = $content;
$this->requestUri = $this->getPath();
Expand Down
1 change: 1 addition & 0 deletions src/Phaseolies/Http/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,7 @@ public function getStatusCodeText(int $statusCode): string
416 => 'Range Not Satisfiable',
417 => 'Expectation Failed',
418 => 'I\'m a Teapot',
419 => 'Page Expired',
421 => 'Misdirected Request',
422 => 'Unprocessable Entity',
423 => 'Locked',
Expand Down
7 changes: 2 additions & 5 deletions src/Phaseolies/Http/Support/RequestParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Phaseolies\Http\Support;

use Phaseolies\Utilities\IpUtils;
use Phaseolies\Utilities\PathMatcher;

trait RequestParser
{
Expand Down Expand Up @@ -305,11 +306,7 @@ public function contentLength(): ?int
*/
public static function is(string $pattern): bool
{
$path = request()->uri();

$pattern = str_replace('\*', '.*', preg_quote($pattern, '#'));

return (bool) preg_match('#^' . $pattern . '$#i', $path);
return PathMatcher::matches($pattern, request()->uri());
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/Phaseolies/Launchers/SessionLauncher.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Phaseolies\Launchers;

use Phaseolies\Session\ConfigSession as Session;
use Phaseolies\Session\SessionManager;

class SessionLauncher extends ServiceLauncher
{
Expand All @@ -13,7 +13,7 @@ class SessionLauncher extends ServiceLauncher
*/
public function register()
{
Session::configAppSession();
SessionManager::initialize();
}

/**
Expand Down
Loading
Loading