Next major version 2 with PHP 8.1, Symfony 6, Tabler UI, 2FA ... (#2902)

This commit is contained in:
Kevin Papst
2022-12-31 21:19:55 +01:00
committed by GitHub
parent 95e06746bd
commit 90a0fd8a22
2164 changed files with 83700 additions and 86426 deletions

View File

@@ -9,7 +9,6 @@
namespace App\EventSubscriber\Actions;
use App\Constants;
use App\Event\PageActionsEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
@@ -20,13 +19,8 @@ use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
*/
abstract class AbstractActionsSubscriber implements EventSubscriberInterface
{
private $auth;
private $urlGenerator;
public function __construct(AuthorizationCheckerInterface $security, UrlGeneratorInterface $urlGenerator)
public function __construct(private AuthorizationCheckerInterface $auth, private UrlGeneratorInterface $urlGenerator)
{
$this->auth = $security;
$this->urlGenerator = $urlGenerator;
}
protected function isGranted($attributes, $subject = null): bool
@@ -39,11 +33,6 @@ abstract class AbstractActionsSubscriber implements EventSubscriberInterface
return $this->urlGenerator->generate($route, $parameters);
}
protected function documentationLink(string $url): string
{
return Constants::HOMEPAGE . '/documentation/' . $url;
}
public static function getSubscribedEvents(): array
{
return [

View File

@@ -29,27 +29,28 @@ abstract class AbstractTimesheetSubscriber extends AbstractActionsSubscriber
}
if (!$timesheet->isRunning() && $this->isGranted('start', $timesheet)) {
$event->addAction('repeat', ['url' => $this->path('restart_timesheet', ['id' => $timesheet->getId()]), 'class' => 'api-link', 'attr' => ['data-payload' => '{"copy": "all"}', 'data-event' => 'kimai.timesheetStart kimai.timesheetUpdate', 'data-method' => 'PATCH', 'data-msg-error' => 'timesheet.start.error', 'data-msg-success' => 'timesheet.start.success']]);
$event->addAction('repeat', ['title' => 'repeat', 'translation_domain' => 'actions', 'url' => $this->path('restart_timesheet', ['id' => $timesheet->getId()]), 'class' => 'api-link', 'attr' => ['data-payload' => '{"copy": "all"}', 'data-event' => 'kimai.timesheetStart kimai.timesheetUpdate', 'data-method' => 'PATCH', 'data-msg-error' => 'timesheet.start.error', 'data-msg-success' => 'timesheet.start.success']]);
}
if ($this->isGranted('edit', $timesheet)) {
$class = $event->isView('edit') ? '' : 'modal-ajax-form';
$event->addAction('edit', ['url' => $this->path($routeEdit, ['id' => $timesheet->getId()]), 'class' => $class]);
$event->addAction('edit', ['title' => 'edit', 'translation_domain' => 'actions', 'url' => $this->path($routeEdit, ['id' => $timesheet->getId()]), 'class' => $class]);
}
if ($this->isGranted('duplicate', $timesheet)) {
$class = $event->isView('edit') ? '' : 'modal-ajax-form';
$event->addAction('copy', ['url' => $this->path($routeDuplicate, ['id' => $timesheet->getId()]), 'class' => $class]);
$event->addAction('copy', ['title' => 'copy', 'translation_domain' => 'actions', 'url' => $this->path($routeDuplicate, ['id' => $timesheet->getId()]), 'class' => $class]);
}
if ($event->countActions() > 0) {
$event->addDivider();
}
if ($event->isIndexView() && $this->isGranted('delete', $timesheet)) {
if (($event->isIndexView() || $event->isView('calendar')) && $this->isGranted('delete', $timesheet)) {
$event->addAction('trash', [
'url' => $this->path('delete_timesheet', ['id' => $timesheet->getId()]),
'class' => 'api-link',
'class' => 'api-link text-red',
'translation_domain' => 'actions',
'attr' => [
'data-event' => 'kimai.timesheetDelete',
'data-method' => 'DELETE',
@@ -60,9 +61,5 @@ abstract class AbstractTimesheetSubscriber extends AbstractActionsSubscriber
]);
}
}
if (!$event->isIndexView()) {
$event->addHelp($this->documentationLink('timesheet.html'));
}
}
}

View File

@@ -10,9 +10,8 @@
namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
use App\Repository\Query\ActivityQuery;
class ActivitiesSubscriber extends AbstractActionsSubscriber
final class ActivitiesSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
@@ -21,23 +20,10 @@ class ActivitiesSubscriber extends AbstractActionsSubscriber
public function onActions(PageActionsEvent $event): void
{
$payload = $event->getPayload();
/** @var ActivityQuery $query */
$query = $payload['query'];
$event->addSearchToggle($query);
$event->addColumnToggle('#modal_activity_admin');
$event->addQuickExport($this->path('activity_export'));
if ($this->isGranted('create_activity')) {
$event->addCreate($this->path('admin_activity_create'));
}
if ($this->isGranted('system_configuration')) {
$event->addAction('settings', ['url' => $this->path('system_configuration_section', ['section' => 'activity']), 'class' => 'modal-ajax-form']);
}
$event->addHelp($this->documentationLink('activity.html'));
$event->addQuickExport($this->path('activity_export'));
}
}

View File

@@ -12,7 +12,7 @@ namespace App\EventSubscriber\Actions;
use App\Entity\Activity;
use App\Event\PageActionsEvent;
class ActivitySubscriber extends AbstractActionsSubscriber
final class ActivitySubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
@@ -31,17 +31,17 @@ class ActivitySubscriber extends AbstractActionsSubscriber
}
if (!$event->isView('activity_details') && $this->isGranted('view', $activity)) {
$event->addAction('details', ['url' => $this->path('activity_details', ['id' => $activity->getId()])]);
$event->addAction('details', ['title' => 'details', 'translation_domain' => 'actions', 'url' => $this->path('activity_details', ['id' => $activity->getId()])]);
}
if ($this->isGranted('edit', $activity)) {
$class = $event->isView('edit') ? '' : 'modal-ajax-form';
$event->addAction('edit', ['url' => $this->path('admin_activity_edit', ['id' => $activity->getId()]), 'class' => $class]);
$event->addAction('edit', ['title' => 'edit', 'translation_domain' => 'actions', 'url' => $this->path('admin_activity_edit', ['id' => $activity->getId()]), 'class' => $class]);
}
if ($this->isGranted('permissions', $activity)) {
$class = $event->isView('permissions') ? '' : 'modal-ajax-form';
$event->addAction('permissions', ['url' => $this->path('admin_activity_permissions', ['id' => $activity->getId()]), 'class' => $class]);
$event->addAction('permissions', ['title' => 'permissions', 'translation_domain' => 'actions', 'url' => $this->path('admin_activity_permissions', ['id' => $activity->getId()]), 'class' => $class]);
}
if ($event->countActions() > 0) {
@@ -66,7 +66,7 @@ class ActivitySubscriber extends AbstractActionsSubscriber
if (!$activity->isGlobal()) {
$parameters['project'] = $activity->getProject()->getId();
}
$event->addAction('create-timesheet', ['icon' => 'start', 'url' => $this->path('admin_timesheet_create', $parameters), 'class' => 'modal-ajax-form']);
$event->addAction('create-timesheet', ['title' => 'create-timesheet', 'translation_domain' => 'actions', 'icon' => 'start', 'url' => $this->path('admin_timesheet_create', $parameters), 'class' => 'modal-ajax-form']);
}
if (($event->isIndexView() || $event->isView('project_details')) && $this->isGranted('delete', $activity)) {

View File

@@ -1,33 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
class CalendarSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
return 'calendar';
}
public function onActions(PageActionsEvent $event): void
{
if ($this->isGranted('create_own_timesheet')) {
$event->addCreate($this->path('timesheet_create'));
}
if ($this->isGranted('system_configuration')) {
$event->addAction('settings', ['url' => $this->path('system_configuration_section', ['section' => 'calendar']), 'class' => 'modal-ajax-form']);
}
$event->addHelp($this->documentationLink('calendar.html'));
}
}

View File

@@ -12,7 +12,7 @@ namespace App\EventSubscriber\Actions;
use App\Entity\Customer;
use App\Event\PageActionsEvent;
class CustomerSubscriber extends AbstractActionsSubscriber
final class CustomerSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
@@ -30,18 +30,35 @@ class CustomerSubscriber extends AbstractActionsSubscriber
return;
}
if (!$event->isView('customer_details') && $this->isGranted('view', $customer)) {
$event->addAction('details', ['url' => $this->path('customer_details', ['id' => $customer->getId()])]);
$canView = $this->isGranted('view', $customer);
$isListingView = $event->isIndexView() || $event->isCustomView();
if (!$event->isView('customer_details') && $canView) {
$event->addAction('details', ['title' => 'details', 'translation_domain' => 'actions', 'url' => $this->path('customer_details', ['id' => $customer->getId()])]);
}
if ($this->isGranted('edit', $customer)) {
$class = $event->isView('edit') ? '' : 'modal-ajax-form';
$event->addAction('edit', ['url' => $this->path('admin_customer_edit', ['id' => $customer->getId()]), 'class' => $class]);
$event->addAction('edit', ['title' => 'edit', 'translation_domain' => 'actions', 'url' => $this->path('admin_customer_edit', ['id' => $customer->getId()]), 'class' => $class]);
}
if ($this->isGranted('permissions', $customer)) {
$class = $event->isView('permissions') ? '' : 'modal-ajax-form';
$event->addAction('permissions', ['url' => $this->path('admin_customer_permissions', ['id' => $customer->getId()]), 'class' => $class]);
$event->addAction('permissions', ['title' => 'permissions', 'translation_domain' => 'actions', 'url' => $this->path('admin_customer_permissions', ['id' => $customer->getId()]), 'class' => $class]);
}
if ($canView && $event->isIndexView()) {
$event->addAction('vcard', ['title' => 'vcard', 'translation_domain' => 'actions', 'icon' => 'far fa-address-card', 'url' => $this->path('customer_vcard', ['id' => $customer->getId()])]);
}
if ($isListingView) {
if ($customer->isVisible() && $this->isGranted('create_project')) {
$event->addAction('create-project', [
'icon' => 'create',
'url' => $this->path('admin_project_create_with_customer', ['customer' => $customer->getId()]),
'class' => 'modal-ajax-form'
]);
}
}
if ($event->countActions() > 0) {
@@ -64,22 +81,17 @@ class CustomerSubscriber extends AbstractActionsSubscriber
$event->addDivider();
}
if (!$event->isView('customer_details')) {
if ($customer->isVisible() && $this->isGranted('create_project')) {
$event->addAction('create-project', [
'icon' => 'create',
'url' => $this->path('admin_project_create_with_customer', ['customer' => $customer->getId()]),
'class' => 'modal-ajax-form'
]);
}
}
if ($event->isIndexView() && $this->isGranted('delete', $customer)) {
$event->addDelete($this->path('admin_customer_delete', ['id' => $customer->getId()]));
}
if ($this->isGranted('view_reporting') && $this->isGranted('budget_any', 'project')) {
$event->addAction('report_project_view', ['url' => $this->path('report_project_view', ['customer' => $customer->getId()]), 'icon' => 'reporting', 'translation_domain' => 'reporting']);
if ($this->isGranted('report:customer') && $this->isGranted('report:other')) {
$event->addActionToSubmenu('report', 'report_customer_monthly_projects', ['title' => 'report_customer_monthly_projects', 'url' => $this->path('report_customer_monthly_projects', ['customer' => $customer->getId()]), 'translation_domain' => 'reporting']);
}
if ($this->isGranted('report:project') && $this->isGranted('budget_any', 'project')) {
$event->addActionToSubmenu('report', 'daterange_projects', ['title' => 'report_project_daterange', 'url' => $this->path('report_project_daterange', ['customer' => $customer->getId()]), 'translation_domain' => 'reporting']);
$event->addActionToSubmenu('report', 'report_project_view', ['title' => 'report_project_view', 'url' => $this->path('report_project_view', ['customer' => $customer->getId()]), 'translation_domain' => 'reporting']);
}
}
}

