*
* 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']);
}
}
}