Skip to content
107 changes: 47 additions & 60 deletions formwork/src/Admin/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
use Formwork\Assets;
use Formwork\Formwork;
use Formwork\Page;
use Formwork\Response\JSONResponse;
use Formwork\Response\RedirectResponse;
use Formwork\Response\Response;
use Formwork\Router\RouteParams;
use Formwork\Router\Router;
use Formwork\Translations\Translation;
use Formwork\Utils\FileSystem;
use Formwork\Utils\Header;
use Formwork\Utils\HTTPRequest;
use Formwork\Utils\JSONResponse;
use Formwork\Utils\Notification;
use Formwork\Utils\Session;
use Formwork\Utils\Str;
Expand Down Expand Up @@ -97,7 +98,7 @@ public function translation(): Translation
/**
* Run the administration panel
*/
public function run(): void
public function run(): Response
{
$this->loadSchemes();

Expand All @@ -109,24 +110,45 @@ public function run(): void
$this->loadRoutes();

if (HTTPRequest::method() === 'POST') {
$this->validateContentLength();
$this->validateCSRFToken();
// Validate HTTP request Content-Length according to post_max_size directive
if (HTTPRequest::contentLength() !== null) {
$maxSize = FileSystem::shorthandToBytes(ini_get('post_max_size'));
if (HTTPRequest::contentLength() > $maxSize && $maxSize > 0) {
$this->notify($this->translate('admin.request.error.post-max-size'), 'error');
return $this->redirectToReferer();
}
}

// Validate CSRF token
try {
CSRFToken::validate();
} catch (RuntimeException $e) {
CSRFToken::destroy();
Session::remove('FORMWORK_USERNAME');
$this->notify($this->translate('admin.login.suspicious-request-detected'), 'warning');
if (HTTPRequest::isXHR()) {
return JSONResponse::error('Bad Request: the CSRF token is not valid', 400);
}
return $this->redirect('/login/');
}
}

if ($this->users->isEmpty()) {
$this->registerAdmin();
return $this->registerAdmin();
}

if (!$this->isLoggedIn() && $this->route() !== '/login/') {
Session::set('FORMWORK_REDIRECT_TO', $this->route());
$this->redirect('/login/');
return $this->redirect('/login/');
}

$this->router->dispatch();
$response = $this->router->dispatch();

if (!$this->router->hasDispatched()) {
$this->errors->notFound();
$response = $this->errors->notFound();
}

return $response;
}

/**
Expand Down Expand Up @@ -199,29 +221,29 @@ public function route(): string
*
* @param int $code HTTP redirect status code
*/
public function redirect(string $route, int $code = 302): void
public function redirect(string $route, int $code = 302): RedirectResponse
{
Header::redirect($this->uri($route), $code);
return new RedirectResponse($this->uri($route), $code);
}

/**
* Redirect to the site index page
*
* @param int $code HTTP redirect status code
*/
public function redirectToSite(int $code = 302): void
public function redirectToSite(int $code = 302): RedirectResponse
{
Header::redirect($this->siteUri(), $code);
return new RedirectResponse($this->siteUri(), $code);
}

/**
* Redirect to the administration panel
*
* @param int $code HTTP redirect status code
*/
public function redirectToPanel(int $code = 302): void
public function redirectToPanel(int $code = 302): RedirectResponse
{
$this->redirect('/', $code);
return $this->redirect('/', $code);
}

/**
Expand All @@ -230,13 +252,12 @@ public function redirectToPanel(int $code = 302): void
* @param int $code HTTP redirect status code
* @param string $default Default route if HTTP referer is not available
*/
public function redirectToReferer(int $code = 302, string $default = '/'): void
public function redirectToReferer(int $code = 302, string $default = '/'): RedirectResponse
{
if (HTTPRequest::validateReferer($this->uri('/')) && HTTPRequest::referer() !== Uri::current()) {
Header::redirect(HTTPRequest::referer(), $code);
} else {
Header::redirect($this->uri($default), $code);
return new RedirectResponse(HTTPRequest::referer(), $code);
}
return new RedirectResponse($this->uri($default), $code);
}

