* * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ namespace Chill\MainBundle\Export; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Export\AggregatorInterface; use Chill\MainBundle\Export\ExportInterface; use Chill\MainBundle\Export\FormatterInterface; use Symfony\Component\HttpFoundation\Response; use Psr\Log\LoggerInterface; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; use Chill\MainBundle\Security\Authorization\AuthorizationHelper; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Chill\MainBundle\Form\Type\Export\PickCenterType; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; use Chill\MainBundle\Form\Type\Export\ExportType; use Chill\MainBundle\Export\ListInterface; /** * Collects all agregators, filters and export from * the installed bundle, and performs the export logic. * * * @author Julien Fastré */ class ExportManager { /** * The collected filters, injected by DI * * @var FilterInterface[] */ private $filters = array(); /** * The collected aggregators, injected by DI * * @var AggregatorInterface[] */ private $aggregators = array(); /** * Collected Exports, injected by DI * * @var ExportInterface[] */ private $exports = array(); /** * Collected Formatters, injected by DI * * @var FormatterInterface[] */ private $formatters = array(); /** * a logger * * @var LoggerInterface */ private $logger; /** * * @var EntityManagerInterface */ private $em; /** * * @var AuthorizationChecker */ private $authorizationChecker; /** * * @var AuthorizationHelper */ private $authorizationHelper; /** * * @var \Symfony\Component\Security\Core\User\UserInterface */ private $user; public function __construct( LoggerInterface $logger, EntityManagerInterface $em, AuthorizationCheckerInterface $authorizationChecker, AuthorizationHelper $authorizationHelper, TokenStorageInterface $tokenStorage) { $this->logger = $logger; $this->em = $em; $this->authorizationChecker = $authorizationChecker; $this->authorizationHelper = $authorizationHelper; $this->user = $tokenStorage->getToken()->getUser(); } /** * add a Filter * * @internal Normally used by the dependency injection * * @param FilterInterface $filter * @param string $alias */ public function addFilter(FilterInterface $filter, $alias) { $this->filters[$alias] = $filter; } /** * add an aggregator * * @internal used by DI * * @param AggregatorInterface $aggregator * @param string $alias */ public function addAggregator(AggregatorInterface $aggregator, $alias) { $this->aggregators[$alias] = $aggregator; } /** * add an export * * @internal used by DI * * @param ExportInterface|DirectExportInterface $export * @param type $alias */ public function addExport($export, $alias) { if ($export instanceof ExportInterface || $export instanceof DirectExportInterface) { $this->exports[$alias] = $export; } else { throw new \InvalidArgumentException(sprintf("The export with alias %s " . "does not implements %s or %s.", $alias, ExportInterface::class, DirectExportInterface::class)); } } /** * add a formatter * * @internal used by DI * * @param FormatterInterface $formatter * @param type $alias */ public function addFormatter(FormatterInterface $formatter, $alias) { $this->formatters[$alias] = $formatter; } public function addExportElementsProvider(ExportElementsProviderInterface $provider, $prefix) { foreach ($provider->getExportElements() as $suffix => $element) { $alias = $prefix.'_'.$suffix; if ($element instanceof ExportInterface) { $this->addExport($element, $alias); } elseif ($element instanceof FilterInterface) { $this->addFilter($element, $alias); } elseif ($element instanceof AggregatorInterface) { $this->addAggregator($element, $alias); } elseif ($element instanceof FormatterInterface) { $this->addFormatter($element, $alias); } else { throw new \LogicException("This element ".\get_class($element)." " . "is not an instance of export element"); } } } /** * * @return string[] the existing type for known exports */ public function getExistingExportsTypes() { $existingTypes = array(); foreach($this->exports as $export) { if (!in_array($export->getType(), $existingTypes)) { array_push($existingTypes, $export->getType()); } } return $existingTypes; } /** * Return all exports. The exports's alias are the array's keys. * * @param boolean $whereUserIsGranted if true (default), restrict to user which are granted the right to execute the export * @return ExportInterface[] an array where export's alias are keys */ public function getExports($whereUserIsGranted = true) { 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. * * @param bool $whereUserIsGranted * @return array where keys are the groups's name and value is an array of exports */ public function getExportsGrouped($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; } /** * Return an export by his alias * * @param string $alias * @return ExportInterface * @throws \RuntimeException */ public function getExport($alias) { if (!array_key_exists($alias, $this->exports)) { throw new \RuntimeException("The export with alias $alias is not known."); } return $this->exports[$alias]; } /** * * @param string $alias * @return FilterInterface * @throws \RuntimeException if the filter is not known */ public function getFilter($alias) { if (!array_key_exists($alias, $this->filters)) { throw new \RuntimeException("The filter with alias $alias is not known."); } return $this->filters[$alias]; } /** * get all filters * * @param \Generator $aliases */ public function getFilters(array $aliases) { foreach($aliases as $alias) { yield $alias => $this->getFilter($alias); } } /** * * @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]; } public function getAggregators(array $aliases) { foreach ($aliases as $alias) { yield $alias => $this->getAggregator($alias); } } public function getFormatter($alias) { if (!array_key_exists($alias, $this->formatters)) { throw new \RuntimeException("The formatter with alias $alias is not known."); } return $this->formatters[$alias]; } /** * Get all formatters which supports one of the given types. * * * @param array $types * @return \Generator */ public function getFormattersByTypes(array $types) { foreach ($this->formatters as $alias => $formatter) { if (in_array($formatter->getType(), $types)) { yield $alias => $formatter; } } } /** * 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(ExportInterface $export, array $centers = null) { foreach ($this->filters as $alias => $filter) { if (in_array($filter->applyOn(), $export->supportsModifiers()) && $this->isGrantedForElement($filter, $export, $centers)) { yield $alias => $filter; } } } /** * 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. * * @param \Chill\MainBundle\Export\ExportElementInterface $element * @param ExportInterface|DirectExportInterface $export * @param array|null $centers, if null, the function take into account all the reachables centers for the current user and the role given by element::requiredRole * @return boolean */ public function isGrantedForElement(ExportElementInterface $element, ExportElementInterface $export = NULL, array $centers = null) { if ($element instanceof ExportInterface || $element instanceof DirectExportInterface) { $role = $element->requiredRole(); } elseif ($element instanceof ModifierInterface ) { if (is_null($element->addRole())) { if (is_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"); } else { $role = $export->requiredRole(); } } else { $role = $element->addRole(); } } else { throw new \LogicException("The element is not an ModifiersInterface or " . "an ExportInterface."); } if ($centers === null) { $centers = $this->authorizationHelper->getReachableCenters($this->user, $role); } if (count($centers) === 0) { return false; } foreach($centers as $center) { if ($this->authorizationChecker->isGranted($role->getRole(), $center) === false) { //debugging $this->logger->debug('user has no access to element', array( 'method' => __METHOD__, 'type' => get_class($element), 'center' => $center->getName(), 'role' => $role->getRole() )); return false; } } return true; } /** * 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 AggregatorInterface[] a \Generator that contains aggretagors. The key is the filter's alias */ public function &getAggregatorsApplyingOn(ExportInterface $export, array $centers = null) { if ($export instanceof ListInterface) { return; } foreach ($this->aggregators as $alias => $aggregator) { if (in_array($aggregator->applyOn(), $export->supportsModifiers()) && $this->isGrantedForElement($aggregator, $export, $centers)) { yield $alias => $aggregator; } } } /** * Generate a response which contains the requested data. * * @param string $exportAlias * @param mixed[] $data * @return Response */ public function generate($exportAlias, array $pickedCentersData, array $data, array $formatterData) { $export = $this->getExport($exportAlias); //$qb = $this->em->createQueryBuilder(); $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->debug('current query is '.$query->getDQL(), array( 'class' => self::class, 'function' => __FUNCTION__ )); } 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 $formatter FormatterInterface */ $formatter = $this->getFormatter($this->getFormatterAlias($data)); $filtersData = array(); $aggregatorsData = array(); 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); } /** * 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 = array(); foreach($centers as $center) { $r[] = array( 'center' => $center, 'circles' => $this->authorizationHelper->getReachableScopes($this->user, $element->requiredRole(), $center) ); } return $r; } /** * get the aggregators typse used in the form export data * * @param array $data the data from the export form * @return string[] */ public function getUsedAggregatorsAliases(array $data) { $aggregators = $this->retrieveUsedAggregators($data[ExportType::AGGREGATOR_KEY]); return array_keys(iterator_to_array($aggregators)); } /** * get the formatter alias from the form export data * * @param array $data the data from the export form * @string the formatter alias */ public function getFormatterAlias(array $data) { return $data[ExportType::PICK_FORMATTER_KEY]['alias']; } /** * 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) { return $data; } /** * parse the data to retrieve the used filters and aggregators * * @param mixed $data * @return string[] */ private function retrieveUsedModifiers($data) { if ($data === null) { return []; } $usedTypes = array_merge( $this->retrieveUsedFiltersType($data[ExportType::FILTER_KEY]), $this->retrieveUsedAggregatorsType($data[ExportType::AGGREGATOR_KEY]) ); $this->logger->debug('Required types are '.implode(', ', $usedTypes), array('class' => self::class, 'function' => __FUNCTION__)); return array_unique($usedTypes); } /** * Retrieve the filter used in this export. * * @param mixed $data the data from the `filters` key of the ExportType * @return array an array with types */ private function retrieveUsedFiltersType($data) { if ($data === null) { return []; } $usedTypes = array(); foreach($data as $alias => $filterData) { if ($filterData['enabled'] == true){ $filter = $this->getFilter($alias); if (!in_array($filter->applyOn(), $usedTypes)) { array_push($usedTypes, $filter->applyOn()); } } } return $usedTypes; } /** * * @param mixed $data * @return string[] */ private function retrieveUsedAggregatorsType($data) { if ($data === null) { return []; } $usedTypes = array(); foreach($this->retrieveUsedAggregators($data) as $alias => $aggregator) { if (!in_array($aggregator->applyOn(), $usedTypes)) { array_push($usedTypes, $aggregator->applyOn()); } } return $usedTypes; } /** * * @param mixed $data * @return AggregatorInterface[] */ private function retrieveUsedAggregators($data) { if ($data === null) { return []; } foreach ($data as $alias => $aggregatorData) { if ($aggregatorData['enabled'] === true){ yield $alias => $this->getAggregator($alias); } } } /** * * @param type $data the data from the filter key of the ExportType */ private function retrieveUsedFilters($data) { if ($data === null) { return []; } foreach ($data as $alias => $filterData) { if ($filterData['enabled'] === true) { yield $alias => $this->getFilter($alias); } } } /** * alter the query with selected filters. * * This function check the acl. * * @param ExportInterface $export * @param QueryBuilder $qb * @param mixed $data the data under the initial 'filters' data * @param \Chill\MainBundle\Entity\Center[] $centers the picked centers * @throw UnauthorizedHttpException if the user is not authorized */ private function handleFilters( ExportInterface $export, QueryBuilder $qb, $data, array $centers) { $filters = $this->retrieveUsedFilters($data); foreach($filters as $alias => $filter) { if ($this->isGrantedForElement($filter, $export, $centers) === false) { throw new UnauthorizedHttpException("You are not authorized to " . "use the filter ".$filter->getTitle()); } $formData = $data[$alias]; $this->logger->debug('alter query by filter '.$alias, array( 'class' => self::class, 'function' => __FUNCTION__ )); $filter->alterQuery($qb, $formData['form']); } } /** * Alter the query with selected aggregators * * Check for acl. If an user is not authorized to see an aggregator, throw an * UnauthorizedException. * * @param ExportInterface $export * @param QueryBuilder $qb * @param type $data * @param \Chill\MainBundle\Entity\Center[] $centers the picked centers * @throw UnauthorizedHttpException if the user is not authorized */ private function handleAggregators( ExportInterface $export, QueryBuilder $qb, $data, array $center) { $aggregators = $this->retrieveUsedAggregators($data); foreach ($aggregators as $alias => $aggregator) { $formData = $data[$alias]; $aggregator->alterQuery($qb, $formData['form']); } } }