View File

@@ -10,9 +10,8 @@
namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
use App\Repository\Query\CustomerQuery;
class CustomersSubscriber extends AbstractActionsSubscriber
final class CustomersSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
@@ -21,23 +20,10 @@ class CustomersSubscriber extends AbstractActionsSubscriber
public function onActions(PageActionsEvent $event): void
{
$payload = $event->getPayload();
/** @var CustomerQuery $query */
$query = $payload['query'];
$event->addSearchToggle($query);
$event->addColumnToggle('#modal_customer_admin');
$event->addQuickExport($this->path('customer_export'));
if ($this->isGranted('create_customer')) {
$event->addCreate($this->path('admin_customer_create'));
}
if ($this->isGranted('system_configuration')) {
$event->addAction('settings', ['url' => $this->path('system_configuration_section', ['section' => 'customer']), 'class' => 'modal-ajax-form']);
}
$event->addHelp($this->documentationLink('customer.html'));
$event->addQuickExport($this->path('customer_export'));
}
}

View File

@@ -0,0 +1,76 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
use App\Widget\WidgetInterface;
final class DashboardSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
return 'dashboard';
}
public function onActions(PageActionsEvent $event): void
{
$payload = $event->getPayload();
/** @var WidgetInterface[] $widgets */
$widgets = $payload['widgets'];
/** @var WidgetInterface[] $available */
$available = $payload['available'];
if (!$event->isView('edit')) {
$event->addConfig($this->path('dashboard_edit'));
} else {
$ids = [];
$event->addAction('save', ['title' => 'action.save', 'onclick' => 'saveDashboard(); return false;', 'icon' => 'save']);
foreach ($widgets as $widget) {
$ids[] = $widget->getId();
}
foreach ($available as $widget) {
if ($widget->isInternal()) {
continue;
}
// prevent to use the same widget multiple times
if (\in_array($widget->getId(), $ids)) {
continue;
}
$permissions = $widget->getPermissions();
if (\count($permissions) > 0) {
$allow = false;
foreach ($widget->getPermissions() as $permission) {
if ($this->isGranted($permission)) {
$allow = true;
}
}
if (!$allow) {
continue;
}
}
if (empty($widget->getTitle())) {
continue;
}
$event->addActionToSubmenu('widget_add', $widget->getId(), ['url' => $this->path('dashboard_add', ['widget' => $widget->getId()]), 'title' => $widget->getTitle()]);
}
$event->addAction('reset', ['title' => 'action.reset', 'url' => $this->path('dashboard_reset'), 'icon' => 'delete', 'class' => 'confirmation-link', 'attr' => ['data-question' => 'confirm.delete']]);
}
}
}

View File

@@ -1,31 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
class ExportSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
return 'export';
}
public function onActions(PageActionsEvent $event): void
{
$event->addColumnToggle('#modal_export');
if ($event->isView('preview')) {
$event->addAction('off', ['id' => 'export-toggle-button']);
}
$event->addHelp($this->documentationLink('export.html'));
}
}

View File

@@ -10,29 +10,16 @@
namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
use App\Repository\Query\InvoiceArchiveQuery;
class InvoiceArchiveSubscriber extends AbstractActionsSubscriber
final class InvoiceArchiveSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
return 'invoice_details';
return 'invoice_archive';
}
public function onActions(PageActionsEvent $event): void
{
$payload = $event->getPayload();
/** @var InvoiceArchiveQuery $query */
$query = $payload['query'];
if ($this->isGranted('view_invoice')) {
$event->addBack($this->path('invoice'));
}
$event->addSearchToggle($query);
$event->addColumnToggle('#modal_invoices');
$event->addQuickExport($this->path('invoice_export'));
$event->addHelp($this->documentationLink('invoices.html'));
}
}

View File

@@ -9,10 +9,10 @@
namespace App\EventSubscriber\Actions;
use App\Entity\InvoiceDocument;
use App\Event\PageActionsEvent;
use App\Model\InvoiceDocument;
class InvoiceDocumentSubscriber extends AbstractActionsSubscriber
final class InvoiceDocumentSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{

View File

@@ -12,7 +12,7 @@ namespace App\EventSubscriber\Actions;
use App\Entity\Invoice;
use App\Event\PageActionsEvent;
class InvoiceSubscriber extends AbstractActionsSubscriber
final class InvoiceSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
@@ -30,15 +30,28 @@ class InvoiceSubscriber extends AbstractActionsSubscriber
return;
}
$event->addAction('edit', ['url' => $this->path('admin_invoice_edit', ['id' => $invoice->getId()]), 'class' => 'modal-ajax-form']);
$event->addAction('download', ['url' => $this->path('admin_invoice_download', ['id' => $invoice->getId()]), 'target' => '_blank']);
$allowCreate = $this->isGranted('create_invoice');
$allowView = $this->isGranted('view_invoice');
$allowCustomer = $this->isGranted('access', $invoice->getCustomer());
$event->addDivider();
if ($allowCustomer && $allowCreate) {
$event->addAction('edit', ['url' => $this->path('admin_invoice_edit', ['id' => $invoice->getId()]), 'class' => 'modal-ajax-form']);
}
if (!$invoice->isPending()) {
$event->addAction('invoice.pending', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'pending', 'token' => $payload['token']])]);
} else {
$event->addAction('invoice.paid', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'paid', 'token' => $payload['token']]), 'class' => 'modal-ajax-form']);
if ($allowCustomer && $allowView) {
$event->addAction('download', ['url' => $this->path('admin_invoice_download', ['id' => $invoice->getId()]), 'target' => '_blank']);
}
if ($event->countActions() > 0) {
$event->addDivider();
}
if ($allowCustomer && $allowCreate) {
if (!$invoice->isPending()) {
$event->addAction('invoice.pending', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'pending', 'token' => $payload['token']])]);
} else {
$event->addAction('invoice.paid', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'paid', 'token' => $payload['token']]), 'class' => 'modal-ajax-form']);
}
}
$allowDelete = $this->isGranted('delete_invoice');

View File

@@ -12,7 +12,7 @@ namespace App\EventSubscriber\Actions;
use App\Entity\InvoiceTemplate;
use App\Event\PageActionsEvent;
class InvoiceTemplateSubscriber extends AbstractActionsSubscriber
final class InvoiceTemplateSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
@@ -31,12 +31,9 @@ class InvoiceTemplateSubscriber extends AbstractActionsSubscriber
}
if ($this->isGranted('manage_invoice_template')) {
if (!$event->isIndexView()) {
$event->addBack($this->path('invoice'));
}
$event->addAction('edit', ['url' => $this->path('admin_invoice_template_edit', ['id' => $template->getId()]), 'class' => 'modal-ajax-form']);
$event->addAction('copy', ['url' => $this->path('admin_invoice_template_copy', ['id' => $template->getId()])]);
$event->addDelete($this->path('admin_invoice_template_delete', ['id' => $template->getId(), 'token' => $payload['token']]), false);
$event->addAction('copy', ['url' => $this->path('admin_invoice_template_copy', ['id' => $template->getId()]), 'class' => 'modal-ajax-form']);
$event->addDelete($this->path('admin_invoice_template_delete', ['id' => $template->getId(), 'csrfToken' => $payload['token']]), false);
}
}
}

View File

@@ -1,29 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
class InvoiceTemplateUploadSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
return 'invoice_upload';
}
public function onActions(PageActionsEvent $event): void
{
if ($event->isIndexView() && $this->isGranted('manage_invoice_template')) {
$event->addBack($this->path('admin_invoice_template'));
}
$event->addHelp($this->documentationLink('invoices.html'));
}
}

View File

@@ -11,7 +11,7 @@ namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
class InvoiceTemplatesSubscriber extends AbstractActionsSubscriber
final class InvoiceTemplatesSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
@@ -20,21 +20,13 @@ class InvoiceTemplatesSubscriber extends AbstractActionsSubscriber
public function onActions(PageActionsEvent $event): void
{
if ($this->isGranted('view_invoice')) {
$event->addBack($this->path('invoice'));
}
$event->addColumnToggle('#modal_invoice_template');
if ($this->isGranted('manage_invoice_template')) {
$event->addAction('create', ['url' => $this->path('admin_invoice_template_create'), 'class' => 'modal-ajax-form']);
$event->addAction('create', ['url' => $this->path('admin_invoice_template_create'), 'class' => 'modal-ajax-form', 'title' => 'create']);
}
// File upload does not work in a modal right now
if ($this->isGranted('upload_invoice_template')) {
$event->addAction('upload', ['url' => $this->path('admin_invoice_document_upload')]);
$event->addAction('upload', ['url' => $this->path('admin_invoice_document_upload'), 'title' => 'invoice_renderer', 'translation_domain' => 'invoice-renderer']);
}
$event->addHelp($this->documentationLink('invoices.html'));
}
}

