Added/updated comments

This commit is contained in:
Igor Scheller 2024-04-06 16:14:30 +02:00 committed by xuwhite
parent 9bf9bd2823
commit 905d91d6ed
27 changed files with 93 additions and 41 deletions

View File

@ -28,7 +28,7 @@ The Engelsystem may be installed manually or by using the provided [docker setup
* MySQL-Server >= 5.7.8 or MariaDB-Server >= 10.2.2 * MySQL-Server >= 5.7.8 or MariaDB-Server >= 10.2.2
* Webserver, i.e. lighttpd, nginx, or Apache * Webserver, i.e. lighttpd, nginx, or Apache
From experience 2 cores and 2GB ram are roughly enough for about 1000 Angels (~700 arrived + 500 arrived but not working) during an event. From previous experience, 2 cores and 2GB ram are roughly enough for up to 1000 Angels (~700 arrived + 500 arrived but not working) during an event.
### Download ### Download
* Go to the [Releases](https://github.com/engelsystem/engelsystem/releases) page and download the latest stable release file. * Go to the [Releases](https://github.com/engelsystem/engelsystem/releases) page and download the latest stable release file.
@ -42,7 +42,14 @@ From experience 2 cores and 2GB ram are roughly enough for about 1000 Angels (~7
* Recommended: Directory Listing should be disabled. * Recommended: Directory Listing should be disabled.
* There must be a MySQL database set up with a user who has full rights to that database. * There must be a MySQL database set up with a user who has full rights to that database.
* If necessary, create a `config/config.php` to override values from `config/config.default.php`. * If necessary, create a `config/config.php` to override values from `config/config.default.php`.
* To disable/remove values from the `themes`, `tshirt_sizes`, `headers`, `header_items`, `footer_items`, or `locales` lists, set the value of the entry to `null`. * To disable/remove values from the following lists, set the value of the entry to `null`:
* `themes`
* `tshirt_sizes`
* `headers`
* `header_items`
* `footer_items`
* `locales`
* `contact_options`
* To import the database, the `bin/migrate` script has to be run. If you can't execute scripts, you can use the `initial-install.sql` file from the release zip. * To import the database, the `bin/migrate` script has to be run. If you can't execute scripts, you can use the `initial-install.sql` file from the release zip.
* In the browser, login with credentials `admin` : `asdfasdf` and change the password. * In the browser, login with credentials `admin` : `asdfasdf` and change the password.

View File

@ -128,11 +128,14 @@ function User_get_shifts_sum_query()
' '
COALESCE(SUM( COALESCE(SUM(
(1 + ( (1 + (
/* Starts during night */
HOUR(shifts.start) >= %1$d AND HOUR(shifts.start) < %2$d HOUR(shifts.start) >= %1$d AND HOUR(shifts.start) < %2$d
/* Ends during night */
OR ( OR (
HOUR(shifts.end) > %1$d HOUR(shifts.end) > %1$d
|| HOUR(shifts.end) = %1$d AND MINUTE(shifts.end) > 0 || HOUR(shifts.end) = %1$d AND MINUTE(shifts.end) > 0
) AND HOUR(shifts.end) <= %2$d ) AND HOUR(shifts.end) <= %2$d
/* Starts before and ends after night */
OR HOUR(shifts.start) <= %1$d AND HOUR(shifts.end) >= %2$d OR HOUR(shifts.start) <= %1$d AND HOUR(shifts.end) >= %2$d
)) ))
* (UNIX_TIMESTAMP(shifts.end) - UNIX_TIMESTAMP(shifts.start)) * (UNIX_TIMESTAMP(shifts.end) - UNIX_TIMESTAMP(shifts.start))

View File

@ -6,6 +6,7 @@ use Engelsystem\Application;
use Engelsystem\Middleware\Dispatcher; use Engelsystem\Middleware\Dispatcher;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
// Include app bootstrapping
require_once realpath(__DIR__ . '/../includes/engelsystem.php'); require_once realpath(__DIR__ . '/../includes/engelsystem.php');
/** @var Application $app */ /** @var Application $app */
@ -18,4 +19,5 @@ $middleware = $app->getMiddleware();
$dispatcher = new Dispatcher($middleware); $dispatcher = new Dispatcher($middleware);
$dispatcher->setContainer($app); $dispatcher->setContainer($app);
// Handle the request
$dispatcher->handle($request); $dispatcher->handle($request);

View File

@ -14,9 +14,16 @@ class ConfigServiceProvider extends ServiceProvider
{ {
protected array $configFiles = ['app.php', 'config.default.php', 'config.php']; protected array $configFiles = ['app.php', 'config.default.php', 'config.php'];
# remember to update ConfigServiceProviderTest, config.default.php, and README.md // Remember to update ConfigServiceProviderTest, config.default.php, and README.md
protected array $configVarsToPruneNulls protected array $configVarsToPruneNulls = [
= ['themes', 'tshirt_sizes', 'headers', 'header_items', 'footer_items', 'locales', 'contact_options']; 'themes',
'tshirt_sizes',
'headers',
'header_items',
'footer_items',
'locales',
'contact_options',
];
public function __construct(Application $app, protected ?EventConfig $eventConfig = null) public function __construct(Application $app, protected ?EventConfig $eventConfig = null)
{ {
@ -29,6 +36,7 @@ class ConfigServiceProvider extends ServiceProvider
$this->app->instance(Config::class, $config); $this->app->instance(Config::class, $config);
$this->app->instance('config', $config); $this->app->instance('config', $config);
// Load configuration from files
foreach ($this->configFiles as $file) { foreach ($this->configFiles as $file) {
$file = $this->getConfigPath($file); $file = $this->getConfigPath($file);
@ -47,6 +55,7 @@ class ConfigServiceProvider extends ServiceProvider
throw new Exception('Configuration not found'); throw new Exception('Configuration not found');
} }
// Prune values with null to remove them
foreach ($this->configVarsToPruneNulls as $key) { foreach ($this->configVarsToPruneNulls as $key) {
$config->set($key, array_filter($config->get($key), function ($v) { $config->set($key, array_filter($config->get($key), function ($v) {
return !is_null($v); return !is_null($v);

View File

@ -98,6 +98,7 @@ class LocationsController extends BaseController
$location->neededAngelTypes()->getQuery()->delete(); $location->neededAngelTypes()->getQuery()->delete();
$angelsInfo = ''; $angelsInfo = '';
// Associate angel types with the room
foreach ($angelTypes as $angelType) { foreach ($angelTypes as $angelType) {
$count = $data['angel_type_' . $angelType->id]; $count = $data['angel_type_' . $angelType->id];
if (!$count) { if (!$count) {

View File

@ -113,6 +113,7 @@ class ShiftTypesController extends BaseController
$shiftType->save(); $shiftType->save();
$shiftType->neededAngelTypes()->delete(); $shiftType->neededAngelTypes()->delete();
// Associate angel types with the shift type
$angelsInfo = ''; $angelsInfo = '';
foreach ($angelTypes as $angelType) { foreach ($angelTypes as $angelType) {
$count = $data['angel_type_' . $angelType->id]; $count = $data['angel_type_' . $angelType->id];

View File

@ -69,6 +69,7 @@ class UserWorkLogController extends BaseController
'comment' => 'required|max:200', 'comment' => 'required|max:200',
]); ]);
// Search / create worklog
if (isset($worklogId)) { if (isset($worklogId)) {
$worklog = $this->worklog->findOrFail((int) $worklogId); $worklog = $this->worklog->findOrFail((int) $worklogId);

View File

@ -70,6 +70,7 @@ class FeedController extends BaseController
$shift = $entry->shift; $shift = $entry->shift;
// Data required for the Fahrplan app integration https://github.com/johnjohndoe/engelsystem // Data required for the Fahrplan app integration https://github.com/johnjohndoe/engelsystem
// See engelsystem-base/src/main/kotlin/info/metadude/kotlin/library/engelsystem/models/Shift.kt // See engelsystem-base/src/main/kotlin/info/metadude/kotlin/library/engelsystem/models/Shift.kt
// Explicitly typecasts used to stay consistent
// ! All attributes not defined in $data might change at any time ! // ! All attributes not defined in $data might change at any time !
$data = [ $data = [
// Name of the shift (type) // Name of the shift (type)

View File

@ -171,7 +171,7 @@ class MessagesController extends BaseController
if ($msg->user_id == $currentUser->id) { if ($msg->user_id == $currentUser->id) {
$msg->delete(); $msg->delete();
} else { } else {
throw new HttpForbidden('You can not delete a message you haven\'t send'); throw new HttpForbidden();
} }
return $this->redirect->to('/messages/' . $otherUserId . '#newest'); return $this->redirect->to('/messages/' . $otherUserId . '#newest');

View File

@ -225,7 +225,7 @@ class Controller extends BaseController
} }
/** /**
* Ensure that the if the request is authorized * Ensure that the request is authorized
*/ */
protected function checkAuth(bool $isJson = false): void protected function checkAuth(bool $isJson = false): void
{ {

View File

@ -51,6 +51,7 @@ class OAuthController extends BaseController
throw new HttpNotFound('oauth.' . $request->get('error')); throw new HttpNotFound('oauth.' . $request->get('error'));
} }
// Initial request redirects to provider
if (!$request->has('code')) { if (!$request->has('code')) {
$authorizationUrl = $provider->getAuthorizationUrl( $authorizationUrl = $provider->getAuthorizationUrl(
[ [
@ -64,6 +65,7 @@ class OAuthController extends BaseController
return $this->redirect->to($authorizationUrl); return $this->redirect->to($authorizationUrl);
} }
// Redirected URL got called a second time
if ( if (
!$this->session->get('oauth2_state') !$this->session->get('oauth2_state')
|| $request->get('state') !== $this->session->get('oauth2_state') || $request->get('state') !== $this->session->get('oauth2_state')
@ -75,6 +77,7 @@ class OAuthController extends BaseController
throw new HttpNotFound('oauth.invalid-state'); throw new HttpNotFound('oauth.invalid-state');
} }
// Fetch access token
$accessToken = null; $accessToken = null;
try { try {
$accessToken = $provider->getAccessToken( $accessToken = $provider->getAccessToken(
@ -87,6 +90,7 @@ class OAuthController extends BaseController
$this->handleOAuthError($e, $providerName); $this->handleOAuthError($e, $providerName);
} }
// Load resource identifier
$resourceOwner = null; $resourceOwner = null;
try { try {
$resourceOwner = $provider->getResourceOwner($accessToken); $resourceOwner = $provider->getResourceOwner($accessToken);
@ -95,6 +99,7 @@ class OAuthController extends BaseController
} }
$resourceId = $this->getId($providerName, $resourceOwner); $resourceId = $this->getId($providerName, $resourceOwner);
// Fetch existing oauth state
/** @var OAuth|null $oauth */ /** @var OAuth|null $oauth */
$oauth = $this->oauth $oauth = $this->oauth
->query() ->query()
@ -105,6 +110,7 @@ class OAuthController extends BaseController
->where('identifier', '===', (string) $resourceId) ->where('identifier', '===', (string) $resourceId)
->first(); ->first();
// Update oauth state
$expirationTime = $accessToken->getExpires(); $expirationTime = $accessToken->getExpires();
$expirationTime = $expirationTime ? Carbon::createFromTimestamp($expirationTime) : null; $expirationTime = $expirationTime ? Carbon::createFromTimestamp($expirationTime) : null;
if ($oauth) { if ($oauth) {
@ -115,6 +121,7 @@ class OAuthController extends BaseController
$oauth->save(); $oauth->save();
} }
// Load user
$user = $this->auth->user(); $user = $this->auth->user();
if ($oauth && $user && $user->id != $oauth->user_id) { if ($oauth && $user && $user->id != $oauth->user_id) {
throw new HttpNotFound('oauth.already-connected'); throw new HttpNotFound('oauth.already-connected');
@ -122,6 +129,7 @@ class OAuthController extends BaseController
$connectProvider = $this->session->get('oauth2_connect_provider'); $connectProvider = $this->session->get('oauth2_connect_provider');
$this->session->remove('oauth2_connect_provider'); $this->session->remove('oauth2_connect_provider');
// Connect user with oauth
if (!$oauth && $user && $connectProvider && $connectProvider == $providerName) { if (!$oauth && $user && $connectProvider && $connectProvider == $providerName) {
$oauth = new OAuth([ $oauth = new OAuth([
'provider' => $providerName, 'provider' => $providerName,
@ -141,6 +149,7 @@ class OAuthController extends BaseController
$this->addNotification('oauth.connected'); $this->addNotification('oauth.connected');
} }
// Load user data
$resourceData = $resourceOwner->toArray(); $resourceData = $resourceOwner->toArray();
if (!empty($config['nested_info'])) { if (!empty($config['nested_info'])) {
$resourceData = Arr::dot($resourceData); $resourceData = Arr::dot($resourceData);
@ -148,6 +157,7 @@ class OAuthController extends BaseController
$userdata = new Collection($resourceData); $userdata = new Collection($resourceData);
if (!$oauth) { if (!$oauth) {
// User authenticated but has no account
return $this->redirectRegister( return $this->redirectRegister(
$providerName, $providerName,
(string) $resourceId, (string) $resourceId,
@ -307,11 +317,13 @@ class OAuthController extends BaseController
throw new HttpNotFound('oauth.not-found'); throw new HttpNotFound('oauth.not-found');
} }
// Set registration form field data
$this->session->set('form-data-username', $userdata->get($config['username'])); $this->session->set('form-data-username', $userdata->get($config['username']));
$this->session->set('form-data-email', $userdata->get($config['email'])); $this->session->set('form-data-email', $userdata->get($config['email']));
$this->session->set('form-data-firstname', $userdata->get($config['first_name'])); $this->session->set('form-data-firstname', $userdata->get($config['first_name']));
$this->session->set('form-data-lastname', $userdata->get($config['last_name'])); $this->session->set('form-data-lastname', $userdata->get($config['last_name']));
// Define OAuth state
$this->session->set('oauth2_groups', $userdata->get($config['groups'], [])); $this->session->set('oauth2_groups', $userdata->get($config['groups'], []));
$this->session->set('oauth2_connect_provider', $providerName); $this->session->set('oauth2_connect_provider', $providerName);
$this->session->set('oauth2_user_id', $providerUserIdentifier); $this->session->set('oauth2_user_id', $providerUserIdentifier);

View File

@ -115,7 +115,7 @@ class RegistrationController extends BaseController
} }
/** /**
* @return Array<string, 1> Checkbox field name/Id 1 * @return Array<string, 1> Checkbox field name/id 1
*/ */
private function determinePreselectedAngelTypes(): array private function determinePreselectedAngelTypes(): array
{ {

View File

@ -210,6 +210,7 @@ class User
*/ */
private function createUser(array $data, array $rawData): EngelsystemUser private function createUser(array $data, array $rawData): EngelsystemUser
{ {
// Ensure all user entries got created before saving
$this->dbConnection->beginTransaction(); $this->dbConnection->beginTransaction();
$user = new EngelsystemUser([ $user = new EngelsystemUser([
@ -274,6 +275,7 @@ class User
->associate($user) ->associate($user)
->save(); ->save();
// Handle OAuth registration
if ($this->session->has('oauth2_connect_provider') && $this->session->has('oauth2_user_id')) { if ($this->session->has('oauth2_connect_provider') && $this->session->has('oauth2_user_id')) {
$oauth = new OAuth([ $oauth = new OAuth([
'provider' => $this->session->get('oauth2_connect_provider'), 'provider' => $this->session->get('oauth2_connect_provider'),

View File

@ -4,14 +4,11 @@ declare(strict_types=1);
namespace Engelsystem\Helpers; namespace Engelsystem\Helpers;
use Engelsystem\Helpers\Carbon;
class DayOfEvent class DayOfEvent
{ {
/** /**
* @return The current day of the event. * @return ?int The current day of the event.
* If "event_has_day0" is set to true in config, * If `event_has_day0` is set to true in config, the first day of the event will be 0, else 1.
* the first day of the event will be 0, else 1.
* Returns null if "event_start" is not set. * Returns null if "event_start" is not set.
*/ */
public static function get(Carbon $date = null): int | null public static function get(Carbon $date = null): int | null

View File

@ -18,11 +18,14 @@ class Shifts
/** @see User_get_shifts_sum_query to keep it in sync */ /** @see User_get_shifts_sum_query to keep it in sync */
return $config['enabled'] && ( return $config['enabled'] && (
// Starts during night
$start->hour >= $config['start'] && $start->hour < $config['end'] $start->hour >= $config['start'] && $start->hour < $config['end']
// Ends during night
|| ( || (
$end->hour > $config['start'] $end->hour > $config['start']
|| $end->hour == $config['start'] && $end->minute > 0 || $end->hour == $config['start'] && $end->minute > 0
) && $end->hour <= $config['end'] ) && $end->hour <= $config['end']
// Starts before and ends after night
|| $start->hour <= $config['start'] && $end->hour >= $config['end'] || $start->hour <= $config['start'] && $end->hour >= $config['end']
); );
} }

View File

@ -72,7 +72,10 @@ class TranslationServiceProvider extends ServiceProvider
public function getTranslator(string $locale): GettextTranslator public function getTranslator(string $locale): GettextTranslator
{ {
if (!isset($this->translators[$locale])) { if (isset($this->translators[$locale])) {
return $this->translators[$locale];
}
$names = ['default', 'additional']; $names = ['default', 'additional'];
/** @var Translations $translations */ /** @var Translations $translations */
@ -86,9 +89,9 @@ class TranslationServiceProvider extends ServiceProvider
$file = $this->getFile($locale, $this->app->get('path.config') . '/lang', 'custom'); $file = $this->getFile($locale, $this->app->get('path.config') . '/lang', 'custom');
$translations = $this->loadFile($file, $translations); $translations = $this->loadFile($file, $translations);
/** @var GettextTranslator $translator */
$translator = GettextTranslator::createFromTranslations($translations); $translator = GettextTranslator::createFromTranslations($translations);
$this->translators[$locale] = $translator; $this->translators[$locale] = $translator;
}
return $this->translators[$locale]; return $this->translators[$locale];
} }

View File

@ -21,7 +21,7 @@ class Uuid
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
// first bit is the uuid version, here 4 // first bit is the uuid version, here 4
mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x0fff) | 0x4000,
// variant // variant, here OSF DCE UUID
mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffffffffffff) mt_rand(0, 0xffffffffffff)
); );
@ -53,7 +53,7 @@ class Uuid
Str::substr($value, 8, 4), Str::substr($value, 8, 4),
// first bit is the uuid version, here 4 // first bit is the uuid version, here 4
'4' . Str::substr($value, 13, 3), '4' . Str::substr($value, 13, 3),
// first bit is the variant (0x8-0xb) // first bit is the variant (0x8-0xb), here OSF DCE UUID
dechex(8 + (hexdec(Str::substr($value, 16, 1)) % 4)) dechex(8 + (hexdec(Str::substr($value, 16, 1)) % 4))
. Str::substr($value, 17, 3), . Str::substr($value, 17, 3),
Str::substr($value, 20, 12) Str::substr($value, 20, 12)

View File

@ -41,7 +41,7 @@ class UrlGenerator implements UrlGeneratorInterface
} }
/** /**
* Prepend the auto detected or configured app base path and domain * Prepend the auto-detected or configured app base path and domain
* *
* @param $path * @param $path
*/ */

View File

@ -37,6 +37,7 @@ class Validator
$value = isset($data[$key]) ? $data[$key] : null; $value = isset($data[$key]) ? $data[$key] : null;
$values = explode('|', $values); $values = explode('|', $values);
// Rules that have side effects on others like inverting the result with not and making them optional
$packing = []; $packing = [];
foreach ($this->nestedRules as $rule) { foreach ($this->nestedRules as $rule) {
if (in_array($rule, $values)) { if (in_array($rule, $values)) {

View File

@ -12,6 +12,9 @@ use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
/**
* Wraps a callable to be used to respond / as a middleware
*/
class CallableHandler implements MiddlewareInterface, RequestHandlerInterface class CallableHandler implements MiddlewareInterface, RequestHandlerInterface
{ {
/** @var callable */ /** @var callable */

View File

@ -43,7 +43,7 @@ class Dispatcher implements MiddlewareInterface, RequestHandlerInterface
/** /**
* Handle the request and return a response. * Handle the request and return a response.
* *
* It calls all configured middleware and handles their response * It calls all configured middlewares and handles their response
*/ */
public function handle(ServerRequestInterface $request): ResponseInterface public function handle(ServerRequestInterface $request): ResponseInterface
{ {

View File

@ -22,7 +22,7 @@ class ErrorHandler implements MiddlewareInterface
protected string $viewPrefix = 'errors/'; protected string $viewPrefix = 'errors/';
/** /**
* A list of inputs that are not saved from form input * A list of inputs that are not saved from input
* *
* @var array<string> * @var array<string>
*/ */
@ -42,7 +42,7 @@ class ErrorHandler implements MiddlewareInterface
} }
/** /**
* Handles any error messages * Handles any error messages / http exceptions / validation errors
* *
* Should be added at the beginning * Should be added at the beginning
*/ */
@ -50,6 +50,7 @@ class ErrorHandler implements MiddlewareInterface
ServerRequestInterface $request, ServerRequestInterface $request,
RequestHandlerInterface $handler RequestHandlerInterface $handler
): ResponseInterface { ): ResponseInterface {
// Handle response
try { try {
$response = $handler->handle($request); $response = $handler->handle($request);
} catch (HttpException $e) { } catch (HttpException $e) {
@ -75,6 +76,7 @@ class ErrorHandler implements MiddlewareInterface
$contentType = 'text/html'; $contentType = 'text/html';
} }
// Handle response based on status
if ( if (
$statusCode < 400 $statusCode < 400
|| !$response instanceof Response || !$response instanceof Response

View File

@ -13,6 +13,9 @@ use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
/**
* Middleware to support the old routing / pages from includes
*/
class LegacyMiddleware implements MiddlewareInterface class LegacyMiddleware implements MiddlewareInterface
{ {
/** @var array<string> */ /** @var array<string> */
@ -33,7 +36,7 @@ class LegacyMiddleware implements MiddlewareInterface
/** /**
* Handle the request the old way * Handle the request the old way
* *
* Should be used before a 404 is send * Should be used before a 404 is sent
*/ */
public function process( public function process(
ServerRequestInterface $request, ServerRequestInterface $request,
@ -42,6 +45,7 @@ class LegacyMiddleware implements MiddlewareInterface
/** @var Request $appRequest */ /** @var Request $appRequest */
$appRequest = $this->container->get('request'); $appRequest = $this->container->get('request');
$page = $appRequest->query->get('p'); $page = $appRequest->query->get('p');
// Support old URL scheme
if (empty($page)) { if (empty($page)) {
$page = $appRequest->path(); $page = $appRequest->path();
$page = str_replace('-', '_', $page); $page = str_replace('-', '_', $page);

View File

@ -24,6 +24,7 @@ class RequestHandler implements MiddlewareInterface
/** /**
* Process an incoming server request and return a response, optionally delegating * Process an incoming server request and return a response, optionally delegating
* response creation to a handler. * response creation to a handler.
* Implements basic permission checking if the controller supports it.
*/ */
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {

View File

@ -35,7 +35,6 @@ class TrimInput implements MiddlewareInterface
$request = $request->withParsedBody($trimmedParsedBody); $request = $request->withParsedBody($trimmedParsedBody);
} }
return $handler->handle($request); return $handler->handle($request);
} }

View File

@ -39,11 +39,10 @@ class Globals extends TwigExtension implements GlobalsInterface
{ {
$user = $this->auth->user(); $user = $this->auth->user();
$themes = config('themes'); $themes = config('themes');
$themeId = config('theme');
$userMessages = null; $userMessages = null;
if ($user === null) { if ($user) {
$themeId = config('theme');
} else {
$themeId = $user->settings->theme; $themeId = $user->settings->theme;
$userMessages = $user $userMessages = $user
->messagesReceived() ->messagesReceived()

View File

@ -26,6 +26,7 @@ class Url extends TwigExtension
public function getUrl(string $path, array $parameters = []): string public function getUrl(string $path, array $parameters = []): string
{ {
// Fix legacy URLs
$path = str_replace('_', '-', $path); $path = str_replace('_', '-', $path);
return $this->urlGenerator->to($path, $parameters); return $this->urlGenerator->to($path, $parameters);