/**
Expand Down Expand Up @@ -301,58 +322,24 @@ protected function loadErrorHandler(): void
{
$this->errors = new Controllers\ErrorsController();
set_exception_handler(function (Throwable $exception): void {
$this->errors->internalServerError($exception);
$this->errors->internalServerError($exception)->send();
throw $exception;
});
}

/**
* Validate HTTP request Content-Length according to post_max_size directive
* and notify if not valid
*/
protected function validateContentLength(): void
{
if (HTTPRequest::contentLength() !== null) {
$maxSize = FileSystem::shorthandToBytes(ini_get('post_max_size'));
if (HTTPRequest::contentLength() > $maxSize && $maxSize > 0) {
$this->notify($this->translate('admin.request.error.post-max-size'), 'error');
$this->redirectToReferer();
}
}
}

/**
* Validate CSRF token and redirect to login view if not valid
*/
protected function validateCSRFToken(): void
{
try {
CSRFToken::validate();
} catch (RuntimeException $e) {
CSRFToken::destroy();
Session::remove('FORMWORK_USERNAME');
$this->notify($this->translate('admin.login.suspicious-request-detected'), 'warning');
if (HTTPRequest::isXHR()) {
JSONResponse::error('Bad Request: the CSRF token is not valid', 400)->send();
}
$this->redirect('/login/');
}
}

/**
* Register administration panel if no user exists
*/
protected function registerAdmin(): void
protected function registerAdmin(): Response
{
if (!HTTPRequest::isLocalhost()) {
$this->redirectToSite();
return $this->redirectToSite();
}
if ($this->router->request() !== '/') {
$this->redirectToPanel();
return $this->redirectToPanel();
}
$controller = new Controllers\RegisterController();
$controller->register();
exit;
return $controller->register();
}