View File

@@ -1,37 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
class InvoicesSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
return 'invoices';
}
public function onActions(PageActionsEvent $event): void
{
$event->addColumnToggle('#modal_invoice');
$event->addAction('list', ['url' => $this->path('admin_invoice_list')]);
if ($this->isGranted('manage_invoice_template')) {
$event->addAction('invoice-template', ['url' => $this->path('admin_invoice_template')]);
}
if ($this->isGranted('system_configuration')) {
$event->addAction('settings', ['url' => $this->path('system_configuration_section', ['section' => 'invoice']), 'class' => 'modal-ajax-form']);
}
$event->addHelp($this->documentationLink('invoices.html'));
}
}

View File

@@ -11,7 +11,7 @@ namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
class PermissionsSubscriber extends AbstractActionsSubscriber
final class PermissionsSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
@@ -23,7 +23,5 @@ class PermissionsSubscriber extends AbstractActionsSubscriber
if ($this->isGranted('role_permissions')) {
$event->addCreate($this->path('admin_user_roles'), !$event->isView('role'));
}
$event->addHelp($this->documentationLink('permissions.html'));
}
}

View File

@@ -12,7 +12,7 @@ namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
use App\Plugin\Plugin;
class PluginSubscriber extends AbstractActionsSubscriber
final class PluginSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{

View File

@@ -1,27 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\EventSubscriber\Actions;
use App\Constants;
use App\Event\PageActionsEvent;
class PluginsSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
return 'plugins';
}
public function onActions(PageActionsEvent $event): void
{
$event->addAction('shop', ['url' => Constants::HOMEPAGE . '/store/', 'target' => '_blank']);
$event->addHelp($this->documentationLink('plugins.html'));
}
}

View File

@@ -12,7 +12,7 @@ namespace App\EventSubscriber\Actions;
use App\Entity\Project;
use App\Event\PageActionsEvent;
class ProjectSubscriber extends AbstractActionsSubscriber
final class ProjectSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
@@ -25,23 +25,26 @@ class ProjectSubscriber extends AbstractActionsSubscriber
/** @var Project $project */
$project = $payload['project'];
$customer = $project->getCustomer();
if ($project->getId() === null) {
if ($project->getId() === null || $customer === null) {
return;
}
$isListingView = $event->isIndexView() || $event->isCustomView();
if (!$event->isView('project_details') && $this->isGranted('view', $project)) {
$event->addAction('details', ['url' => $this->path('project_details', ['id' => $project->getId()])]);
$event->addAction('details', ['title' => 'details', 'translation_domain' => 'actions', 'url' => $this->path('project_details', ['id' => $project->getId()])]);
}
if ($this->isGranted('edit', $project)) {
$class = $event->isView('edit') ? '' : 'modal-ajax-form';
$event->addAction('edit', ['url' => $this->path('admin_project_edit', ['id' => $project->getId()]), 'class' => $class]);
$event->addAction('edit', ['title' => 'edit', 'translation_domain' => 'actions', 'url' => $this->path('admin_project_edit', ['id' => $project->getId()]), 'class' => $class]);
}
if ($this->isGranted('permissions', $project)) {
$class = $event->isView('permissions') ? '' : 'modal-ajax-form';
$event->addAction('permissions', ['url' => $this->path('admin_project_permissions', ['id' => $project->getId()]), 'class' => $class]);
$event->addAction('permissions', ['title' => 'permissions', 'translation_domain' => 'actions', 'url' => $this->path('admin_project_permissions', ['id' => $project->getId()]), 'class' => $class]);
}
if ($event->countActions() > 0) {
@@ -49,19 +52,23 @@ class ProjectSubscriber extends AbstractActionsSubscriber
}
if ($this->isGranted('view_activity')) {
$event->addActionToSubmenu('filter', 'activity', ['title' => 'activity.filter', 'translation_domain' => 'actions', 'url' => $this->path('admin_activity', ['customers[]' => $project->getCustomer()->getId(), 'projects[]' => $project->getId()])]);
$event->addActionToSubmenu('filter', 'activity', ['title' => 'activity.filter', 'translation_domain' => 'actions', 'url' => $this->path('admin_activity', ['customers[]' => $customer->getId(), 'projects[]' => $project->getId()])]);
}
if ($this->isGranted('view_other_timesheet')) {
$event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'translation_domain' => 'actions', 'url' => $this->path('admin_timesheet', ['customers[]' => $project->getCustomer()->getId(), 'projects[]' => $project->getId()])]);
$event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'translation_domain' => 'actions', 'url' => $this->path('admin_timesheet', ['customers[]' => $customer->getId(), 'projects[]' => $project->getId()])]);
}
if ($this->isGranted('create_export')) {
$event->addActionToSubmenu('filter', 'export', ['title' => 'export', 'url' => $this->path('export', ['customers[]' => $customer->getId(), 'projects[]' => $project->getId(), 'exported' => 1, 'daterange' => ''])]);
}
if ($event->hasSubmenu('filter')) {
$event->addDivider();
}
if (!$event->isView('project_details')) {
if ($project->isVisible() && $project->getCustomer()->isVisible() && $this->isGranted('create_activity')) {
if ($isListingView) {
if ($project->isVisible() && $customer->isVisible() && $this->isGranted('create_activity')) {
$event->addAction('create-activity', [
'icon' => 'create',
'url' => $this->path('admin_activity_create_with_project', ['project' => $project->getId()]),
@@ -70,10 +77,10 @@ class ProjectSubscriber extends AbstractActionsSubscriber
}
}
if ($this->isGranted('edit', $project) && $this->isGranted('create_project')) {
if (\array_key_exists('token', $payload) && $this->isGranted('edit', $project) && $this->isGranted('create_project')) {
$event->addAction(
'copy',
['url' => $this->path('admin_project_duplicate', ['id' => $project->getId(), 'token' => $payload['token']])]
['title' => 'copy', 'translation_domain' => 'actions', 'url' => $this->path('admin_project_duplicate', ['id' => $project->getId(), 'token' => $payload['token']])]
);
}
@@ -81,8 +88,8 @@ class ProjectSubscriber extends AbstractActionsSubscriber
$event->addDelete($this->path('admin_project_delete', ['id' => $project->getId()]));
}
if ($project->isVisible() && $this->isGranted('view_reporting') && $this->isGranted('details', $project)) {
$event->addAction('report_project_details', ['url' => $this->path('report_project_details', ['project' => $project->getId()]), 'icon' => 'reporting', 'translation_domain' => 'reporting']);
if (!$event->isView('project_details_report') && $this->isGranted('report:project') && $this->isGranted('details', $project)) {
$event->addAction('report_project_details', ['title' => 'report_project_details', 'translation_domain' => 'reporting', 'url' => $this->path('report_project_details', ['project' => $project->getId()]), 'icon' => 'reporting']);
}
}
}

View File

@@ -10,9 +10,8 @@
namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
use App\Repository\Query\ProjectQuery;
class ProjectsSubscriber extends AbstractActionsSubscriber
final class ProjectsSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
@@ -21,24 +20,10 @@ class ProjectsSubscriber extends AbstractActionsSubscriber
public function onActions(PageActionsEvent $event): void
{
$payload = $event->getPayload();
/** @var ProjectQuery $query */
$query = $payload['query'];
$event->addSearchToggle($query);
$event->addColumnToggle('#modal_project_admin');
$event->addQuickExport($this->path('project_export'));
if ($this->isGranted('create_project')) {
$event->addCreate($this->path('admin_project_create'));
}
if ($this->isGranted('system_configuration')) {
$event->addAction('settings', ['url' => $this->path('system_configuration_section', ['section' => 'project']), 'class' => 'modal-ajax-form']);
}
$event->addHelp($this->documentationLink('project.html'));
$event->addQuickExport($this->path('project_export'));
}
}

View File

@@ -1,29 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
class QuickEntrySubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
return 'weekly_times';
}
public function onActions(PageActionsEvent $event): void
{
if ($this->isGranted('system_configuration')) {
$event->addAction('settings', ['url' => $this->path('system_configuration_section', ['section' => 'quick_entry']), 'class' => 'modal-ajax-form']);
}
$event->addHelp($this->documentationLink('weekly-times.html'));
}
}

View File

@@ -1,47 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
use App\Reporting\Report;
use App\Reporting\ReportingService;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class ReportingSubscriber extends AbstractActionsSubscriber
{
private $reportingService;
public function __construct(AuthorizationCheckerInterface $security, UrlGeneratorInterface $urlGenerator, ReportingService $reportingService)
{
parent::__construct($security, $urlGenerator);
$this->reportingService = $reportingService;
}
public static function getActionName(): string
{
return 'reporting';
}
public function onActions(PageActionsEvent $event): void
{
$reports = $this->reportingService->getAvailableReports($event->getUser());
foreach ($reports as $report) {
$subMenu = 'reporting';
if ($report instanceof Report) {
$subMenu = $report->getReportIcon();
}
$event->addActionToSubmenu($subMenu, $report->getId(), ['title' => $report->getLabel(), 'translation_domain' => 'reporting', 'url' => $this->path($report->getRoute()), 'class' => 'report-' . $report->getId()]);
}
$event->addHelp($this->documentationLink('reporting.html'));
}
}

View File

@@ -1,25 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
class SystemConfigurationSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
return 'system_configuration';
}
public function onActions(PageActionsEvent $event): void
{
$event->addHelp($this->documentationLink('configurations.html'));
}
}

View File

