filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center']; } /** * @\Symfony\Component\Routing\Annotation\Route(path="/{_locale}/exports/download/{alias}", name="chill_main_export_download", methods={"GET"}) */ public function downloadResultAction(Request $request, mixed $alias) { /** @var ExportManager $exportManager */ $exportManager = $this->exportManager; $export = $exportManager->getExport($alias); $key = $request->query->get('key', null); $savedExport = $this->getSavedExportFromRequest($request); [$dataCenters, $dataExport, $dataFormatter] = $this->rebuildData($key, $savedExport); $formatterAlias = $exportManager->getFormatterAlias($dataExport['export']); if (null !== $formatterAlias) { $formater = $exportManager->getFormatter($formatterAlias); } else { $formater = null; } $viewVariables = [ 'alias' => $alias, 'export' => $export, 'export_group' => $this->getExportGroup($export), 'saved_export' => $savedExport, ]; if ($formater instanceof \Chill\MainBundle\Export\Formatter\CSVListFormatter) { // due to a bug in php, we add the mime type in the download view $viewVariables['mime_type'] = 'text/csv'; } return $this->render('@ChillMain/Export/download.html.twig', $viewVariables); } /** * Generate a report. * * This action must work with GET queries. * * @param string $alias * * @return Response * * @\Symfony\Component\Routing\Annotation\Route(path="/{_locale}/exports/generate/{alias}", name="chill_main_export_generate", methods={"GET"}) */ public function generateAction(Request $request, $alias) { /** @var ExportManager $exportManager */ $exportManager = $this->exportManager; $key = $request->query->get('key', null); $savedExport = $this->getSavedExportFromRequest($request); [$dataCenters, $dataExport, $dataFormatter] = $this->rebuildData($key, $savedExport); return $exportManager->generate( $alias, $dataCenters['centers'], $dataExport['export'], null !== $dataFormatter ? $dataFormatter['formatter'] : [] ); } /** * @Route("/{_locale}/exports/generate-from-saved/{id}", name="chill_main_export_generate_from_saved") * * @throws \RedisException */ public function generateFromSavedExport(SavedExport $savedExport): RedirectResponse { $this->denyAccessUnlessGranted(SavedExportVoter::GENERATE, $savedExport); $key = md5(uniqid((string) random_int(0, mt_getrandmax()), false)); $this->redis->setEx($key, 3600, \serialize($savedExport->getOptions())); return $this->redirectToRoute( 'chill_main_export_download', [ 'alias' => $savedExport->getExportAlias(), 'key' => $key, 'prevent_save' => true, 'returnPath' => $this->generateUrl('chill_main_export_saved_list_my'), ] ); } /** * Render the list of available exports. * * @\Symfony\Component\Routing\Annotation\Route(path="/{_locale}/exports/", name="chill_main_export_index") */ public function indexAction(): Response { $exportManager = $this->exportManager; $exports = $exportManager->getExportsGrouped(true); return $this->render('@ChillMain/Export/layout.html.twig', [ 'grouped_exports' => $exports, ]); } /** * handle the step to build a query for an export. * * This action has three steps : * * 1.'export', the export form. When the form is posted, the data is stored * in the session (if valid), and then a redirection is done to next step. * 2. 'formatter', the formatter form. When the form is posted, the data is * stored in the session (if valid), and then a redirection is done to next step. * 3. 'generate': gather data from session from the previous steps, and * make a redirection to the "generate" action with data in query (HTTP GET) * * @\Symfony\Component\Routing\Annotation\Route(path="/{_locale}/exports/new/{alias}", name="chill_main_export_new") */ public function newAction(Request $request, string $alias): Response { // first check for ACL $exportManager = $this->exportManager; $export = $exportManager->getExport($alias); if (false === $exportManager->isGrantedForElement($export)) { throw $this->createAccessDeniedException('The user does not have access to this export'); } $savedExport = $this->getSavedExportFromRequest($request); $step = $request->query->getAlpha('step', 'centers'); return match ($step) { 'centers' => $this->selectCentersStep($request, $export, $alias, $savedExport), 'export' => $this->exportFormStep($request, $export, $alias, $savedExport), 'formatter' => $this->formatterFormStep($request, $export, $alias, $savedExport), 'generate' => $this->forwardToGenerate($request, $export, $alias, $savedExport), default => throw $this->createNotFoundException("The given step '{$step}' is invalid"), }; } /** * @Route("/{_locale}/export/saved/update-from-key/{id}/{key}", name="chill_main_export_saved_edit_options_from_key") */ public function editSavedExportOptionsFromKey(SavedExport $savedExport, string $key): Response { $this->denyAccessUnlessGranted('ROLE_USER'); $user = $this->getUser(); if (!$user instanceof User) { throw new AccessDeniedHttpException(); } $data = $this->rebuildRawData($key); $savedExport ->setOptions($data); $this->entityManager->flush(); return $this->redirectToRoute('chill_main_export_saved_edit', ['id' => $savedExport->getId()]); } /** * @Route("/{_locale}/export/save-from-key/{alias}/{key}", name="chill_main_export_save_from_key") */ public function saveFromKey(string $alias, string $key, Request $request): Response { $this->denyAccessUnlessGranted('ROLE_USER'); $user = $this->getUser(); if (!$user instanceof User) { throw new AccessDeniedHttpException(); } $data = $this->rebuildRawData($key); $savedExport = new SavedExport(); $savedExport ->setOptions($data) ->setExportAlias($alias) ->setUser($user); $form = $this->createForm(SavedExportType::class, $savedExport); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $this->entityManager->persist($savedExport); $this->entityManager->flush(); return $this->redirectToRoute('chill_main_export_index'); } return $this->render( '@ChillMain/SavedExport/new.html.twig', [ 'form' => $form->createView(), 'saved_export' => $savedExport, ] ); } /** * create a form to show on different steps. * * @param array $data the data from previous step. Required for steps 'formatter' and 'generate_formatter' */ protected function createCreateFormExport(string $alias, string $step, array $data, ?SavedExport $savedExport): FormInterface { /** @var ExportManager $exportManager */ $exportManager = $this->exportManager; $isGenerate = str_starts_with($step, 'generate_'); $options = match ($step) { 'export', 'generate_export' => [ 'export_alias' => $alias, 'picked_centers' => $exportManager->getPickedCenters($data['centers'] ?? []), ], 'formatter', 'generate_formatter' => [ 'export_alias' => $alias, 'formatter_alias' => $exportManager->getFormatterAlias($data['export']), 'aggregator_aliases' => $exportManager->getUsedAggregatorsAliases($data['export']), ], default => [ 'export_alias' => $alias, ], }; $defaultFormData = match ($savedExport) { null => $this->exportFormHelper->getDefaultData($step, $exportManager->getExport($alias), $options), default => $this->exportFormHelper->savedExportDataToFormData($savedExport, $step, $options), }; $builder = $this->formFactory ->createNamedBuilder( null, FormType::class, $defaultFormData, [ 'method' => $isGenerate ? Request::METHOD_GET : Request::METHOD_POST, 'csrf_protection' => !$isGenerate, ] ); if ('centers' === $step || 'generate_centers' === $step) { $builder->add('centers', PickCenterType::class, $options); } if ('export' === $step || 'generate_export' === $step) { $builder->add('export', ExportType::class, $options); } if ('formatter' === $step || 'generate_formatter' === $step) { $builder->add('formatter', FormatterType::class, $options); } $builder->add('submit', SubmitType::class, [ 'label' => 'Generate', ]); return $builder->getForm(); } /** * Render the export form. * * When the method is POST, the form is stored if valid, and a redirection * is done to next step. */ private function exportFormStep(Request $request, DirectExportInterface|ExportInterface $export, string $alias, ?SavedExport $savedExport = null): Response { $exportManager = $this->exportManager; // check we have data from the previous step (export step) $data = $this->session->get('centers_step', []); if (null === $data && true === $this->filterStatsByCenters) { return $this->redirectToRoute('chill_main_export_new', [ 'step' => $this->getNextStep('export', $export, true), 'alias' => $alias, ]); } $export = $exportManager->getExport($alias); $form = $this->createCreateFormExport($alias, 'export', $data, $savedExport); if (Request::METHOD_POST === $request->getMethod()) { $form->handleRequest($request); if ($form->isValid()) { $this->logger->debug('form export is valid', [ 'location' => __METHOD__, ]); // store data for reusing in next steps $data = $form->getData(); $this->session->set( 'export_step_raw', $request->request->all() ); $this->session->set('export_step', $data); // redirect to next step return $this->redirectToRoute('chill_main_export_new', [ 'step' => $this->getNextStep('export', $export), 'alias' => $alias, 'from_saved' => $request->get('from_saved', ''), ]); } $this->logger->debug('form export is invalid', [ 'location' => __METHOD__, ]); } return $this->render('@ChillMain/Export/new.html.twig', [ 'form' => $form->createView(), 'export_alias' => $alias, 'export' => $export, 'export_group' => $this->getExportGroup($export), ]); } /** * Render the form for formatter. * * If the form is posted and valid, store the data in session and * redirect to the next step. */ private function formatterFormStep(Request $request, DirectExportInterface|ExportInterface $export, string $alias, ?SavedExport $savedExport = null): Response { // check we have data from the previous step (export step) $data = $this->session->get('export_step', null); if (null === $data) { return $this->redirectToRoute('chill_main_export_new', [ 'step' => $this->getNextStep('formatter', $export, true), 'alias' => $alias, ]); } $form = $this->createCreateFormExport($alias, 'formatter', $data, $savedExport); if (Request::METHOD_POST === $request->getMethod()) { $form->handleRequest($request); if ($form->isValid()) { $dataFormatter = $form->getData(); $this->session->set('formatter_step', $dataFormatter); $this->session->set( 'formatter_step_raw', $request->request->all() ); // redirect to next step return $this->redirectToRoute('chill_main_export_new', [ 'alias' => $alias, 'step' => $this->getNextStep('formatter', $export), 'from_saved' => $request->get('from_saved', ''), ]); } } return $this->render( '@ChillMain/Export/new_formatter_step.html.twig', [ 'form' => $form->createView(), 'export' => $export, 'export_group' => $this->getExportGroup($export), ] ); } /** * Gather data stored in session from previous steps, store it inside redis * and redirect to the `generate` action. * * The data from previous steps is removed from session. * * @param string $alias * * @return RedirectResponse */ private function forwardToGenerate(Request $request, DirectExportInterface|ExportInterface $export, $alias, ?SavedExport $savedExport) { $dataCenters = $this->session->get('centers_step_raw', null); $dataFormatter = $this->session->get('formatter_step_raw', null); $dataExport = $this->session->get('export_step_raw', null); if (null === $dataFormatter && $export instanceof ExportInterface) { return $this->redirectToRoute('chill_main_export_new', [ 'alias' => $alias, 'step' => $this->getNextStep('generate', $export, true), 'from_saved' => $savedExport?->getId() ?? '', ]); } $parameters = [ 'formatter' => $dataFormatter ?? [], 'export' => $dataExport ?? [], 'centers' => $dataCenters ?? [], 'alias' => $alias, ]; unset($parameters['_token']); $key = md5(uniqid((string) random_int(0, mt_getrandmax()), false)); $this->redis->setEx($key, 3600, \serialize($parameters)); // remove data from session $this->session->remove('export_step_raw'); $this->session->remove('export_step'); $this->session->remove('formatter_step_raw'); $this->session->remove('formatter_step'); return $this->redirectToRoute('chill_main_export_download', [ 'key' => $key, 'alias' => $alias, 'from_saved' => $savedExport?->getId(), ]); } private function rebuildData($key, ?SavedExport $savedExport) { $rawData = $this->rebuildRawData($key); $alias = $rawData['alias']; if ($this->filterStatsByCenters) { $formCenters = $this->createCreateFormExport($alias, 'generate_centers', [], $savedExport); $formCenters->submit($rawData['centers']); $dataCenters = $formCenters->getData(); } else { $dataCenters = ['centers' => []]; } $formExport = $this->createCreateFormExport($alias, 'generate_export', $dataCenters, $savedExport); $formExport->submit($rawData['export']); $dataExport = $formExport->getData(); if (\count($rawData['formatter']) > 0) { $formFormatter = $this->createCreateFormExport( $alias, 'generate_formatter', $dataExport, $savedExport ); $formFormatter->submit($rawData['formatter']); $dataFormatter = $formFormatter->getData(); } return [$dataCenters, $dataExport, $dataFormatter ?? null]; } /** * @param string $alias * * @return Response */ private function selectCentersStep(Request $request, DirectExportInterface|ExportInterface $export, $alias, ?SavedExport $savedExport = null) { if (!$this->filterStatsByCenters) { return $this->redirectToRoute('chill_main_export_new', [ 'step' => $this->getNextStep('centers', $export), 'alias' => $alias, 'from_saved' => $request->get('from_saved', ''), ]); } /** @var ExportManager $exportManager */ $exportManager = $this->exportManager; $form = $this->createCreateFormExport($alias, 'centers', [], $savedExport); if (Request::METHOD_POST === $request->getMethod()) { $form->handleRequest($request); if ($form->isValid()) { $this->logger->debug('form centers is valid', [ 'location' => __METHOD__, ]); $data = $form->getData(); // check ACL if ( false === $exportManager->isGrantedForElement( $export, null, $exportManager->getPickedCenters($data['centers']) ) ) { throw $this->createAccessDeniedException('you do not have access to this export for those centers'); } $this->session->set( 'centers_step_raw', $request->request->all() ); $this->session->set('centers_step', $data); return $this->redirectToRoute('chill_main_export_new', [ 'step' => $this->getNextStep('centers', $export), 'alias' => $alias, 'from_saved' => $request->get('from_saved', ''), ]); } } return $this->render( '@ChillMain/Export/new_centers_step.html.twig', [ 'form' => $form->createView(), 'export' => $export, 'export_group' => $this->getExportGroup($export), ] ); } private function getExportGroup($target): string { $exportManager = $this->exportManager; $groups = $exportManager->getExportsGrouped(true); foreach ($groups as $group => $array) { foreach ($array as $alias => $export) { if ($export === $target) { return $group; } } } return ''; } /** * get the next step. If $reverse === true, the previous step is returned. * * This method provides a centralized way of handling next/previous step. * * @param string $step the current step * @param bool $reverse set to true to get the previous step * * @return string the next/current step * * @throws \LogicException if there is no step before or after the given step */ private function getNextStep($step, DirectExportInterface|ExportInterface $export, $reverse = false) { switch ($step) { case 'centers': if (false !== $reverse) { throw new \LogicException("there is no step before 'export'"); } return 'export'; case 'export': if ($export instanceof ExportInterface) { return $reverse ? 'centers' : 'formatter'; } if ($export instanceof DirectExportInterface) { return $reverse ? 'centers' : 'generate'; } // no break case 'formatter': return $reverse ? 'export' : 'generate'; case 'generate': if (false === $reverse) { throw new \LogicException("there is no step after 'generate'"); } return 'formatter'; default: throw new \LogicException("the step {$step} is not defined."); } } private function rebuildRawData(string $key): array { if (null === $key) { throw $this->createNotFoundException('key does not exists'); } if (1 !== $this->redis->exists($key)) { $this->addFlash('error', $this->translator->trans('This report is not available any more')); throw $this->createNotFoundException('key does not exists'); } $serialized = $this->redis->get($key); if (false === $serialized) { throw new \LogicException('the key could not be reached from redis'); } $rawData = \unserialize($serialized); $this->logger->notice('[export] choices for an export unserialized', [ 'key' => $key, 'rawData' => json_encode($rawData, JSON_THROW_ON_ERROR), ]); return $rawData; } private function getSavedExportFromRequest(Request $request): ?SavedExport { $savedExport = match ($savedExportId = $request->query->get('from_saved', '')) { '' => null, default => $this->savedExportRepository->find($savedExportId), }; if (null !== $savedExport && !$this->security->isGranted(SavedExportVoter::EDIT, $savedExport)) { throw new AccessDeniedHttpException('saved export edition not allowed'); } return $savedExport; } }