/**
Expand All @@ -363,8 +350,8 @@ protected function loadRoutes(): void
// Default route
$this->router->add(
'/',
function (RouteParams $params): void {
$this->redirect('/dashboard/');
function (RouteParams $params): Response {
return $this->redirect('/dashboard/');
}
);

Expand Down
2 changes: 1 addition & 1 deletion formwork/src/Admin/Controllers/AbstractController.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ protected function ensurePermission(string $permission): void
{
if (!$this->user()->permissions()->has($permission)) {
$errors = new ErrorsController();
$errors->forbidden();
$errors->forbidden()->send();
exit;
}
}
Expand Down
32 changes: 16 additions & 16 deletions formwork/src/Admin/Controllers/AuthenticationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
use Formwork\Admin\Security\AccessLimiter;
use Formwork\Admin\Security\CSRFToken;
use Formwork\Formwork;
use Formwork\Response\RedirectResponse;
use Formwork\Response\Response;
use Formwork\Utils\HTTPRequest;
use Formwork\Utils\Log;
use Formwork\Utils\Registry;
Expand All @@ -15,7 +17,7 @@ class AuthenticationController extends AbstractController
/**
* Authentication@login action
*/
public function login(): void
public function login(): Response
{
$attemptsRegistry = new Registry(Formwork::instance()->config()->get('admin.paths.logs') . 'accessAttempts.json');

Expand All @@ -27,22 +29,21 @@ public function login(): void

if ($limiter->hasReachedLimit()) {
$minutes = round(Formwork::instance()->config()->get('admin.login_reset_time') / 60);
$this->error($this->admin()->translate('admin.login.attempt.too-many', $minutes));
return;
return $this->error($this->admin()->translate('admin.login.attempt.too-many', $minutes));
}

switch (HTTPRequest::method()) {
case 'GET':
if (Session::has('FORMWORK_USERNAME')) {
$this->admin()->redirectToPanel();
return $this->admin()->redirectToPanel();
}

// Always generate a new CSRF token
CSRFToken::generate();

$this->view('authentication.login', [
return new Response($this->view('authentication.login', [
'title' => $this->admin()->translate('admin.login.login')
]);
], true));

break;

Expand Down Expand Up @@ -78,13 +79,13 @@ public function login(): void

if (($destination = Session::get('FORMWORK_REDIRECT_TO')) !== null) {
Session::remove('FORMWORK_REDIRECT_TO');
$this->admin()->redirect($destination);
return $this->admin()->redirect($destination);
}

$this->admin()->redirectToPanel();
return $this->admin()->redirectToPanel();
}

$this->error($this->admin()->translate('admin.login.attempt.failed'), [
return $this->error($this->admin()->translate('admin.login.attempt.failed'), [
'username' => $data->get('username'),
'error' => true
]);
Expand All @@ -96,18 +97,17 @@ public function login(): void
/**
* Authentication@logout action
*/
public function logout(): void
public function logout(): RedirectResponse
{
CSRFToken::destroy();
Session::remove('FORMWORK_USERNAME');
Session::destroy();

if (Formwork::instance()->config()->get('admin.logout_redirect') === 'home') {
$this->admin()->redirectToSite();
} else {
$this->admin()->notify($this->admin()->translate('admin.login.logged-out'), 'info');
$this->admin()->redirectToPanel();
return $this->admin()->redirectToSite();
}
$this->admin()->notify($this->admin()->translate('admin.login.logged-out'), 'info');
return $this->admin()->redirectToPanel();
}

/**
Expand All @@ -116,13 +116,13 @@ public function logout(): void
* @param string $message Error message
* @param array $data Data to pass to the view
*/
protected function error(string $message, array $data = []): void
protected function error(string $message, array $data = []): Response
{
// Ensure CSRF token is re-generated
CSRFToken::generate();

$defaults = ['title' => $this->admin()->translate('admin.login.login')];
$this->admin()->notify($message, 'error');
$this->view('authentication.login', array_merge($defaults, $data));
return new Response($this->view('authentication.login', array_merge($defaults, $data), true));
}
}
22 changes: 11 additions & 11 deletions formwork/src/Admin/Controllers/BackupController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,49 +5,49 @@
use Formwork\Admin\Backupper;
use Formwork\Exceptions\TranslatedException;
use Formwork\Formwork;
use Formwork\Response\FileResponse;
use Formwork\Response\JSONResponse;
use Formwork\Response\Response;
use Formwork\Router\RouteParams;
use Formwork\Utils\FileSystem;
use Formwork\Utils\HTTPResponse;
use Formwork\Utils\JSONResponse;
use RuntimeException;

class BackupController extends AbstractController
{
/**
* Backup@make action
*/
public function make(): void
public function make(): JSONResponse
{
$this->ensurePermission('backup.make');
$backupper = new Backupper();
try {
$file = $backupper->backup();
} catch (TranslatedException $e) {
JSONResponse::error($this->admin()->translate('admin.backup.error.cannot-make', $e->getTranslatedMessage()), 500)->send();
return JSONResponse::error($this->admin()->translate('admin.backup.error.cannot-make', $e->getTranslatedMessage()), 500);
}
$filename = basename($file);
JSONResponse::success($this->admin()->translate('admin.backup.ready'), 200, [
return JSONResponse::success($this->admin()->translate('admin.backup.ready'), 200, [
'filename' => $filename,
'uri' => $this->admin()->uri('/backup/download/' . urlencode(base64_encode($filename)) . '/')
])->send();
]);
}

/**
* Backup@download action
*/
public function download(RouteParams $params): void
public function download(RouteParams $params): Response
{
$this->ensurePermission('backup.download');
$file = Formwork::instance()->config()->get('backup.path') . base64_decode($params->get('backup'));
try {
if (FileSystem::isFile($file, false)) {
HTTPResponse::download($file);
} else {
throw new RuntimeException($this->admin()->translate('admin.backup.error.cannot-download.invalid-filename'));
return new FileResponse($file, true);
}
throw new RuntimeException($this->admin()->translate('admin.backup.error.cannot-download.invalid-filename'));
} catch (TranslatedException $e) {
$this->admin()->notify($this->admin()->translate('admin.backup.error.cannot-download', $e->getTranslatedMessage()), 'error');
$this->admin()->redirectToReferer(302, '/dashboard/');
return $this->admin()->redirectToReferer(302, '/dashboard/');
}
}
}
6 changes: 3 additions & 3 deletions formwork/src/Admin/Controllers/CacheController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@
namespace Formwork\Admin\Controllers;

use Formwork\Formwork;
use Formwork\Utils\JSONResponse;
use Formwork\Response\JSONResponse;

class CacheController extends AbstractController
{
/**
* Cache@clear action
*/
public function clear(): void
public function clear(): JSONResponse
{
$this->ensurePermission('cache.clear');
if (Formwork::instance()->config()->get('cache.enabled')) {
Formwork::instance()->cache()->clear();
}
JSONResponse::success($this->admin()->translate('admin.cache.cleared'))->send();
return JSONResponse::success($this->admin()->translate('admin.cache.cleared'));
}
}
Loading