@@ -12,7 +12,7 @@ namespace App\EventSubscriber\Actions;
use App\Entity\Tag;
use App\Event\PageActionsEvent;
class TagSubscriber extends AbstractActionsSubscriber
final class TagSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
@@ -53,7 +53,8 @@ class TagSubscriber extends AbstractActionsSubscriber
if ($event->isIndexView() && $this->isGranted('delete_tag')) {
$event->addAction('trash', [
'url' => $this->path('delete_tag', ['id' => $id]),
'class' => 'api-link',
'class' => 'api-link text-red',
'translation_domain' => 'actions',
'attr' => [
'data-event' => 'kimai.tagDelete kimai.tagUpdate',
'data-method' => 'DELETE',

View File

@@ -10,9 +10,8 @@
namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
use App\Repository\Query\TagQuery;
class TagsSubscriber extends AbstractActionsSubscriber
final class TagsSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
@@ -21,17 +20,8 @@ class TagsSubscriber extends AbstractActionsSubscriber
public function onActions(PageActionsEvent $event): void
{
$payload = $event->getPayload();
/** @var TagQuery $query */
$query = $payload['query'];
$event->addSearchToggle($query);
if ($this->isGranted('manage_tag')) {
if ($this->isGranted('manage_tag') || $this->isGranted('create_tag')) {
$event->addCreate($this->path('tags_create'));
}
$event->addHelp($this->documentationLink('tags.html'));
}
}

View File

@@ -12,7 +12,7 @@ namespace App\EventSubscriber\Actions;
use App\Entity\Team;
use App\Event\PageActionsEvent;
class TeamSubscriber extends AbstractActionsSubscriber
final class TeamSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
@@ -31,17 +31,20 @@ class TeamSubscriber extends AbstractActionsSubscriber
}
if ($this->isGranted('edit', $team)) {
$event->addAction('edit', ['url' => $this->path('admin_team_edit', ['id' => $team->getId()])]);
if (!$event->isView('edit')) {
$event->addAction('edit', ['url' => $this->path('admin_team_edit', ['id' => $team->getId()]), 'title' => 'action.edit']);
}
if ($this->isGranted('create_team')) {
$event->addAction('copy', ['url' => $this->path('team_duplicate', ['id' => $team->getId(), 'token' => $payload['token']])]);
$event->addAction('copy', ['url' => $this->path('team_duplicate', ['id' => $team->getId()]), 'title' => 'copy', 'translation_domain' => 'actions', 'class' => 'modal-ajax-form']);
}
}
if ($event->isIndexView() && $this->isGranted('delete', $team)) {
$event->addAction('trash', [
'url' => $this->path('delete_team', ['id' => $team->getId()]),
'class' => 'api-link',
'class' => 'api-link text-red',
'translation_domain' => 'actions',
'attr' => [
'data-event' => 'kimai.teamDelete kimai.teamUpdate',
'data-method' => 'DELETE',

View File

@@ -10,9 +10,8 @@
namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
use App\Repository\Query\TeamQuery;
class TeamsSubscriber extends AbstractActionsSubscriber
final class TeamsSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
@@ -21,17 +20,8 @@ class TeamsSubscriber extends AbstractActionsSubscriber
public function onActions(PageActionsEvent $event): void
{
$payload = $event->getPayload();
/** @var TeamQuery $query */
$query = $payload['query'];
$event->addSearchToggle($query);
if ($this->isGranted('create_team')) {
$event->addCreate($this->path('admin_team_create'), false);
$event->addCreate($this->path('admin_team_create'));
}
$event->addHelp($this->documentationLink('teams.html'));
}
}

View File

@@ -1,26 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
class TimesheetMultiUpdateSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
return 'timesheets_multi_update';
}
public function onActions(PageActionsEvent $event): void
{
$event->addBack($this->path('timesheet'));
$event->addHelp($this->documentationLink('timesheet.html'));
}
}

View File

@@ -11,7 +11,7 @@ namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
class TimesheetSubscriber extends AbstractTimesheetSubscriber
final class TimesheetSubscriber extends AbstractTimesheetSubscriber
{
public static function getActionName(): string
{

View File

@@ -1,26 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
class TimesheetTeamMultiUpdateSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
return 'timesheets_team_multi_update';
}
public function onActions(PageActionsEvent $event): void
{
$event->addBack($this->path('admin_timesheet'));
$event->addHelp($this->documentationLink('timesheet.html'));
}
}

View File

@@ -11,7 +11,7 @@ namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
class TimesheetTeamSubscriber extends AbstractTimesheetSubscriber
final class TimesheetTeamSubscriber extends AbstractTimesheetSubscriber
{
public static function getActionName(): string
{

View File

@@ -10,9 +10,8 @@
namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
use App\Repository\Query\TimesheetQuery;
class TimesheetsSubscriber extends AbstractActionsSubscriber
final class TimesheetsSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
@@ -21,22 +20,12 @@ class TimesheetsSubscriber extends AbstractActionsSubscriber
public function onActions(PageActionsEvent $event): void
{
$payload = $event->getPayload();
/** @var TimesheetQuery $query */
$query = $payload['query'];
$event->addSearchToggle($query);
$event->addColumnToggle('#modal_timesheet');
if ($this->isGranted('export_own_timesheet')) {
$event->addAction('download', ['url' => $this->path('timesheet_export'), 'class' => 'toolbar-action modal-ajax-form']);
}
if ($this->isGranted('create_own_timesheet')) {
$event->addCreate($this->path('timesheet_create'));
}
$event->addHelp($this->documentationLink('timesheet.html'));
if ($this->isGranted('export_own_timesheet')) {
$event->addAction('download', ['url' => $this->path('timesheet_export'), 'class' => 'modal-ajax-form', 'title' => 'export']);
}
}
}

View File

@@ -10,9 +10,8 @@
namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
use App\Repository\Query\TimesheetQuery;
class TimesheetsTeamSubscriber extends AbstractActionsSubscriber
final class TimesheetsTeamSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
@@ -21,23 +20,13 @@ class TimesheetsTeamSubscriber extends AbstractActionsSubscriber
public function onActions(PageActionsEvent $event): void
{
$payload = $event->getPayload();
/** @var TimesheetQuery $query */
$query = $payload['query'];
$event->addSearchToggle($query);
$event->addColumnToggle('#modal_timesheet_admin');
if ($this->isGranted('create_other_timesheet')) {
$event->addAction('create', ['title' => 'create', 'url' => $this->path('admin_timesheet_create'), 'class' => 'create-ts modal-ajax-form']);
$event->addAction('multi-user', ['title' => 'create-timesheet-multiuser', 'translation_domain' => 'actions', 'url' => $this->path('admin_timesheet_create_multiuser'), 'class' => 'create-ts-mu modal-ajax-form', 'icon' => 'fas fa-user-plus']);
}
if ($this->isGranted('export_other_timesheet')) {
$event->addAction('download', ['url' => $this->path('admin_timesheet_export'), 'class' => 'toolbar-action modal-ajax-form']);
$event->addAction('download', ['url' => $this->path('admin_timesheet_export'), 'class' => 'modal-ajax-form', 'title' => 'export']);
}
if ($this->isGranted('create_other_timesheet')) {
$event->addActionToSubmenu('create', 'single', ['title' => 'create', 'url' => $this->path('admin_timesheet_create'), 'class' => 'create-ts modal-ajax-form']);
$event->addActionToSubmenu('create', 'multi-user', ['title' => 'create-timesheet-multiuser', 'translation_domain' => 'actions', 'url' => $this->path('admin_timesheet_create_multiuser'), 'class' => 'create-ts-mu modal-ajax-form']);
}
$event->addHelp($this->documentationLink('timesheet.html'));
}
}

View File

@@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\EventSubscriber\Actions;
use App\Entity\User;
use App\Event\PageActionsEvent;
final class UserFormsSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
return 'user_forms';
}
public function onActions(PageActionsEvent $event): void
{
$payload = $event->getPayload();
/** @var User $user */
$user = $payload['user'];
if ($user->getId() === null) {
return;
}
if ($this->isGranted('edit', $user)) {
$event->addAction('edit', ['url' => $this->path('user_profile_edit', ['username' => $user->getUserIdentifier()]), 'title' => 'edit', 'translation_domain' => 'actions']);
}
if ($this->isGranted('password', $user)) {
$event->addAction('password', ['url' => $this->path('user_profile_password', ['username' => $user->getUserIdentifier()]), 'title' => 'profile.password']);
}
if ($this->isGranted('2fa', $user)) {
$event->addAction('2fa', ['url' => $this->path('user_profile_2fa', ['username' => $user->getUserIdentifier()]), 'title' => 'profile.2fa']);
}
if ($this->isGranted('api-token', $user)) {
$event->addAction('api-token', ['url' => $this->path('user_profile_api_token', ['username' => $user->getUserIdentifier()]), 'title' => 'profile.api-token']);
}
if ($this->isGranted('teams', $user)) {
$event->addAction('teams', ['url' => $this->path('user_profile_teams', ['username' => $user->getUserIdentifier()]), 'title' => 'teams']);
}
if ($this->isGranted('roles', $user)) {
$event->addAction('roles', ['url' => $this->path('user_profile_roles', ['username' => $user->getUserIdentifier()]), 'title' => 'profile.roles']);
}
}
}

View File

@@ -12,7 +12,7 @@ namespace App\EventSubscriber\Actions;
use App\Entity\User;
use App\Event\PageActionsEvent;
class UserSubscriber extends AbstractActionsSubscriber
final class UserSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
@@ -31,43 +31,30 @@ class UserSubscriber extends AbstractActionsSubscriber
}
if ($this->isGranted('view', $user)) {
$event->addAction('profile-stats', ['icon' => 'avatar', 'url' => $this->path('user_profile', ['username' => $user->getUsername()]), 'translation_domain' => 'actions']);
$event->addAction('profile-stats', ['icon' => 'avatar', 'url' => $this->path('user_profile', ['username' => $user->getUserIdentifier()]), 'translation_domain' => 'actions', 'title' => 'profile-stats']);
$event->addDivider();
}
if ($this->isGranted('edit', $user)) {
$event->addActionToSubmenu('edit', 'edit', ['url' => $this->path('user_profile_edit', ['username' => $user->getUsername()]), 'title' => 'edit', 'translation_domain' => 'actions']);
$event->addAction('edit', ['url' => $this->path('user_profile_edit', ['username' => $user->getUserIdentifier()]), 'title' => 'edit', 'translation_domain' => 'actions']);
}
if ($this->isGranted('preferences', $user)) {
$event->addActionToSubmenu('edit', 'settings', ['url' => $this->path('user_profile_preferences', ['username' => $user->getUsername()]), 'title' => 'settings', 'translation_domain' => 'actions']);
}
if ($this->isGranted('password', $user)) {
$event->addActionToSubmenu('edit', 'password', ['url' => $this->path('user_profile_password', ['username' => $user->getUsername()]), 'title' => 'profile.password']);
}
if ($this->isGranted('api-token', $user)) {
$event->addActionToSubmenu('edit', 'api-token', ['url' => $this->path('user_profile_api_token', ['username' => $user->getUsername()]), 'title' => 'profile.api-token']);
}
if ($this->isGranted('teams', $user)) {
$event->addActionToSubmenu('edit', 'teams', ['url' => $this->path('user_profile_teams', ['username' => $user->getUsername()]), 'title' => 'profile.teams']);
}
if ($this->isGranted('roles', $user)) {
$event->addActionToSubmenu('edit', 'roles', ['url' => $this->path('user_profile_roles', ['username' => $user->getUsername()]), 'title' => 'profile.roles']);
$event->addConfig($this->path('user_profile_preferences', ['username' => $user->getUserIdentifier()]));
}
if ($event->hasSubmenu('edit')) {
$event->addDivider();
if ($this->isGranted('report:other') || ($this->isGranted('report:user') && $event->getUser()->getId() === $user->getId())) {
$event->addActionToSubmenu('report', 'weekly', ['url' => $this->path('report_user_week', ['user' => $user->getId()]), 'translation_domain' => 'reporting', 'title' => 'report_user_week']);
$event->addActionToSubmenu('report', 'monthly', ['url' => $this->path('report_user_month', ['user' => $user->getId()]), 'translation_domain' => 'reporting', 'title' => 'report_user_month']);
$event->addActionToSubmenu('report', 'yearly', ['url' => $this->path('report_user_year', ['user' => $user->getId()]), 'translation_domain' => 'reporting', 'title' => 'report_user_year']);
}
$viewOther = $this->isGranted('view_other_timesheet');
if ($this->isGranted('view_reporting')) {
// also found in App\Controller\Reporting\ReportByUserController
if (($viewOther && $this->isGranted('view_other_reporting')) || ($event->getUser()->getId() === $user->getId())) {
$event->addAction('menu.reporting', ['url' => $this->path('report_user_month', ['user' => $user->getId()]), 'icon' => 'reporting']);
}
if ($this->isGranted('view_other_timesheet') && $user->isEnabled()) {
$event->addActionToSubmenu('filter', 'timesheet', ['url' => $this->path('admin_timesheet', ['users[]' => $user->getId()]), 'title' => 'timesheet.filter', 'translation_domain' => 'actions']);
}
if ($viewOther && $user->isEnabled()) {
$event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'translation_domain' => 'actions', 'url' => $this->path('admin_timesheet', ['users[]' => $user->getId()])]);
if ($this->isGranted('view_team')) {
$event->addActionToSubmenu('filter', 'teams', ['url' => $this->path('admin_team', ['users[]' => $user->getId()]), 'title' => 'teams']);
}
if ($event->isIndexView() && $this->isGranted('delete', $user)) {

View File

@@ -10,9 +10,8 @@
namespace App\EventSubscriber\Actions;
use App\Event\PageActionsEvent;
use App\Repository\Query\UserQuery;
class UsersSubscriber extends AbstractActionsSubscriber
final class UsersSubscriber extends AbstractActionsSubscriber
{
public static function getActionName(): string
{
@@ -21,27 +20,16 @@ class UsersSubscriber extends AbstractActionsSubscriber
public function onActions(PageActionsEvent $event): void
{
$payload = $event->getPayload();
/** @var UserQuery $query */
$query = $payload['query'];
$event->addSearchToggle($query);
if ($event->isIndexView()) {
$event->addColumnToggle('#modal_user_admin');
if ($this->isGranted('create_user')) {
$event->addCreate($this->path('admin_user_create'), true);
}
$event->addQuickExport($this->path('user_export'));
if ($this->isGranted('create_user')) {
$event->addCreate($this->path('admin_user_create'), false);
if ($this->isGranted('report:other')) {
$event->addActionToSubmenu('report', 'weekly', ['url' => $this->path('report_weekly_users'), 'translation_domain' => 'reporting', 'title' => 'report_weekly_users']);
$event->addActionToSubmenu('report', 'monthly', ['url' => $this->path('report_monthly_users'), 'translation_domain' => 'reporting', 'title' => 'report_monthly_users']);
$event->addActionToSubmenu('report', 'yearly', ['url' => $this->path('report_yearly_users'), 'translation_domain' => 'reporting', 'title' => 'report_yearly_users']);
}
if ($this->isGranted('system_configuration')) {
$event->addAction('settings', ['url' => $this->path('system_configuration_section', ['section' => 'user']), 'class' => 'modal-ajax-form']);
}
$event->addHelp($this->documentationLink('users.html'));
}
}

View File

@@ -17,7 +17,7 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\AuthenticationExpiredException;
class AjaxAuthenticationSubscriber implements EventSubscriberInterface
final class AjaxAuthenticationSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
@@ -26,11 +26,12 @@ class AjaxAuthenticationSubscriber implements EventSubscriberInterface
];
}
public function onCoreException(ExceptionEvent $event)
public function onCoreException(ExceptionEvent $event): void
{
$request = $event->getRequest();
$header = $request->headers->get('X-Requested-With');
if ($request->isXmlHttpRequest()) {
if ($request->isXmlHttpRequest() || ($header !== null && str_contains(strtolower($header), 'kimai'))) {
$exception = $event->getThrowable();
if ($exception instanceof AuthenticationExpiredException) {
$event->setResponse(new Response('Session expired', 403, ['Login-Required' => true]));

View File

@@ -18,11 +18,8 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
*/
final class EmailSubscriber implements EventSubscriberInterface
{
private $mailer;
public function __construct(KimaiMailer $mailer)
public function __construct(private KimaiMailer $mailer)
{
$this->mailer = $mailer;
}
public static function getSubscribedEvents(): array
@@ -32,7 +29,7 @@ final class EmailSubscriber implements EventSubscriberInterface
];
}
public function onMailEvent(EmailEvent $event)
public function onMailEvent(EmailEvent $event): void
{
$this->mailer->send($event->getEmail());
}

View File

@@ -13,50 +13,38 @@ use App\Entity\User;
use App\Event\UserInteractiveLoginEvent;
use App\Repository\UserRepository;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
class LastLoginSubscriber implements EventSubscriberInterface
final class LastLoginSubscriber implements EventSubscriberInterface
{
private $repository;
public function __construct(UserRepository $repository)
public function __construct(private UserRepository $repository)
{
$this->repository = $repository;
}
/**
* @return array
*/
public static function getSubscribedEvents()
public static function getSubscribedEvents(): array
{
return [
// triggered for programmatic logins like "password reset" or "registration"
UserInteractiveLoginEvent::class => 'onImplicitLogin',
SecurityEvents::INTERACTIVE_LOGIN => 'onSecurityInteractiveLogin',
// We do not use the InteractiveLoginEvent because it is not triggered e.g. for SAML
LoginSuccessEvent::class => 'onFormLogin',
];
}
public function onImplicitLogin(UserInteractiveLoginEvent $event)
public function onImplicitLogin(UserInteractiveLoginEvent $event): void
{
$user = $event->getUser();
$user->setLastLogin(new \DateTime());
$user->setLastLogin(new \DateTime('now', new \DateTimeZone($user->getTimezone())));
$this->repository->saveUser($user);
}
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
public function onFormLogin(LoginSuccessEvent $event): void
{
// do not count API calls as logins
// this point could be used to add API rate limitation in the future
if ($event->getAuthenticationToken() instanceof PostAuthenticationGuardToken) {
return;
}
$user = $event->getAuthenticationToken()->getUser();
$user = $event->getUser();
if ($user instanceof User) {
$user->setLastLogin(new \DateTime());
$user->setLastLogin(new \DateTime('now', new \DateTimeZone($user->getTimezone())));
$this->repository->saveUser($user);
}
}

View File

@@ -9,112 +9,98 @@
namespace App\EventSubscriber;
use App\Entity\User;
use App\Event\ConfigureMainMenuEvent;
use App\Utils\MenuItemModel;
use App\Utils\MenuItemModel as KimaiMenuItemModel;
use KevinPapst\AdminLTEBundle\Event\SidebarMenuEvent;
use KevinPapst\AdminLTEBundle\Model\MenuItemInterface;
use KevinPapst\TablerBundle\Event\MenuEvent;
use KevinPapst\TablerBundle\Model\MenuItemInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Security;
/**
* Class MenuBuilder configures the main navigation.
* @internal
*/
class MenuBuilderSubscriber implements EventSubscriberInterface
final class MenuBuilderSubscriber implements EventSubscriberInterface
{
/**
* @var EventDispatcherInterface
*/
private $eventDispatcher;
/**
* @var TokenStorageInterface
*/
private $tokenStorage;
public function __construct(EventDispatcherInterface $dispatcher, TokenStorageInterface $storage)
public function __construct(private EventDispatcherInterface $eventDispatcher, private Security $security)
{
$this->eventDispatcher = $dispatcher;
$this->tokenStorage = $storage;
}
/**
* @return array
*/
public static function getSubscribedEvents(): array
{
return [
SidebarMenuEvent::class => ['onSetupNavbar', 100],
MenuEvent::class => ['onSetupNavbar', 100],
];
}
/**
* Generate the main menu.
*
* @param SidebarMenuEvent $event
*/
public function onSetupNavbar(SidebarMenuEvent $event)
public function onSetupNavbar(MenuEvent $event): void
{
$request = $event->getRequest();
$menuEvent = new ConfigureMainMenuEvent();
$event->addItem(
new MenuItemModel('dashboard', 'menu.homepage', 'dashboard', [], 'fas fa-tachometer-alt')
);
$menuEvent = new ConfigureMainMenuEvent(
$request,
$event,
new MenuItemModel('admin', 'menu.admin', ''),
new MenuItemModel('system', 'menu.system', '')
);
/** @var User $user */
$user = $this->security->getUser();
// error pages don't have a user and will fail when is_granted() is called
if (null !== $this->tokenStorage->getToken()) {
if (null !== $user) {
$this->eventDispatcher->dispatch($menuEvent);
}
foreach ($menuEvent->getMenu()->getChildren() as $child) {
if ($child->getRoute() === null && !$child->hasChildren()) {
continue;
}
$event->addItem($child);
}
if ($menuEvent->getAppsMenu()->hasChildren()) {
$event->addItem($menuEvent->getAppsMenu());
}
if ($menuEvent->getAdminMenu()->hasChildren()) {
$event->addItem(new MenuItemModel('admin', 'menu.admin', ''));
foreach ($menuEvent->getAdminMenu()->getChildren() as $child) {
$event->addItem($child);
}
$event->addItem($menuEvent->getAdminMenu());
}
if ($menuEvent->getSystemMenu()->hasChildren()) {
$event->addItem(new MenuItemModel('system', 'menu.system', ''));
foreach ($menuEvent->getSystemMenu()->getChildren() as $child) {
$event->addItem($child);
}
$event->addItem($menuEvent->getSystemMenu());
}
$this->activateByRoute(
$event->getRequest()->get('_route'),
$event->getItems()
);
$route = $event->getRequest()->get('_route');
if (!\is_string($route)) {
return;
}
$this->activateByRoute($route, $event->getItems());
}
/**
* @param string $route
* @param MenuItemInterface[] $items
* @return bool
*/
protected function activateByRoute($route, $items)
private function activateByRoute(string $route, array $items): bool
{
foreach ($items as $item) {
if ($item->hasChildren()) {
$this->activateByRoute($route, $item->getChildren());
} else {
if ($item->getRoute() == $route) {
if ($item instanceof MenuItemModel) {
if ($item->isChildRoute($route)) {
$item->setIsActive(true);
continue;
return true;
}
if ($item instanceof KimaiMenuItemModel) {
if ($item->isChildRoute($route)) {
$item->setIsActive(true);
continue;
}
}
if ($item->getRoute() === $route) {
$item->setIsActive(true);
return true;
}
if ($item->hasChildren()) {
if ($this->activateByRoute($route, $item->getChildren())) {
return true;
}
}
}
return false;
}
}

View File

@@ -0,0 +1,75 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\EventSubscriber;
use App\Entity\User;
use App\Event\ConfigureMainMenuEvent;
use App\Utils\MenuItemModel;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Security;
final class MenuFavoritesSubscriber implements EventSubscriberInterface
{
public function __construct(private Security $security)
{
}
public static function getSubscribedEvents(): array
{
return [
ConfigureMainMenuEvent::class => ['onMainMenuConfigure', 90], // see MenuSubscriber
];
}
public function onMainMenuConfigure(ConfigureMainMenuEvent $menuEvent): void
{
/** @var User $user */
$user = $this->security->getUser();
if (null === $user) {
return;
}
$favMenu = $menuEvent->getMenu()->findChild('favorites');
if ($favMenu === null) {
return;
}
$userFavorites = $user->getPreferenceValue('favorite_routes');
if (!\is_string($userFavorites) || trim($userFavorites) === '') {
return;
}
$root = new MenuItemModel('', '');
$root->addChild($menuEvent->getMenu());
if ($menuEvent->getAppsMenu()->hasChildren()) {
$root->addChild($menuEvent->getAppsMenu());
}
if ($menuEvent->getAdminMenu()->hasChildren()) {
$root->addChild($menuEvent->getAdminMenu());
}
if ($menuEvent->getSystemMenu()->hasChildren()) {
$root->addChild($menuEvent->getSystemMenu());
}
$userFavorites = explode(',', $userFavorites);
foreach ($userFavorites as $fav) {
$tmp = $root->findChild($fav);
if ($tmp !== null && !$tmp->hasChildren()) {
$favMenu->addChild(clone $tmp);
}
}
if ($favMenu->hasChildren()) {
$favMenu->setExpanded(true);
$menuEvent->getTimesheetMenu()?->setExpanded(false);
}
}
}

View File

@@ -10,21 +10,15 @@
namespace App\EventSubscriber;
use App\Event\ConfigureMainMenuEvent;
use App\Twig\IconExtension;
use App\Utils\MenuItemModel;
use KevinPapst\TablerBundle\Helper\ContextHelper;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
/**
* Menu event subscriber is creating the Kimai default menu structure.
*/
final class MenuSubscriber implements EventSubscriberInterface
{
private $security;
public function __construct(AuthorizationCheckerInterface $security)
public function __construct(private AuthorizationCheckerInterface $security, private ContextHelper $helper)
{
$this->security = $security;
}
public static function getSubscribedEvents(): array
@@ -34,7 +28,14 @@ final class MenuSubscriber implements EventSubscriberInterface
];
}
public function onMainMenuConfigure(ConfigureMainMenuEvent $event)
private function addDivider(MenuItemModel $menu): void
{
if ($this->helper->isBoxedLayout()) {
$menu->addChild(MenuItemModel::createDivider());
}
}
public function onMainMenuConfigure(ConfigureMainMenuEvent $event): void
{
$auth = $this->security;
@@ -42,68 +43,94 @@ final class MenuSubscriber implements EventSubscriberInterface
return;
}
$icons = new IconExtension();
// ------------------- main menu -------------------
// main menu
$menu = $event->getMenu();
$menu->addChild(new MenuItemModel('dashboard', 'dashboard.title', 'dashboard', [], 'dashboard'));
$menu->addChild(new MenuItemModel('favorites', 'favorite_routes', null, [], 'bookmarked'));
// ------------------- timesheet menu -------------------
$times = new MenuItemModel('timesheet', 'time_tracking', null, [], 'timesheet');
if ($auth->isGranted('view_own_timesheet')) {
$timesheets = new MenuItemModel('timesheet', 'menu.timesheet', 'timesheet', [], $icons->icon('timesheet'));
$timesheets->setChildRoutes(['timesheet_export', 'timesheet_edit', 'timesheet_create', 'timesheet_multi_update']);
$menu->addItem($timesheets);
$timesheets = new MenuItemModel('times', 'my_times', 'timesheet', [], 'timesheet');
$timesheets->setChildRoutes(['times', 'timesheet_export', 'timesheet_edit', 'timesheet_create', 'timesheet_multi_update']);
$times->addChild($timesheets);
if ($auth->isGranted('quick-entry')) {
$menu->addItem(
new MenuItemModel('quick_entry', 'quick_entry.title', 'quick_entry', [], $icons->icon('weekly-times'))
$times->addChild(
new MenuItemModel('quick_entry', 'quick_entry.title', 'quick_entry', [], 'weekly-times')
);
}
$menu->addItem(
new MenuItemModel('calendar', 'calendar', 'calendar', [], $icons->icon('calendar'))
$times->addChild(
new MenuItemModel('calendar', 'calendar', 'calendar', [], 'calendar')
);
}
if ($auth->isGranted('view_invoice')) {
$invoice = new MenuItemModel('invoice', 'invoices', 'invoice', [], $icons->icon('invoice'));
$invoice->setChildRoutes(['admin_invoice_template', 'admin_invoice_template_edit', 'admin_invoice_template_create', 'admin_invoice_template_copy', 'admin_invoice_list', 'admin_invoice_document_upload']);
$menu->addItem($invoice);
}
$this->addDivider($times);
if ($auth->isGranted('create_export')) {
$menu->addItem(
new MenuItemModel('export', 'menu.export', 'export', [], $icons->icon('export'))
$times->addChild(
new MenuItemModel('export', 'export', 'export', [], 'export')
);
}
// ------------------- admin menu -------------------
$menu = $event->getAdminMenu();
if ($auth->isGranted('view_other_timesheet')) {
$timesheets = new MenuItemModel('timesheet_admin', 'menu.admin_timesheet', 'admin_timesheet', [], $icons->icon('timesheet-team'));
$timesheets = new MenuItemModel('timesheet_admin', 'all_times', 'admin_timesheet', [], 'timesheet-team');
$timesheets->setChildRoutes(['admin_timesheet_export', 'admin_timesheet_edit', 'admin_timesheet_create', 'admin_timesheet_multi_update']);
$menu->addChild($timesheets);
$times->addChild($timesheets);
}
if ($times->hasChildren()) {
$times->setExpanded(true); // Kimai is all about time-tracking, so we expand this menu always
$menu->addChild($times);
}
if ($auth->isGranted('view_reporting')) {
$reporting = new MenuItemModel('reporting', 'menu.reporting', 'reporting', [], $icons->icon('reporting'));
$reporting = new MenuItemModel('reporting', 'menu.reporting', 'reporting', [], 'reporting');
$reporting->setChildRoutes(['report_user_week', 'report_user_month', 'report_weekly_users', 'report_monthly_users', 'report_project_view']);
$menu->addChild($reporting);
}
$invoice = new MenuItemModel('invoice', 'invoices', null, [], 'invoice');
$invoice->setChildRoutes(['admin_invoice_template', 'admin_invoice_template_edit', 'admin_invoice_template_create', 'admin_invoice_template_copy', 'admin_invoice_list', 'admin_invoice_document_upload', 'admin_invoice_edit']);
if ($auth->isGranted('create_invoice')) {
$invoice->addChild(new MenuItemModel('invoice', 'invoice_form.title', 'invoice', [], 'invoice'));
}
if ($auth->isGranted('view_invoice')) {
$invoice->addChild(new MenuItemModel('invoice-listing', 'all_invoices', 'admin_invoice_list', [], 'list'));
}
if ($auth->isGranted('manage_invoice_template')) {
$invoice->addChild(new MenuItemModel('invoice-template', 'admin_invoice_template.title', 'admin_invoice_template', [], 'invoice-template'));
}
if ($invoice->hasChildren()) {
$this->addDivider($invoice);
}
$menu->addChild($invoice);
// ------------------- admin menu -------------------
$menu = $event->getAdminMenu();
if ($auth->isGranted('view_customer') || $auth->isGranted('view_teamlead_customer') || $auth->isGranted('view_team_customer')) {
$customers = new MenuItemModel('customer_admin', 'customers', 'admin_customer', [], $icons->icon('customer'));
$customers = new MenuItemModel('customer_admin', 'customers', 'admin_customer', [], 'customer');
$customers->setChildRoutes(['admin_customer_create', 'admin_customer_permissions', 'customer_details', 'admin_customer_edit', 'admin_customer_delete']);
$menu->addChild($customers);
}
if ($auth->isGranted('view_project') || $auth->isGranted('view_teamlead_project') || $auth->isGranted('view_team_project')) {
$projects = new MenuItemModel('project_admin', 'projects', 'admin_project', [], $icons->icon('project'));
$projects = new MenuItemModel('project_admin', 'projects', 'admin_project', [], 'project');
$projects->setChildRoutes(['admin_project_permissions', 'admin_project_create', 'project_details', 'admin_project_edit', 'admin_project_delete']);
$menu->addChild($projects);
}
if ($auth->isGranted('view_activity') || $auth->isGranted('view_teamlead_activity') || $auth->isGranted('view_team_activity')) {
$activities = new MenuItemModel('activity_admin', 'activities', 'admin_activity', [], $icons->icon('activity'));
$activities = new MenuItemModel('activity_admin', 'activities', 'admin_activity', [], 'activity');
$activities->setChildRoutes(['admin_activity_create', 'activity_details', 'admin_activity_edit', 'admin_activity_delete']);
$menu->addChild($activities);
}
@@ -114,42 +141,50 @@ final class MenuSubscriber implements EventSubscriberInterface
);
}
$this->addDivider($menu);
// ------------------- system menu -------------------
$menu = $event->getSystemMenu();
if ($auth->isGranted('view_user')) {
$users = new MenuItemModel('user_admin', 'users', 'admin_user', [], $icons->icon('users'));
$users->setChildRoutes(['admin_user_create', 'admin_user_delete', 'user_profile', 'user_profile_edit', 'user_profile_password', 'user_profile_api_token', 'user_profile_roles', 'user_profile_teams', 'user_profile_preferences']);
$users = new MenuItemModel('user_admin', 'users', 'admin_user', [], 'users');
$users->setChildRoutes(['admin_user_create', 'admin_user_delete', 'user_profile', 'user_profile_edit', 'user_profile_password', 'user_profile_api_token', 'user_profile_roles', 'user_profile_teams', 'user_profile_preferences', 'user_profile_2fa']);
$menu->addChild($users);
}
if ($auth->isGranted('role_permissions')) {
$users = new MenuItemModel('admin_user_permissions', 'profile.roles', 'admin_user_permissions', [], $icons->icon('permissions'));
$users = new MenuItemModel('admin_user_permissions', 'profile.roles', 'admin_user_permissions', [], 'permissions');
$menu->addChild($users);
}
if ($auth->isGranted('view_team')) {
$teams = new MenuItemModel('user_team', 'menu.admin_team', 'admin_team', [], $icons->icon('team'));
$teams = new MenuItemModel('user_team', 'teams', 'admin_team', [], 'team');
$teams->setChildRoutes(['admin_team_create', 'admin_team_edit']);
$menu->addChild($teams);
}
if ($menu->hasChildren()) {
$this->addDivider($menu);
}
if ($auth->isGranted('plugins')) {
$menu->addChild(
new MenuItemModel('plugins', 'menu.plugin', 'plugins', [], $icons->icon('plugin'))
new MenuItemModel('plugins', 'menu.plugin', 'plugins', [], 'plugin')
);
}
if ($auth->isGranted('system_configuration')) {
$menu->addChild(
new MenuItemModel('system_configuration', 'menu.system_configuration', 'system_configuration', [], $icons->icon('configuration'))
);
$systemConfig = new MenuItemModel('system_configuration', 'menu.system_configuration', 'system_configuration', [], 'configuration');
$systemConfig->setChildRoutes(['system_configuration_update', 'system_configuration_section']);
$menu->addChild($systemConfig);
}
if ($auth->isGranted('system_information')) {
$menu->addChild(
new MenuItemModel('doctor', 'menu.doctor', 'doctor', [], $icons->icon('doctor'))
new MenuItemModel('doctor', 'Doctor', 'doctor', [], 'doctor')
);
}
$this->addDivider($menu);
}
}

View File

@@ -30,10 +30,7 @@ final class PagerfantaExceptionSubscriber implements EventSubscriberInterface
];
}
/**
* @param ExceptionEvent $event
*/
public function onCoreException(ExceptionEvent $event)
public function onCoreException(ExceptionEvent $event): void
{
$throwable = $event->getThrowable();

View File

@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\EventSubscriber;
use App\Utils\ProfileManager;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
final class ProfileSubscriber implements EventSubscriberInterface
{
public function __construct(private ProfileManager $profileManager)
{
}
public static function getSubscribedEvents(): array
{
return [
// We do not use the InteractiveLoginEvent because it is not triggered e.g. for SAML
LoginSuccessEvent::class => 'onFormLogin',
];
}
public function onFormLogin(LoginSuccessEvent $event): void
{
$request = $event->getRequest();
$profile = $this->profileManager->getProfileFromCookie($request);
if ($this->profileManager->isValidProfile($profile)) {
$this->profileManager->setProfile($request->getSession(), $profile);
}
}
}

View File

@@ -9,7 +9,7 @@
namespace App\EventSubscriber;
use App\Utils\LanguageService;
use App\Configuration\LocaleService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
@@ -24,15 +24,10 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
*
* @author Oleg Voronkovich <oleg-voronkovich@yandex.ru>
*/
class RedirectToLocaleSubscriber implements EventSubscriberInterface
final class RedirectToLocaleSubscriber implements EventSubscriberInterface
{
private $urlGenerator;
private $languageService;
public function __construct(UrlGeneratorInterface $urlGenerator, LanguageService $languageService)
public function __construct(private UrlGeneratorInterface $urlGenerator, private LocaleService $localeService)
{
$this->urlGenerator = $urlGenerator;
$this->languageService = $languageService;
}
public static function getSubscribedEvents(): array
@@ -42,7 +37,7 @@ class RedirectToLocaleSubscriber implements EventSubscriberInterface
];
}
public function onKernelRequest(RequestEvent $event)
public function onKernelRequest(RequestEvent $event): void
{
$request = $event->getRequest();
@@ -58,11 +53,11 @@ class RedirectToLocaleSubscriber implements EventSubscriberInterface
return;
}
$allLanguages = $this->languageService->getAllLanguages();
$allLanguages = $this->localeService->getAllLocales();
// Add the default locale at the first position of the array, because getPreferredLanguage()
// returns the first element when no appropriate language is found
array_unshift($allLanguages, $this->languageService->getDefaultLanguage());
array_unshift($allLanguages, $this->localeService->getDefaultLocale());
$preferredLanguage = $request->getPreferredLanguage(array_unique($allLanguages));

View File

@@ -9,38 +9,24 @@
namespace App\EventSubscriber;
use App\Configuration\LocaleService;
use App\Entity\User;
use App\Entity\UserPreference;
use App\Form\Type\SkinType;
use KevinPapst\AdminLTEBundle\Helper\ContextHelper;
use KevinPapst\TablerBundle\Helper\ContextHelper;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\KernelEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
/**
* Allows dynamic injection of theme related options.
* Prepare all theme settings for user and context.
*/
final class ThemeOptionsSubscriber implements EventSubscriberInterface
{
/**
* @var TokenStorageInterface
*/
private $storage;
/**
* @var ContextHelper
*/
private $helper;
public function __construct(TokenStorageInterface $storage, ContextHelper $helper)
public function __construct(private TokenStorageInterface $storage, private ContextHelper $helper, private LocaleService $localeService)
{
$this->storage = $storage;
$this->helper = $helper;
}
/**
* @return array
*/
public static function getSubscribedEvents(): array
{
return [
@@ -51,10 +37,14 @@ final class ThemeOptionsSubscriber implements EventSubscriberInterface
public function setThemeOptions(KernelEvent $event): void
{
// Ignore sub-requests
if (!$event->isMasterRequest()) {
if (!$event->isMainRequest()) {
return;
}
if ($this->localeService->isRightToLeft(\Locale::getDefault())) {
$this->helper->setIsRightToLeft(true);
}
// ignore events like the toolbar where we do not have a token
if (null === $this->storage->getToken()) {
return;
@@ -66,30 +56,18 @@ final class ThemeOptionsSubscriber implements EventSubscriberInterface
return;
}
/** @var UserPreference $ref */
foreach ($user->getPreferences() as $ref) {
$name = $ref->getName();
switch ($name) {
case UserPreference::SKIN:
if (!empty($ref->getValue()) && \in_array($ref->getValue(), SkinType::THEMES)) {
$this->helper->setOption('skin', 'skin-' . $ref->getValue());
}
break;
$skin = $user->getPreferenceValue(UserPreference::SKIN);
if ($skin === 'dark') {
$this->helper->setIsDarkMode(true);
}
case 'theme.layout':
if ($ref->getValue() === 'boxed') {
$this->helper->setOption('boxed_layout', true);
$this->helper->setOption('fixed_layout', false);
} else {
$this->helper->setOption('boxed_layout', false);
$this->helper->setOption('fixed_layout', true);
}
break;
case 'theme.collapsed_sidebar':
$this->helper->setOption('collapsed_sidebar', $ref->getValue());
break;
}
// do not allow boxed layout, header is not compatible and other functions need the full size as well
$this->helper->setIsBoxedLayout(false);
$this->helper->setIsCondensedUserMenu(false);
$this->helper->setIsCondensedNavbar(false);
$this->helper->setIsNavbarOverlapping(false);
if (!$this->helper->isDarkMode()) {
$this->helper->setIsNavbarDark(true);
}
}
}

View File

@@ -0,0 +1,63 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\EventSubscriber;
use App\Entity\User;
use App\Utils\MenuItemModel;
use KevinPapst\TablerBundle\Event\UserDetailsEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Security;
/**
* @internal
*/
class UserDetailsSubscriber implements EventSubscriberInterface
{
public function __construct(private AuthorizationCheckerInterface $auth, private Security $security)
{
}
public static function getSubscribedEvents(): array
{
return [
UserDetailsEvent::class => ['onUserDetailsEvent', 100],
];
}
public function onUserDetailsEvent(UserDetailsEvent $event): void
{
/** @var User|null $user */
$user = $this->security->getUser();
if ($user === null) {
return;
}
$event->setUser($user);
if ($this->auth->isGranted('view', $user)) {
$event->addLink(
new MenuItemModel('user_profile', 'my.profile', 'user_profile', ['username' => $user->getUserIdentifier()], 'fas fa-tachometer-alt')
);
}
if ($this->auth->isGranted('edit', $user)) {
$event->addLink(
new MenuItemModel('user_profile_edit', 'action.edit', 'user_profile_edit', ['username' => $user->getUserIdentifier()], 'fas fa-tachometer-alt')
);
}
if ($this->auth->isGranted('preferences', $user)) {
$event->addLink(
new MenuItemModel('user_profile_preferences', 'profile.preferences', 'user_profile_preferences', ['username' => $user->getUserIdentifier()], 'fas fa-tachometer-alt')
);
}
}
}

View File

@@ -18,19 +18,8 @@ use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
final class UserEnvironmentSubscriber implements EventSubscriberInterface
{
/**
* @var TokenStorageInterface
*/
private $storage;
/**
* @var AuthorizationCheckerInterface
*/
private $auth;
public function __construct(TokenStorageInterface $tokenStorage, AuthorizationCheckerInterface $auth)
public function __construct(private TokenStorageInterface $tokenStorage, private AuthorizationCheckerInterface $auth)
{
$this->storage = $tokenStorage;
$this->auth = $auth;
}
public static function getSubscribedEvents(): array
@@ -43,7 +32,7 @@ final class UserEnvironmentSubscriber implements EventSubscriberInterface
public function prepareEnvironment(RequestEvent $event): void
{
// ignore sub-requests
if (!$event->isMasterRequest()) {
if (!$event->isMainRequest()) {
return;
}
@@ -51,7 +40,7 @@ final class UserEnvironmentSubscriber implements EventSubscriberInterface
\Locale::setDefault($event->getRequest()->getLocale());
// ignore events like the toolbar where we do not have a token
if (null === ($token = $this->storage->getToken())) {
if (null === ($token = $this->tokenStorage->getToken())) {
return;
}

View File

@@ -10,37 +10,30 @@
namespace App\EventSubscriber;
use App\Configuration\SystemConfiguration;
use App\Controller\HomepageController;
use App\Entity\User;
use App\Entity\UserPreference;
use App\Event\PrepareUserEvent;
use App\Event\UserPreferenceEvent;
use App\Form\Type\CalendarViewType;
use App\Form\Type\FavoriteMenuType;
use App\Form\Type\FirstWeekDayType;
use App\Form\Type\InitialViewType;
use App\Form\Type\LanguageType;
use App\Form\Type\ReportType;
use App\Form\Type\SkinType;
use App\Form\Type\ThemeLayoutType;
use App\Form\Type\TimezoneType;
use App\Reporting\ReportingService;
use App\Form\Type\YesNoType;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\Range;
final class UserPreferenceSubscriber implements EventSubscriberInterface
{
private $eventDispatcher;
private $voter;
private $configuration;
public function __construct(EventDispatcherInterface $eventDispatcher, AuthorizationCheckerInterface $voter, SystemConfiguration $systemConfiguration)
public function __construct(private EventDispatcherInterface $eventDispatcher, private AuthorizationCheckerInterface $voter, private SystemConfiguration $systemConfiguration)
{
$this->eventDispatcher = $eventDispatcher;
$this->voter = $voter;
$this->configuration = $systemConfiguration;
}
public static function getSubscribedEvents(): array
@@ -54,141 +47,93 @@ final class UserPreferenceSubscriber implements EventSubscriberInterface
* @param User $user
* @return UserPreference[]
*/
public function getDefaultPreferences(User $user)
public function getDefaultPreferences(User $user): array
{
$timezone = $this->configuration->getUserDefaultTimezone();
$timezone = $this->systemConfiguration->getUserDefaultTimezone();
if (null === $timezone) {
$timezone = date_default_timezone_get();
}
$enableDefaultReport = $this->voter->isGranted('view_reporting');
$enableHourlyRate = false;
$hourlyRateOptions = [];
if ($this->voter->isGranted('hourly-rate', $user)) {
$enableHourlyRate = true;
$hourlyRateOptions = ['currency' => $this->configuration->getUserDefaultCurrency()];
$hourlyRateOptions = ['currency' => $this->systemConfiguration->getUserDefaultCurrency()];
}
return [
(new UserPreference())
->setName(UserPreference::HOURLY_RATE)
->setValue(0)
(new UserPreference(UserPreference::HOURLY_RATE, 0))
->setOrder(100)
->setSection('rate')
->setType(MoneyType::class)
->setEnabled($enableHourlyRate)
->setOptions($hourlyRateOptions)
->setOptions(array_merge($hourlyRateOptions, ['label' => 'hourlyRate']))
->addConstraint(new Range(['min' => 0])),
(new UserPreference())
->setName(UserPreference::INTERNAL_RATE)
->setValue(null)
(new UserPreference(UserPreference::INTERNAL_RATE, null))
->setOrder(101)
->setSection('rate')
->setType(MoneyType::class)
->setEnabled($enableHourlyRate)
->setOptions(array_merge($hourlyRateOptions, ['label' => 'label.rate_internal', 'required' => false]))
->setOptions(array_merge($hourlyRateOptions, ['label' => 'internalRate', 'required' => false]))
->addConstraint(new Range(['min' => 0])),
(new UserPreference())
->setName(UserPreference::TIMEZONE)
->setValue($timezone)
(new UserPreference(UserPreference::TIMEZONE, $timezone))
->setOrder(200)
->setSection('locale')
->setType(TimezoneType::class),
(new UserPreference())
->setName(UserPreference::LOCALE)
->setValue($this->configuration->getUserDefaultLanguage())
(new UserPreference(UserPreference::LOCALE, $this->systemConfiguration->getUserDefaultLanguage()))
->setOrder(250)
->setSection('locale')
->setType(LanguageType::class),
(new UserPreference())
->setName(UserPreference::FIRST_WEEKDAY)
->setValue(User::DEFAULT_FIRST_WEEKDAY)
(new UserPreference(UserPreference::FIRST_WEEKDAY, User::DEFAULT_FIRST_WEEKDAY))
->setOrder(300)
->setSection('locale')
->setType(FirstWeekDayType::class),
(new UserPreference())
->setName(UserPreference::HOUR_24)
->setValue(true)
->setOrder(305)
->setSection('locale')
->setType(CheckboxType::class),
(new UserPreference())
->setName(UserPreference::SKIN)
->setValue($this->configuration->getUserDefaultTheme())
(new UserPreference(UserPreference::SKIN, $this->systemConfiguration->getUserDefaultTheme()))
->setOrder(400)
->setSection('theme')
->setType(SkinType::class),
(new UserPreference())
->setName('theme.layout')
->setValue('fixed')
->setOrder(450)
->setSection('theme')
->setType(ThemeLayoutType::class),
(new UserPreference())
->setName('theme.collapsed_sidebar')
->setValue(false)
->setOrder(500)
->setSection('theme')
->setType(CheckboxType::class),
(new UserPreference())
->setName('theme.update_browser_title')
->setValue(true)
(new UserPreference('update_browser_title', true))
->setOrder(550)
->setSection('theme')
->setType(CheckboxType::class),
->setType(YesNoType::class),
(new UserPreference())
->setName('calendar.initial_view')
->setValue(CalendarViewType::DEFAULT_VIEW)
(new UserPreference('calendar_initial_view', CalendarViewType::DEFAULT_VIEW))
->setOrder(600)
->setSection('behaviour')
->setType(CalendarViewType::class),
(new UserPreference())
->setName('reporting.initial_view')
->setValue(ReportingService::DEFAULT_VIEW)
->setOrder(650)
->setSection('behaviour')
->setEnabled($enableDefaultReport)
->setType(ReportType::class),
(new UserPreference())
->setName('login.initial_view')
->setValue(InitialViewType::DEFAULT_VIEW)
(new UserPreference('login_initial_view', HomepageController::DEFAULT_ROUTE))
->setOrder(700)
->setSection('behaviour')
->setType(InitialViewType::class),
(new UserPreference())
->setName('timesheet.daily_stats')
->setValue(false)
(new UserPreference('favorite_routes', ''))
->setOrder(710)
->setSection('behaviour')
->setOptions(['required' => false])
->addConstraint(new Length(['max' => 75]))
->setType(FavoriteMenuType::class),
(new UserPreference('daily_stats', false))
->setOrder(800)
->setSection('behaviour')
->setType(CheckboxType::class),
->setType(YesNoType::class),
(new UserPreference())
->setName('timesheet.export_decimal')
->setValue(false)
(new UserPreference('export_decimal', false))
->setOrder(900)
->setSection('behaviour')
->setType(CheckboxType::class),
->setType(YesNoType::class),
];
}
/**
* @param PrepareUserEvent $event
*/
public function loadUserPreferences(PrepareUserEvent $event)
public function loadUserPreferences(PrepareUserEvent $event): void
{
$user = $event->getUser();

View File

@@ -19,19 +19,8 @@ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInt
final class UserProfileSubscriber implements EventSubscriberInterface
{
/**
* @var EventDispatcherInterface
*/
private $eventDispatcher;
/**
* @var TokenStorageInterface
*/
private $storage;
public function __construct(EventDispatcherInterface $dispatcher, TokenStorageInterface $storage)
public function __construct(private EventDispatcherInterface $eventDispatcher, private TokenStorageInterface $storage)
{
$this->eventDispatcher = $dispatcher;
$this->storage = $storage;
}
public static function getSubscribedEvents(): array
@@ -44,7 +33,7 @@ final class UserProfileSubscriber implements EventSubscriberInterface
public function prepareUserProfile(KernelEvent $event): void
{
// ignore sub-requests
if (!$event->isMasterRequest()) {
if (!$event->isMainRequest()) {
return;
}

View File

@@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\EventSubscriber;
use App\Entity\User;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class WizardSubscriber implements EventSubscriberInterface
{
public function __construct(
private UrlGeneratorInterface $urlGenerator,
private AuthorizationCheckerInterface $security,
private TokenStorageInterface $storage
) {
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['onKernelRequest']
];
}
public function onKernelRequest(RequestEvent $event): void
{
// ignore sub-requests
if (!$event->isMainRequest()) {
return;
}
// ignore events like the toolbar where we do not have a token
if (null === ($token = $this->storage->getToken())) {
return;
}
$uri = $event->getRequest()->getRequestUri();
// never require 2FA on API calls
if (str_starts_with($uri, '/api/') || stripos($uri, '/register/') !== false) {
return;
}
$user = $token->getUser();
if ($user instanceof User) {
if (stripos($uri, '/wizard/') !== false) {
return;
}
if (!$this->security->isGranted('IS_AUTHENTICATED_FULLY')) {
return;
}
foreach (User::WIZARDS as $wizard) {
if (!$user->hasSeenWizard($wizard)) {
$response = new RedirectResponse($this->urlGenerator->generate('wizard', ['wizard' => $wizard]));
$event->setResponse($response);
return;
}
}
}
}
}