added plugin screen (#671)
This commit is contained in:
@@ -11,6 +11,10 @@ namespace App\Controller;
|
||||
|
||||
use App\Constants;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
@@ -40,6 +44,11 @@ class AboutController extends AbstractController
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function indexAction()
|
||||
{
|
||||
return $this->getAboutView();
|
||||
}
|
||||
|
||||
protected function getAboutView(array $additional = [])
|
||||
{
|
||||
$phpInfo = $this->getPhpInfo();
|
||||
unset($phpInfo[0]);
|
||||
@@ -71,7 +80,8 @@ class AboutController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('about/system.html.twig', [
|
||||
return $this->render('about/system.html.twig', array_merge(
|
||||
[
|
||||
'modules' => get_loaded_extensions(),
|
||||
'dotenv' => [
|
||||
'APP_ENV' => getenv('APP_ENV'),
|
||||
@@ -81,7 +91,9 @@ class AboutController extends AbstractController
|
||||
'info' => $phpInfo,
|
||||
'settings' => $settings,
|
||||
'license' => $this->getLicense(),
|
||||
]);
|
||||
],
|
||||
$additional
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,4 +153,26 @@ class AboutController extends AbstractController
|
||||
|
||||
return $phpinfo['phpinfo'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/flush-cache", name="system_flush_cache", methods={"GET"})
|
||||
*
|
||||
* @Security("is_granted('system_actions')")
|
||||
*/
|
||||
public function rebuildContainer(KernelInterface $kernel)
|
||||
{
|
||||
$application = new Application($kernel);
|
||||
$application->setAutoExit(false);
|
||||
|
||||
$input = new ArrayInput([
|
||||
'command' => 'cache:clear',
|
||||
'--env' => $kernel->getEnvironment(),
|
||||
'-n',
|
||||
]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$application->run($input, $output);
|
||||
|
||||
return $this->getAboutView(['content_action' => $output->fetch()]);
|
||||
}
|
||||
}
|
||||
|
||||
51
src/Controller/PluginController.php
Normal file
51
src/Controller/PluginController.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?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\Controller;
|
||||
|
||||
use App\Plugin\PluginManager;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route(path="/admin/plugins")
|
||||
* @Security("is_granted('plugins')")
|
||||
*/
|
||||
class PluginController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var PluginManager
|
||||
*/
|
||||
protected $plugins;
|
||||
|
||||
/**
|
||||
* @param PluginManager $manager
|
||||
*/
|
||||
public function __construct(PluginManager $manager)
|
||||
{
|
||||
$this->plugins = $manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="plugins", methods={"GET"})
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function indexAction()
|
||||
{
|
||||
$plugins = $this->plugins->getPlugins();
|
||||
foreach ($this->plugins->getPlugins() as $plugin) {
|
||||
$this->plugins->loadMetadata($plugin);
|
||||
}
|
||||
|
||||
return $this->render('plugin/index.html.twig', [
|
||||
'plugins' => $plugins,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -27,11 +27,10 @@ class DoctrineCompilerPass implements CompilerPassInterface
|
||||
];
|
||||
|
||||
/**
|
||||
* @param ContainerBuilder $container
|
||||
* @return array|false|null|string
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function findEngine(ContainerBuilder $container)
|
||||
protected function findEngine()
|
||||
{
|
||||
$engine = null;
|
||||
|
||||
@@ -47,7 +46,7 @@ class DoctrineCompilerPass implements CompilerPassInterface
|
||||
if (null === $engine) {
|
||||
throw new \Exception(
|
||||
'Could not detect database engine. Please set the environment config DATABASE_ENGINE ' .
|
||||
'to one of: "' . implode(', ', $this->allowedEngines) . '" in your .env file: DATABASE_ENGINE=sqlite'
|
||||
'to one of: "' . implode(', ', $this->allowedEngines) . '" in your .env file, e.g. DATABASE_ENGINE=sqlite'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,7 +67,7 @@ class DoctrineCompilerPass implements CompilerPassInterface
|
||||
*/
|
||||
protected function getConfigFile(ContainerBuilder $container)
|
||||
{
|
||||
$engine = $this->findEngine($container);
|
||||
$engine = $this->findEngine();
|
||||
|
||||
$configDir = realpath(
|
||||
$container->getParameter('kernel.project_dir') . '/vendor/beberlei/DoctrineExtensions/config/'
|
||||
|
||||
@@ -1,41 +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\DependencyInjection\Compiler;
|
||||
|
||||
use App\Export\ServiceExport;
|
||||
use App\Kernel;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
/**
|
||||
* Dynamically adds all dependencies to the ExportService.
|
||||
*/
|
||||
class ExportServiceCompilerPass implements CompilerPassInterface
|
||||
{
|
||||
/**
|
||||
* @param ContainerBuilder $container
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
// always first check if the primary service is defined
|
||||
if (!$container->has(ServiceExport::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$definition = $container->findDefinition(ServiceExport::class);
|
||||
|
||||
$taggedRenderer = $container->findTaggedServiceIds(Kernel::TAG_EXPORT_RENDERER);
|
||||
foreach ($taggedRenderer as $id => $tags) {
|
||||
$definition->addMethodCall('addRenderer', [new Reference($id)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,10 +45,11 @@ abstract class AbstractMigration extends BaseAbstractMigration implements Contai
|
||||
/**
|
||||
* @param string $name
|
||||
* @return string
|
||||
* @deprecated since 0.9
|
||||
*/
|
||||
protected function getTableName($name)
|
||||
{
|
||||
return TablePrefixSubscriber::PREFIX . $name;
|
||||
return 'kimai2_' . $name;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,56 +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\Doctrine;
|
||||
|
||||
use Doctrine\Common\EventSubscriber;
|
||||
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
|
||||
/**
|
||||
* Adds a prefix to every doctrine entity AKA database table
|
||||
*/
|
||||
class TablePrefixSubscriber implements EventSubscriber
|
||||
{
|
||||
public const PREFIX = 'kimai2_';
|
||||
|
||||
/**
|
||||
* @return array|string[]
|
||||
*/
|
||||
public function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
Events::loadClassMetadata,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LoadClassMetadataEventArgs $args
|
||||
*/
|
||||
public function loadClassMetadata(LoadClassMetadataEventArgs $args)
|
||||
{
|
||||
$classMetadata = $args->getClassMetadata();
|
||||
if ($classMetadata->isInheritanceTypeSingleTable() && !$classMetadata->isRootEntity()) {
|
||||
// if we are in an inheritance hierarchy, only apply this once
|
||||
return;
|
||||
}
|
||||
|
||||
$classMetadata->setPrimaryTable(['name' => self::PREFIX . $classMetadata->getTableName()]);
|
||||
|
||||
foreach ($classMetadata->getAssociationMappings() as $fieldName => $mapping) {
|
||||
if (\Doctrine\ORM\Mapping\ClassMetadataInfo::MANY_TO_MANY == $mapping['type']
|
||||
// Check if "joinTable" exists:
|
||||
// it can be null if this field is the reverse side of a ManyToMany relationship
|
||||
&& array_key_exists('name', $classMetadata->associationMappings[$fieldName]['joinTable'])) {
|
||||
$mappedTableName = $classMetadata->associationMappings[$fieldName]['joinTable']['name'];
|
||||
$classMetadata->associationMappings[$fieldName]['joinTable']['name'] = self::PREFIX . $mappedTableName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="activities")
|
||||
* @ORM\Table(name="kimai2_activities")
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ActivityRepository")
|
||||
*/
|
||||
class Activity
|
||||
|
||||
@@ -15,8 +15,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ConfigurationRepository")
|
||||
* @ORM\Table(
|
||||
* name="configuration",
|
||||
* @ORM\Table(name="kimai2_configuration",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"name"})
|
||||
* }
|
||||
|
||||
@@ -13,7 +13,7 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="customers")
|
||||
* @ORM\Table(name="kimai2_customers")
|
||||
* @ORM\Entity(repositoryClass="App\Repository\CustomerRepository")
|
||||
*/
|
||||
class Customer
|
||||
|
||||
@@ -13,11 +13,8 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* InvoiceTemplate
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="App\Repository\InvoiceTemplateRepository")
|
||||
* @ORM\Table(
|
||||
* name="invoice_templates",
|
||||
* @ORM\Table(name="kimai2_invoice_templates",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"name"})
|
||||
* }
|
||||
|
||||
@@ -13,7 +13,7 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="projects")
|
||||
* @ORM\Table(name="kimai2_projects")
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ProjectRepository")
|
||||
*/
|
||||
class Project
|
||||
|
||||
@@ -13,8 +13,7 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(
|
||||
* name="timesheet",
|
||||
* @ORM\Table(name="kimai2_timesheet",
|
||||
* indexes={
|
||||
* @ORM\Index(columns={"user"}),
|
||||
* @ORM\Index(columns={"activity_id"})
|
||||
|
||||
@@ -18,11 +18,8 @@ use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* Application main User entity.
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
|
||||
* @ORM\Table(
|
||||
* name="users",
|
||||
* @ORM\Table(name="kimai2_users",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"username"}),
|
||||
* @ORM\UniqueConstraint(columns={"email"})
|
||||
|
||||
@@ -16,11 +16,8 @@ use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* UserPreference
|
||||
*
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(
|
||||
* name="user_preferences",
|
||||
* @ORM\Table(name="kimai2_user_preferences",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"user_id", "name"})
|
||||
* }
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Event;
|
||||
|
||||
use KevinPapst\AdminLTEBundle\Event\SidebarMenuEvent;
|
||||
use KevinPapst\AdminLTEBundle\Model\MenuItemModel;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
@@ -28,17 +29,27 @@ class ConfigureMainMenuEvent extends Event
|
||||
* @var SidebarMenuEvent
|
||||
*/
|
||||
private $event;
|
||||
/**
|
||||
* @var MenuItemModel
|
||||
*/
|
||||
private $admin;
|
||||
/**
|
||||
* @var MenuItemModel
|
||||
*/
|
||||
private $system;
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param SidebarMenuEvent $event
|
||||
* @param MenuItemModel $admin
|
||||
* @param MenuItemModel $system
|
||||
*/
|
||||
public function __construct(
|
||||
Request $request,
|
||||
SidebarMenuEvent $event
|
||||
) {
|
||||
public function __construct(Request $request, SidebarMenuEvent $event, MenuItemModel $admin, MenuItemModel $system)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->event = $event;
|
||||
$this->admin = $admin;
|
||||
$this->system = $system;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,4 +67,20 @@ class ConfigureMainMenuEvent extends Event
|
||||
{
|
||||
return $this->event;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MenuItemModel
|
||||
*/
|
||||
public function getAdminMenu()
|
||||
{
|
||||
return $this->admin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MenuItemModel
|
||||
*/
|
||||
public function getSystemMenu()
|
||||
{
|
||||
return $this->system;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,10 @@ class ThemeEvent extends Event
|
||||
public const JAVASCRIPT = 'app.theme.javascript';
|
||||
public const STYLESHEET = 'app.theme.css';
|
||||
public const HTML_HEAD = 'app.theme.html_head';
|
||||
public const CONTENT_BEFORE = 'app.theme.content_before';
|
||||
public const CONTENT_START = 'app.theme.content_start';
|
||||
public const CONTENT_END = 'app.theme.content_end';
|
||||
public const CONTENT_AFTER = 'app.theme.content_after';
|
||||
|
||||
/**
|
||||
* @var User
|
||||
|
||||
@@ -66,26 +66,36 @@ class MenuBuilderSubscriber implements EventSubscriberInterface
|
||||
new MenuItemModel('dashboard', 'menu.homepage', 'dashboard', [], 'fas fa-tachometer-alt')
|
||||
);
|
||||
|
||||
$this->eventDispatcher->dispatch(
|
||||
ConfigureMainMenuEvent::CONFIGURE,
|
||||
new ConfigureMainMenuEvent(
|
||||
$request,
|
||||
$event
|
||||
)
|
||||
$menuEvent = new ConfigureMainMenuEvent(
|
||||
$request,
|
||||
$event,
|
||||
new MenuItemModel('admin', 'menu.admin', ''),
|
||||
new MenuItemModel('system', 'menu.system', '')
|
||||
);
|
||||
|
||||
$admin = new MenuItemModel('admin', 'menu.admin', '', [], 'fas fa-wrench');
|
||||
$this->eventDispatcher->dispatch(ConfigureMainMenuEvent::CONFIGURE, $menuEvent);
|
||||
|
||||
// @deprecated since 0.9, will be removed with 1.0
|
||||
$this->eventDispatcher->dispatch(
|
||||
ConfigureAdminMenuEvent::CONFIGURE,
|
||||
new ConfigureAdminMenuEvent(
|
||||
$request,
|
||||
$admin
|
||||
$menuEvent->getAdminMenu()
|
||||
)
|
||||
);
|
||||
|
||||
if ($admin->hasChildren()) {
|
||||
$event->addItem($admin);
|
||||
if ($menuEvent->getAdminMenu()->hasChildren()) {
|
||||
$event->addItem(new MenuItemModel('admin', 'menu.admin', ''));
|
||||
foreach ($menuEvent->getAdminMenu()->getChildren() as $child) {
|
||||
$event->addItem($child);
|
||||
}
|
||||
}
|
||||
|
||||
if ($menuEvent->getSystemMenu()->hasChildren()) {
|
||||
$event->addItem(new MenuItemModel('system', 'menu.system', ''));
|
||||
foreach ($menuEvent->getSystemMenu()->getChildren() as $child) {
|
||||
$event->addItem($child);
|
||||
}
|
||||
}
|
||||
|
||||
$this->activateByRoute(
|
||||
|
||||
@@ -9,15 +9,14 @@
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Event\ConfigureAdminMenuEvent;
|
||||
use App\Event\ConfigureMainMenuEvent;
|
||||
use KevinPapst\AdminLTEBundle\Event\SidebarMenuEvent;
|
||||
use KevinPapst\AdminLTEBundle\Model\MenuItemModel;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
/**
|
||||
* Menu event subscriber for timesheet, customer, projects, activities.
|
||||
* This is a sample implementation for developer who want to add new navigation entries in their bundles.
|
||||
* Menu event subscriber is creating the Kimai default menu structure.
|
||||
*/
|
||||
class MenuSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
@@ -42,7 +41,6 @@ class MenuSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
return [
|
||||
ConfigureMainMenuEvent::CONFIGURE => ['onMainMenuConfigure', 100],
|
||||
ConfigureAdminMenuEvent::CONFIGURE => ['onAdminMenuConfigure', 100],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -57,7 +55,17 @@ class MenuSubscriber implements EventSubscriberInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$menu = $event->getMenu();
|
||||
$this->configureMainMenu($event->getMenu());
|
||||
$this->configureAdminMenu($event->getAdminMenu());
|
||||
$this->configureSystemMenu($event->getSystemMenu());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SidebarMenuEvent $menu
|
||||
*/
|
||||
protected function configureMainMenu(SidebarMenuEvent $menu)
|
||||
{
|
||||
$auth = $this->security;
|
||||
|
||||
if ($auth->isGranted('view_own_timesheet')) {
|
||||
$menu->addItem(
|
||||
@@ -67,7 +75,7 @@ class MenuSubscriber implements EventSubscriberInterface
|
||||
|
||||
if ($auth->isGranted('view_invoice')) {
|
||||
$menu->addItem(
|
||||
new MenuItemModel('invoice', 'menu.invoice', 'invoice', [], 'fas fa-file-invoice')
|
||||
new MenuItemModel('invoice', 'menu.invoice', 'invoice', [], 'far fa-file-alt')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,33 +87,21 @@ class MenuSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \App\Event\ConfigureAdminMenuEvent $event
|
||||
* @param MenuItemModel $menu
|
||||
*/
|
||||
public function onAdminMenuConfigure(ConfigureAdminMenuEvent $event)
|
||||
protected function configureAdminMenu(MenuItemModel $menu)
|
||||
{
|
||||
$auth = $this->security;
|
||||
|
||||
if (!$auth->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$menu = $event->getAdminMenu();
|
||||
|
||||
if ($auth->isGranted('view_other_timesheet')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('timesheet_admin', 'menu.admin_timesheet', 'admin_timesheet', [], 'far fa-clock')
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_user')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('user_admin', 'menu.admin_user', 'admin_user', [], 'fas fa-user')
|
||||
new MenuItemModel('timesheet_admin', 'menu.admin_timesheet', 'admin_timesheet', [], 'fas fa-user-clock')
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_customer')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('customer_admin', 'menu.admin_customer', 'admin_customer', [], 'fas fa-users')
|
||||
new MenuItemModel('customer_admin', 'menu.admin_customer', 'admin_customer', [], 'fas fa-user-tie')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -120,6 +116,26 @@ class MenuSubscriber implements EventSubscriberInterface
|
||||
new MenuItemModel('activity_admin', 'menu.admin_activity', 'admin_activity', [], 'fas fa-tasks')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MenuItemModel $menu
|
||||
*/
|
||||
protected function configureSystemMenu(MenuItemModel $menu)
|
||||
{
|
||||
$auth = $this->security;
|
||||
|
||||
if ($auth->isGranted('view_user')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('user_admin', 'menu.admin_user', 'admin_user', [], 'fas fa-users')
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('plugins')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('plugins', 'menu.plugin', 'plugins', [], 'fas fa-plug')
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('system_configuration')) {
|
||||
$menu->addChild(
|
||||
|
||||
@@ -13,20 +13,21 @@ use App\Entity\Timesheet;
|
||||
use App\Export\RendererInterface;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Twig\Environment;
|
||||
|
||||
class HtmlRenderer implements RendererInterface
|
||||
{
|
||||
use RendererTrait;
|
||||
|
||||
/**
|
||||
* @var \Twig_Environment
|
||||
* @var Environment
|
||||
*/
|
||||
protected $twig;
|
||||
|
||||
/**
|
||||
* @param \Twig_Environment $twig
|
||||
* @param Environment $twig
|
||||
*/
|
||||
public function __construct(\Twig_Environment $twig)
|
||||
public function __construct(Environment $twig)
|
||||
{
|
||||
$this->twig = $twig;
|
||||
}
|
||||
@@ -35,9 +36,9 @@ class HtmlRenderer implements RendererInterface
|
||||
* @param Timesheet[] $timesheets
|
||||
* @param TimesheetQuery $query
|
||||
* @return Response
|
||||
* @throws \Twig_Error_Loader
|
||||
* @throws \Twig_Error_Runtime
|
||||
* @throws \Twig_Error_Syntax
|
||||
* @throws \Twig\Error\LoaderError
|
||||
* @throws \Twig\Error\RuntimeError
|
||||
* @throws \Twig\Error\SyntaxError
|
||||
*/
|
||||
public function render(array $timesheets, TimesheetQuery $query): Response
|
||||
{
|
||||
|
||||
@@ -16,13 +16,14 @@ use App\Timesheet\UserDateTimeFactory;
|
||||
use App\Utils\HtmlToPdfConverter;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Twig\Environment;
|
||||
|
||||
class PDFRenderer implements RendererInterface
|
||||
{
|
||||
use RendererTrait;
|
||||
|
||||
/**
|
||||
* @var \Twig_Environment
|
||||
* @var Environment
|
||||
*/
|
||||
protected $twig;
|
||||
/**
|
||||
@@ -35,10 +36,11 @@ class PDFRenderer implements RendererInterface
|
||||
protected $converter;
|
||||
|
||||
/**
|
||||
* @param \Twig_Environment $twig
|
||||
* @param Environment $twig
|
||||
* @param UserDateTimeFactory $dateTime
|
||||
* @param HtmlToPdfConverter $converter
|
||||
*/
|
||||
public function __construct(\Twig_Environment $twig, UserDateTimeFactory $dateTime, HtmlToPdfConverter $converter)
|
||||
public function __construct(Environment $twig, UserDateTimeFactory $dateTime, HtmlToPdfConverter $converter)
|
||||
{
|
||||
$this->twig = $twig;
|
||||
$this->dateTime = $dateTime;
|
||||
@@ -49,10 +51,9 @@ class PDFRenderer implements RendererInterface
|
||||
* @param Timesheet[] $timesheets
|
||||
* @param TimesheetQuery $query
|
||||
* @return Response
|
||||
* @throws \Mpdf\MpdfException
|
||||
* @throws \Twig_Error_Loader
|
||||
* @throws \Twig_Error_Runtime
|
||||
* @throws \Twig_Error_Syntax
|
||||
* @throws \Twig\Error\LoaderError
|
||||
* @throws \Twig\Error\RuntimeError
|
||||
* @throws \Twig\Error\SyntaxError
|
||||
*/
|
||||
public function render(array $timesheets, TimesheetQuery $query): Response
|
||||
{
|
||||
|
||||
@@ -16,6 +16,16 @@ class ServiceExport
|
||||
*/
|
||||
protected $renderer = [];
|
||||
|
||||
/**
|
||||
* @param RendererInterface[] $renderer
|
||||
*/
|
||||
public function __construct(iterable $renderer)
|
||||
{
|
||||
foreach ($renderer as $render) {
|
||||
$this->addRenderer($render);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RendererInterface $renderer
|
||||
* @return $this
|
||||
|
||||
@@ -13,18 +13,19 @@ use App\Entity\InvoiceDocument;
|
||||
use App\Invoice\RendererInterface;
|
||||
use App\Model\InvoiceModel;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Twig\Environment;
|
||||
|
||||
class TwigRenderer implements RendererInterface
|
||||
{
|
||||
/**
|
||||
* @var \Twig_Environment
|
||||
* @var Environment
|
||||
*/
|
||||
protected $twig;
|
||||
|
||||
/**
|
||||
* @param \Twig_Environment $twig
|
||||
* @param Environment $twig
|
||||
*/
|
||||
public function __construct(\Twig_Environment $twig)
|
||||
public function __construct(Environment $twig)
|
||||
{
|
||||
$this->twig = $twig;
|
||||
}
|
||||
|
||||
@@ -11,13 +11,13 @@ namespace App;
|
||||
|
||||
use App\DependencyInjection\AppExtension;
|
||||
use App\DependencyInjection\Compiler\DoctrineCompilerPass;
|
||||
use App\DependencyInjection\Compiler\ExportServiceCompilerPass;
|
||||
use App\DependencyInjection\Compiler\InvoiceServiceCompilerPass;
|
||||
use App\DependencyInjection\Compiler\TwigContextCompilerPass;
|
||||
use App\Export\RendererInterface as ExportRendererInterface;
|
||||
use App\Invoice\CalculatorInterface as InvoiceCalculator;
|
||||
use App\Invoice\NumberGeneratorInterface;
|
||||
use App\Invoice\RendererInterface as InvoiceRendererInterface;
|
||||
use App\Plugin\PluginInterface;
|
||||
use App\Timesheet\CalculatorInterface as TimesheetCalculator;
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Component\Config\Loader\LoaderInterface;
|
||||
@@ -34,6 +34,7 @@ class Kernel extends BaseKernel
|
||||
|
||||
public const CONFIG_EXTS = '.{php,xml,yaml,yml}';
|
||||
|
||||
public const TAG_PLUGIN = 'kimai.plugin';
|
||||
public const TAG_EXPORT_RENDERER = 'export.renderer';
|
||||
public const TAG_INVOICE_RENDERER = 'invoice.renderer';
|
||||
public const TAG_INVOICE_NUMBER_GENERATOR = 'invoice.number_generator';
|
||||
@@ -56,6 +57,7 @@ class Kernel extends BaseKernel
|
||||
$container->registerForAutoconfiguration(InvoiceRendererInterface::class)->addTag(self::TAG_INVOICE_RENDERER);
|
||||
$container->registerForAutoconfiguration(NumberGeneratorInterface::class)->addTag(self::TAG_INVOICE_NUMBER_GENERATOR);
|
||||
$container->registerForAutoconfiguration(InvoiceCalculator::class)->addTag(self::TAG_INVOICE_CALCULATOR);
|
||||
$container->registerForAutoconfiguration(PluginInterface::class)->addTag(self::TAG_PLUGIN);
|
||||
}
|
||||
|
||||
public function registerBundles()
|
||||
@@ -67,6 +69,12 @@ class Kernel extends BaseKernel
|
||||
}
|
||||
}
|
||||
|
||||
// do not load Kimai plugin in test environment, they may alter the default behaviour and create
|
||||
// false-negatives in integration/system tests
|
||||
if ('test' === $this->environment) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pluginsDir = $this->getProjectDir() . '/var/plugins';
|
||||
if (!file_exists($pluginsDir)) {
|
||||
return;
|
||||
@@ -110,7 +118,6 @@ class Kernel extends BaseKernel
|
||||
$container->addCompilerPass(new DoctrineCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
||||
$container->addCompilerPass(new TwigContextCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
||||
$container->addCompilerPass(new InvoiceServiceCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
||||
$container->addCompilerPass(new ExportServiceCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
||||
}
|
||||
|
||||
protected function configureRoutes(RouteCollectionBuilder $routes)
|
||||
|
||||
83
src/Plugin/Plugin.php
Normal file
83
src/Plugin/Plugin.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?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\Plugin;
|
||||
|
||||
class Plugin
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $path;
|
||||
/**
|
||||
* @var PluginMetadata
|
||||
*/
|
||||
private $metadata;
|
||||
|
||||
/**
|
||||
* @return PluginMetadata
|
||||
*/
|
||||
public function getMetadata(): ?PluginMetadata
|
||||
{
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PluginMetadata $metadata
|
||||
* @return Plugin
|
||||
*/
|
||||
public function setMetadata(PluginMetadata $metadata)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPath(): ?string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return Plugin
|
||||
*/
|
||||
public function setPath(string $path)
|
||||
{
|
||||
$this->path = $path;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return Plugin
|
||||
*/
|
||||
public function setName(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
23
src/Plugin/PluginInterface.php
Normal file
23
src/Plugin/PluginInterface.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?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\Plugin;
|
||||
|
||||
interface PluginInterface
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName();
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPath();
|
||||
}
|
||||
111
src/Plugin/PluginManager.php
Normal file
111
src/Plugin/PluginManager.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?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\Plugin;
|
||||
|
||||
use App\Constants;
|
||||
|
||||
class PluginManager
|
||||
{
|
||||
/**
|
||||
* @var Plugin[]
|
||||
*/
|
||||
private $plugins = [];
|
||||
|
||||
/**
|
||||
* @param PluginInterface[] $plugins
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct(iterable $plugins)
|
||||
{
|
||||
foreach ($plugins as $plugin) {
|
||||
$this->addPlugin($plugin);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PluginInterface $plugin
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function addPlugin(PluginInterface $plugin)
|
||||
{
|
||||
if (isset($this->plugins[$plugin->getName()])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->plugins[$plugin->getName()] = $this->createPlugin($plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Plugin[]
|
||||
*/
|
||||
public function getPlugins()
|
||||
{
|
||||
return $this->plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return Plugin|null
|
||||
*/
|
||||
public function getPlugin(string $name): ?Plugin
|
||||
{
|
||||
if (!isset($this->plugins[$name])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->plugins[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PluginInterface $bundle
|
||||
* @return Plugin
|
||||
*/
|
||||
protected function createPlugin(PluginInterface $bundle)
|
||||
{
|
||||
$plugin = new Plugin();
|
||||
$plugin
|
||||
->setName($bundle->getName())
|
||||
->setPath($bundle->getPath())
|
||||
->setMetadata(new PluginMetadata())
|
||||
;
|
||||
|
||||
return $plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this method and pass a plugin, to set its metdata.
|
||||
* This is not pre-filled by default, as it would mean to parse several composer.json on each request.
|
||||
*
|
||||
* @param Plugin $plugin
|
||||
*/
|
||||
public function loadMetadata(Plugin $plugin)
|
||||
{
|
||||
$composer = $plugin->getPath() . '/composer.json';
|
||||
if (!file_exists($composer) || !is_readable($composer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$json = json_decode(file_get_contents($composer), true);
|
||||
|
||||
$reqVersion = $json['extra']['kimai']['require'] ?? 'unknown';
|
||||
$version = $json['extra']['kimai']['version'] ?? 'unknown';
|
||||
$description = $json['description'] ?? '';
|
||||
|
||||
$homepage = $json['homepage'] ?? Constants::HOMEPAGE . '/store/';
|
||||
|
||||
$plugin
|
||||
->getMetadata()
|
||||
->setHomepage($homepage)
|
||||
->setKimaiVersion($reqVersion)
|
||||
->setVersion($version)
|
||||
->setDescription($description)
|
||||
;
|
||||
}
|
||||
}
|
||||
106
src/Plugin/PluginMetadata.php
Normal file
106
src/Plugin/PluginMetadata.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?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\Plugin;
|
||||
|
||||
class PluginMetadata
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $version;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $kimaiVersion;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $homepage;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $description;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $description
|
||||
* @return PluginMetadata
|
||||
*/
|
||||
public function setDescription(string $description)
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getVersion(): ?string
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $version
|
||||
* @return PluginMetadata
|
||||
*/
|
||||
public function setVersion(string $version)
|
||||
{
|
||||
$this->version = $version;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getKimaiVersion(): ?string
|
||||
{
|
||||
return $this->kimaiVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $kimaiVersion
|
||||
* @return PluginMetadata
|
||||
*/
|
||||
public function setKimaiVersion(string $kimaiVersion)
|
||||
{
|
||||
$this->kimaiVersion = $kimaiVersion;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getHomepage(): ?string
|
||||
{
|
||||
return $this->homepage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $homepage
|
||||
* @return PluginMetadata
|
||||
*/
|
||||
public function setHomepage(string $homepage)
|
||||
{
|
||||
$this->homepage = $homepage;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,8 @@ class Extensions extends AbstractExtension
|
||||
'on' => 'fas fa-toggle-on',
|
||||
'off' => 'fas fa-toggle-off',
|
||||
'audit' => 'fas fa-history',
|
||||
'home' => 'fas fa-home',
|
||||
'shop' => 'fas fa-shopping-cart',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -77,7 +77,7 @@ class TimesheetVoter extends AbstractVoter
|
||||
|
||||
switch ($attribute) {
|
||||
case self::START:
|
||||
if (!$this->canStart($subject, $user, $token)) {
|
||||
if (!$this->canStart($subject)) {
|
||||
return false;
|
||||
}
|
||||
$permission .= $attribute;
|
||||
@@ -113,11 +113,9 @@ class TimesheetVoter extends AbstractVoter
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canStart(Timesheet $timesheet, User $user, TokenInterface $token)
|
||||
protected function canStart(Timesheet $timesheet)
|
||||
{
|
||||
// possible improvements for the future:
|
||||
// we could check the amount of active entries (maybe slow)
|
||||
|
||||
@@ -74,7 +74,7 @@ class UserVoter extends AbstractVoter
|
||||
switch ($attribute) {
|
||||
// special case for the UserController
|
||||
case self::DELETE:
|
||||
if (!$this->canDelete($subject, $user, $token)) {
|
||||
if (!$this->canDelete($subject, $user)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ class UserVoter extends AbstractVoter
|
||||
* @param User $user
|
||||
* @return bool
|
||||
*/
|
||||
protected function canDelete(User $profile, User $user, TokenInterface $token)
|
||||
protected function canDelete(User $profile, User $user)
|
||||
{
|
||||
return $profile->getId() !== $user->getId();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user