exports = iterator_to_array($exports); $this->aggregators = iterator_to_array($aggregators); $this->filters = iterator_to_array($filters); // NOTE: PHP crashes on the next line (exit error code 11). This is desactivated until further investigation // $this->formatters = iterator_to_array($formatters); // foreach ($exportElementProvider as $prefix => $provider) { // $this->addExportElementsProvider($provider, $prefix); // } } /** * Return a \Generator containing filter which support type. If `$centers` is * not null, restrict the given filters to the center the user have access to. * * if $centers is null, the function will returns all filters where the user * has access in every centers he can reach (if the user can use the filter F in * center A, but not in center B, the filter F will not be returned) * * @param \Chill\MainBundle\Entity\Center[] $centers the centers where the user have access to * * @return FilterInterface[] a \Generator that contains filters. The key is the filter's alias */ public function getFiltersApplyingOn(DirectExportInterface|ExportInterface $export, ?array $centers = null): array { if ($export instanceof DirectExportInterface) { return []; } $filters = []; foreach ($this->filters as $alias => $filter) { if ( \in_array($filter->applyOn(), $export->supportsModifiers(), true) && $this->isGrantedForElement($filter, $export, $centers) ) { $filters[$alias] = $filter; } } return $filters; } /** * Return a \Generator containing aggregators supported by the given export. * * @internal This class check the interface implemented by export, and, if ´ListInterface´ is used, return an empty array * * @return array an array that contains aggregators. The key is the filter's alias */ public function getAggregatorsApplyingOn(DirectExportInterface|ExportInterface $export, ?array $centers = null): array { if ($export instanceof ListInterface || $export instanceof DirectExportInterface) { return []; } $aggregators = []; foreach ($this->aggregators as $alias => $aggregator) { if ( \in_array($aggregator->applyOn(), $export->supportsModifiers(), true) && $this->isGrantedForElement($aggregator, $export, $centers) ) { $aggregators[$alias] = $aggregator; } } return $aggregators; } public function addExportElementsProvider(ExportElementsProviderInterface $provider, string $prefix): void { foreach ($provider->getExportElements() as $suffix => $element) { $alias = $prefix.'_'.$suffix; if ($element instanceof ExportInterface) { $this->exports[$alias] = $element; } elseif ($element instanceof FilterInterface) { $this->filters[$alias] = $element; } elseif ($element instanceof AggregatorInterface) { $this->aggregators[$alias] = $element; } elseif ($element instanceof FormatterInterface) { $this->addFormatter($element, $alias); } else { throw new \LogicException('This element '.$element::class.' is not an instance of export element'); } } } /** * add a formatter. * * @internal used by DI */ public function addFormatter(FormatterInterface $formatter, string $alias) { $this->formatters[$alias] = $formatter; } /** * Generate a response which contains the requested data. */ public function generate(string $exportAlias, array $pickedCentersData, array $data, array $formatterData): Response { $export = $this->getExport($exportAlias); $centers = $this->getPickedCenters($pickedCentersData); if ($export instanceof DirectExportInterface) { return $export->generate( $this->buildCenterReachableScopes($centers, $export), $data[ExportType::EXPORT_KEY] ); } $query = $export->initiateQuery( $this->retrieveUsedModifiers($data), $this->buildCenterReachableScopes($centers, $export), $data[ExportType::EXPORT_KEY] ); if ($query instanceof \Doctrine\ORM\NativeQuery) { // throw an error if the export require other modifier, which is // not allowed when the export return a `NativeQuery` if (\count($export->supportsModifiers()) > 0) { throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\\Doctrine\\ORM\\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\\ORM\\QueryBuilder`'); } } elseif ($query instanceof QueryBuilder) { // handle filters $this->handleFilters($export, $query, $data[ExportType::FILTER_KEY], $centers); // handle aggregators $this->handleAggregators($export, $query, $data[ExportType::AGGREGATOR_KEY], $centers); $this->logger->notice('[export] will execute this qb in export', [ 'dql' => $query->getDQL(), ]); } else { throw new \UnexpectedValueException('The method `intiateQuery` should return a `\\Doctrine\\ORM\\NativeQuery` or a `Doctrine\\ORM\\QueryBuilder` object.'); } $result = $export->getResult($query, $data[ExportType::EXPORT_KEY]); if (!is_iterable($result)) { throw new \UnexpectedValueException(sprintf('The result of the export should be an iterable, %s given', \gettype($result))); } /** @var FormatterInterface $formatter */ $formatter = $this->getFormatter($this->getFormatterAlias($data)); $filtersData = []; $aggregatorsData = []; if ($query instanceof QueryBuilder) { $aggregators = $this->retrieveUsedAggregators($data[ExportType::AGGREGATOR_KEY]); foreach ($aggregators as $alias => $aggregator) { $aggregatorsData[$alias] = $data[ExportType::AGGREGATOR_KEY][$alias]['form']; } } $filters = $this->retrieveUsedFilters($data[ExportType::FILTER_KEY]); foreach ($filters as $alias => $filter) { $filtersData[$alias] = $data[ExportType::FILTER_KEY][$alias]['form']; } return $formatter->getResponse( $result, $formatterData, $exportAlias, $data[ExportType::EXPORT_KEY], $filtersData, $aggregatorsData ); } /** * @param string $alias * * @return AggregatorInterface * * @throws \RuntimeException if the aggregator is not known */ public function getAggregator($alias) { if (!\array_key_exists($alias, $this->aggregators)) { throw new \RuntimeException("The aggregator with alias {$alias} is not known."); } return $this->aggregators[$alias]; } /** * @return iterable */ public function getAggregators(array $aliases): iterable { foreach ($aliases as $alias) { yield $alias => $this->getAggregator($alias); } } /** * Get the types for known exports. * * @return list the existing type for known exports */ public function getExistingExportsTypes(): array { $existingTypes = []; foreach ($this->exports as $export) { if (!\in_array($export->getType(), $existingTypes, true)) { $existingTypes[] = $export->getType(); } } return $existingTypes; } /** * Return an export by his alias. * * @param string $alias * * @throws \RuntimeException */ public function getExport($alias): DirectExportInterface|ExportInterface { if (!\array_key_exists($alias, $this->exports)) { throw new \RuntimeException("The export with alias {$alias} is not known."); } return $this->exports[$alias]; } /** * Return all exports. The exports's alias are the array's keys. * * @param bool $whereUserIsGranted if true (default), restrict to user which are granted the right to execute the export * * @return iterable an array where export's alias are keys */ public function getExports($whereUserIsGranted = true): iterable { foreach ($this->exports as $alias => $export) { if ($whereUserIsGranted) { if ($this->isGrantedForElement($export, null, null)) { yield $alias => $export; } } else { yield $alias => $export; } } } /** * Get all exports grouped in an array. * * @return array> where keys are the groups's name and value is an array of exports */ public function getExportsGrouped(bool $whereUserIsGranted = true): array { $groups = ['_' => []]; foreach ($this->getExports($whereUserIsGranted) as $alias => $export) { if ($export instanceof GroupedExportInterface) { $groups[$export->getGroup()][$alias] = $export; } else { $groups['_'][$alias] = $export; } } return $groups; } /** * @throws \RuntimeException if the filter is not known */ public function getFilter(string $alias): FilterInterface { if (!\array_key_exists($alias, $this->filters)) { throw new \RuntimeException("The filter with alias {$alias} is not known."); } return $this->filters[$alias]; } public function getAllFilters(): array { $filters = []; foreach ($this->filters as $alias => $filter) { $filters[$alias] = $filter; } return $filters; } /** * get all filters. * * @param array $aliases * * @return iterable $aliases */ public function getFilters(array $aliases): iterable { foreach ($aliases as $alias) { yield $alias => $this->getFilter($alias); } } public function getFormatter(string $alias): FormatterInterface { if (!\array_key_exists($alias, $this->formatters)) { throw new \RuntimeException("The formatter with alias {$alias} is not known."); } return $this->formatters[$alias]; } /** * get the formatter alias from the form export data. * * @param array $data the data from the export form * * @string the formatter alias|null */ public function getFormatterAlias(array $data): ?string { if (\array_key_exists(ExportType::PICK_FORMATTER_KEY, $data)) { return $data[ExportType::PICK_FORMATTER_KEY]['alias']; } return null; } /** * Get all formatters which supports one of the given types. * * @return iterable */ public function getFormattersByTypes(array $types): iterable { foreach ($this->formatters as $alias => $formatter) { if (\in_array($formatter->getType(), $types, true)) { yield $alias => $formatter; } } } /** * Get the Center picked by the user for this export. The data are * extracted from the PickCenterType data. * * @param array $data the data from a PickCenterType * * @return \Chill\MainBundle\Entity\Center[] the picked center */ public function getPickedCenters(array $data): array { return $data; } /** * get the aggregators typse used in the form export data. * * @param array $data the data from the export form * * @return list */ public function getUsedAggregatorsAliases(array $data): array { $aggregators = $this->retrieveUsedAggregators($data[ExportType::AGGREGATOR_KEY]); return array_keys(iterator_to_array($aggregators)); } /** * Return true if the current user has access to the ExportElement for every * center, false if the user hasn't access to element for at least one center. */ public function isGrantedForElement( DirectExportInterface|ExportInterface|ModifierInterface $element, DirectExportInterface|ExportInterface|null $export = null, ?array $centers = null ): bool { if ($element instanceof ExportInterface || $element instanceof DirectExportInterface) { $role = $element->requiredRole(); } else { if (null === $element->addRole()) { if (null === $export) { throw new \LogicException('The export should not be null: as the ModifierInstance element is not an export, we should be aware of the export to determine which role is required'); } $role = $export->requiredRole(); } else { $role = $element->addRole(); } } if (null === $centers || [] === $centers) { // we want to try if at least one center is reachable return [] !== $this->authorizationHelper->getReachableCenters( $this->tokenStorage->getToken()->getUser(), $role ); } foreach ($centers as $center) { if (false === $this->authorizationChecker->isGranted($role, $center)) { // debugging $this->logger->debug('user has no access to element', [ 'method' => __METHOD__, 'type' => $element::class, 'center' => $center->getName(), 'role' => $role, ]); return false; } } return true; } /** * build the array required for defining centers and circles in the initiate * queries of ExportElementsInterfaces. * * @param \Chill\MainBundle\Entity\Center[] $centers */ private function buildCenterReachableScopes(array $centers, ExportElementInterface $element) { $r = []; $user = $this->tokenStorage->getToken()->getUser(); if (!$user instanceof User) { return []; } foreach ($centers as $center) { $r[] = [ 'center' => $center, 'circles' => $this->authorizationHelper->getReachableScopes( $user, $element->requiredRole(), $center ), ]; } return $r; } /** * Alter the query with selected aggregators. * * Check for acl. If an user is not authorized to see an aggregator, throw an * UnauthorizedException. * * @throw UnauthorizedHttpException if the user is not authorized */ private function handleAggregators( ExportInterface $export, QueryBuilder $qb, array $data, array $center ) { $aggregators = $this->retrieveUsedAggregators($data); foreach ($aggregators as $alias => $aggregator) { if (false === $this->isGrantedForElement($aggregator, $export, $center)) { throw new UnauthorizedHttpException('You are not authorized to use the aggregator'.$aggregator->getTitle()); } $formData = $data[$alias]; $aggregator->alterQuery($qb, $formData['form']); } } /** * alter the query with selected filters. * * This function check the acl. * * @param \Chill\MainBundle\Entity\Center[] $centers the picked centers * * @throw UnauthorizedHttpException if the user is not authorized */ private function handleFilters( ExportInterface $export, QueryBuilder $qb, mixed $data, array $centers ) { $filters = $this->retrieveUsedFilters($data); foreach ($filters as $alias => $filter) { if (false === $this->isGrantedForElement($filter, $export, $centers)) { throw new UnauthorizedHttpException('You are not authorized to use the filter '.$filter->getTitle()); } $formData = $data[$alias]; $this->logger->debug('alter query by filter '.$alias, [ 'class' => self::class, 'function' => __FUNCTION__, ]); $filter->alterQuery($qb, $formData['form']); } } /** * @return iterable */ private function retrieveUsedAggregators(mixed $data): iterable { if (null === $data) { return []; } foreach ($data as $alias => $aggregatorData) { if (true === $aggregatorData['enabled']) { yield $alias => $this->getAggregator($alias); } } } /** * @return string[] */ private function retrieveUsedAggregatorsType(mixed $data) { if (null === $data) { return []; } $usedTypes = []; foreach ($this->retrieveUsedAggregators($data) as $alias => $aggregator) { if (!\in_array($aggregator->applyOn(), $usedTypes, true)) { $usedTypes[] = $aggregator->applyOn(); } } return $usedTypes; } private function retrieveUsedFilters(mixed $data): iterable { if (null === $data) { return []; } foreach ($data as $alias => $filterData) { if (true === $filterData['enabled']) { yield $alias => $this->getFilter($alias); } } } /** * Retrieve the filter used in this export. * * @return array an array with types */ private function retrieveUsedFiltersType(mixed $data): iterable { if (null === $data) { return []; } $usedTypes = []; foreach ($data as $alias => $filterData) { if (true === $filterData['enabled']) { $filter = $this->getFilter($alias); if (!\in_array($filter->applyOn(), $usedTypes, true)) { $usedTypes[] = $filter->applyOn(); } } } return $usedTypes; } /** * parse the data to retrieve the used filters and aggregators. * * @return string[] */ private function retrieveUsedModifiers(mixed $data) { if (null === $data) { return []; } $usedTypes = array_merge( $this->retrieveUsedFiltersType($data[ExportType::FILTER_KEY]), $this->retrieveUsedAggregatorsType($data[ExportType::AGGREGATOR_KEY]) ); $this->logger->debug( 'Required types are '.implode(', ', $usedTypes), ['class' => self::class, 'function' => __FUNCTION__] ); return array_unique($usedTypes); } }