. */ declare(strict_types=1); namespace App\Controller; use App\DataTables\Column\LogEntryTargetColumn; use App\DataTables\Filters\LogFilter; use App\DataTables\LogDataTable; use App\Entity\Base\AbstractDBElement; use App\Entity\LogSystem\AbstractLogEntry; use App\Entity\LogSystem\CollectionElementDeleted; use App\Entity\LogSystem\ElementCreatedLogEntry; use App\Entity\LogSystem\ElementDeletedLogEntry; use App\Entity\LogSystem\ElementEditedLogEntry; use App\Form\Filters\LogFilterType; use App\Repository\DBElementRepository; use App\Services\LogSystem\EventUndoHelper; use App\Services\LogSystem\EventUndoMode; use App\Services\LogSystem\LogEntryExtraFormatter; use App\Services\LogSystem\LogLevelHelper; use App\Services\LogSystem\LogTargetHelper; use App\Services\LogSystem\TimeTravel; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use InvalidArgumentException; use Omines\DataTablesBundle\DataTableFactory; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; #[Route(path: '/log')] class LogController extends AbstractController { protected DBElementRepository $dbRepository; public function __construct(protected EntityManagerInterface $entityManager, protected TimeTravel $timeTravel) { $this->dbRepository = $entityManager->getRepository(AbstractDBElement::class); } #[Route(path: '/', name: 'log_view')] public function showLogs(Request $request, DataTableFactory $dataTable): Response { $this->denyAccessUnlessGranted('@system.show_logs'); $formRequest = clone $request; $formRequest->setMethod('GET'); $filter = new LogFilter(); $filterForm = $this->createForm(LogFilterType::class, $filter, ['method' => 'GET']); $filterForm->handleRequest($formRequest); $table = $dataTable->createFromType(LogDataTable::class, [ 'filter' => $filter, ]) ->handleRequest($request); if ($table->isCallback()) { return $table->getResponse(); } return $this->render('log_system/log_list.html.twig', [ 'datatable' => $table, 'filterForm' => $filterForm, ]); } #[Route(path: '/{id}/details', name: 'log_details')] public function logDetails(AbstractLogEntry $logEntry, LogEntryExtraFormatter $logEntryExtraFormatter, LogLevelHelper $logLevelHelper, LogTargetHelper $logTargetHelper, EntityManagerInterface $entityManager): Response { $this->denyAccessUnlessGranted('show_details', $logEntry); $extra_html = $logEntryExtraFormatter->format($logEntry); $target_html = $logTargetHelper->formatTarget($logEntry); $repo = $entityManager->getRepository(AbstractLogEntry::class); $target_element = $repo->getTargetElement($logEntry); return $this->render('log_system/details/log_details.html.twig', [ 'log_entry' => $logEntry, 'target_element' => $target_element, 'extra_html' => $extra_html, 'target_html' => $target_html, 'log_level_helper' => $logLevelHelper, ]); } #[Route(path: '/{id}/delete', name: 'log_delete', methods: ['DELETE'])] public function deleteLogEntry(Request $request, AbstractLogEntry $logEntry, EntityManagerInterface $entityManager): RedirectResponse { $this->denyAccessUnlessGranted('delete', $logEntry); if ($this->isCsrfTokenValid('delete'.$logEntry->getID(), $request->request->get('_token'))) { //Remove part $entityManager->remove($logEntry); //Flush changes $entityManager->flush(); $this->addFlash('success', 'log.delete.success'); } return $this->redirectToRoute('homepage'); } #[Route(path: '/undo', name: 'log_undo', methods: ['POST'])] public function undoRevertLog(Request $request, EventUndoHelper $eventUndoHelper): RedirectResponse { $mode = EventUndoMode::UNDO; $id = $request->request->getInt('undo'); //If no undo value was set check if a revert was set if (0 === $id) { $id = $request->request->getInt('revert'); $mode = EventUndoMode::REVERT; } if (0 === $id) { throw new InvalidArgumentException('No log entry ID was given!'); } $log_element = $this->entityManager->find(AbstractLogEntry::class, $id); if (!$log_element instanceof AbstractLogEntry) { throw new InvalidArgumentException('No log entry with the given ID is existing!'); } $this->denyAccessUnlessGranted('revert_element', $log_element->getTargetClass()); $eventUndoHelper->setMode($mode); $eventUndoHelper->setUndoneEvent($log_element); if (EventUndoMode::UNDO === $mode) { $this->undoLog($log_element); } elseif (EventUndoMode::REVERT === $mode) { $this->revertLog($log_element); } $eventUndoHelper->clearUndoneEvent(); $redirect = $request->request->get('redirect_back'); return $this->redirect($redirect); } protected function revertLog(AbstractLogEntry $logEntry): void { $timestamp = $logEntry->getTimestamp(); $element = $this->entityManager->find($logEntry->getTargetClass(), $logEntry->getTargetID()); //If the element is not available in DB try to undelete it if (null === $element) { $element = $this->timeTravel->undeleteEntity($logEntry->getTargetClass(), $logEntry->getTargetID()); $this->entityManager->persist($element); $this->entityManager->flush(); $this->dbRepository->changeID($element, $logEntry->getTargetID()); } if (!$element instanceof AbstractDBElement) { $this->addFlash('error', 'log.undo.target_not_found'); return; } $this->timeTravel->revertEntityToTimestamp($element, $timestamp); $this->entityManager->flush(); $this->addFlash('success', 'log.undo.revert_success'); } protected function undoLog(AbstractLogEntry $log_element): void { if ($log_element instanceof ElementDeletedLogEntry || $log_element instanceof CollectionElementDeleted) { if ($log_element instanceof ElementDeletedLogEntry) { $element_class = $log_element->getTargetClass(); $element_id = $log_element->getTargetID(); } else { $element_class = $log_element->getDeletedElementClass(); $element_id = $log_element->getDeletedElementID(); } //Check if the element we want to undelete already exits if (null === $this->entityManager->find($element_class, $element_id)) { $undeleted_element = $this->timeTravel->undeleteEntity($element_class, $element_id); $this->entityManager->persist($undeleted_element); $this->entityManager->flush(); $this->dbRepository->changeID($undeleted_element, $element_id); $this->addFlash('success', 'log.undo.element_undelete_success'); } else { $this->addFlash('warning', 'log.undo.element_element_already_undeleted'); } } elseif ($log_element instanceof ElementCreatedLogEntry) { $element = $this->entityManager->find($log_element->getTargetClass(), $log_element->getTargetID()); if (null !== $element) { $this->entityManager->remove($element); $this->entityManager->flush(); $this->addFlash('success', 'log.undo.element_delete_success'); } else { $this->addFlash('warning', 'log.undo.element.element_already_delted'); } } elseif ($log_element instanceof ElementEditedLogEntry) { $element = $this->entityManager->find($log_element->getTargetClass(), $log_element->getTargetID()); if ($element instanceof AbstractDBElement) { $this->timeTravel->applyEntry($element, $log_element); $this->entityManager->flush(); $this->addFlash('success', 'log.undo.element_change_undone'); } else { $this->addFlash('error', 'log.undo.do_undelete_before'); } } else { $this->addFlash('error', 'log.undo.log_type_invalid'); } } }