771 lines
22 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Export;
use Chill\MainBundle\Form\Type\Export\ExportType;
use Chill\MainBundle\Form\Type\Export\PickCenterType;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Generator;
use InvalidArgumentException;
use LogicException;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use UnexpectedValueException;
use function array_key_exists;
use function count;
use function get_class;
use function gettype;
use function in_array;
/**
* Collects all agregators, filters and export from
* the installed bundle, and performs the export logic.
*/
class ExportManager
{
/**
* The collected aggregators, injected by DI.
*
* @var AggregatorInterface[]
*/
private $aggregators = [];
/**
* @var AuthorizationChecker
*/
private $authorizationChecker;
/**
* @var AuthorizationHelper
*/
private $authorizationHelper;
/**
* @var EntityManagerInterface
*/
private $em;
/**
* Collected Exports, injected by DI.
*
* @var ExportInterface[]
*/
private $exports = [];
/**
* The collected filters, injected by DI.
*
* @var FilterInterface[]
*/
private $filters = [];
/**
* Collected Formatters, injected by DI.
*
* @var FormatterInterface[]
*/
private $formatters = [];
/**
* a logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* @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();
}
/**
* 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(), true)
&& $this->isGrantedForElement($filter, $export, $centers)
) {
yield $alias => $filter;
}
}
}
/**
* 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(), true)
&& $this->isGrantedForElement($aggregator, $export, $centers)
) {
yield $alias => $aggregator;
}
}
}
/**
* add an aggregator.
*
* @internal used by DI
*
* @param string $alias
*/
public function addAggregator(AggregatorInterface $aggregator, $alias)
{
$this->aggregators[$alias] = $aggregator;
}
/**
* add an export.
*
* @internal used by DI
*
* @param DirectExportInterface|ExportInterface $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
));
}
}
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');
}
}
}
/**
* add a Filter.
*
* @internal Normally used by the dependency injection
*
* @param string $alias
*/
public function addFilter(FilterInterface $filter, $alias)
{
$this->filters[$alias] = $filter;
}
/**
* add a formatter.
*
* @internal used by DI
*
* @param type $alias
*/
public function addFormatter(FormatterInterface $formatter, $alias)
{
$this->formatters[$alias] = $formatter;
}
/**
* 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(), [
'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 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
*
* @throws RuntimeException if the aggregator is not known
*
* @return AggregatorInterface
*/
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);
}
}
/**
* @return string[] the existing type for known exports
*/
public function getExistingExportsTypes()
{
$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
*
* @return ExportInterface
*/
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];
}
/**
* 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 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;
}
/**
* @param string $alias
*
* @throws RuntimeException if the filter is not known
*
* @return FilterInterface
*/
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);
}
}
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 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)
{
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 Generator
*/
public function getFormattersByTypes(array $types)
{
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)
{
return $data;
}
/**
* 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));
}
/**
* 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 DirectExportInterface|ExportInterface $export
*
* @return bool
*/
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 (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();
}
} else {
throw new LogicException('The element is not an ModifiersInterface or '
. 'an ExportInterface.');
}
if (null === $centers) {
$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', [
'method' => __METHOD__,
'type' => get_class($element),
'center' => $center->getName(),
'role' => $role->getRole(),
]);
///// Bypasse les autorisations qui empêche d'afficher les nouveaux exports
return true;
///// TODO supprimer le return true
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 = [];
foreach ($centers as $center) {
$r[] = [
'center' => $center,
'circles' => $this->authorizationHelper->getReachableScopes(
$this->user,
$element->requiredRole()->getRole(),
$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.
*
* @param type $data
* @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']);
}
}
/**
* alter the query with selected filters.
*
* This function check the acl.
*
* @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, [
'class' => self::class, 'function' => __FUNCTION__,
]);
$filter->alterQuery($qb, $formData['form']);
}
}
/**
* @param mixed $data
*
* @return AggregatorInterface[]
*/
private function retrieveUsedAggregators($data)
{
if (null === $data) {
return [];
}
foreach ($data as $alias => $aggregatorData) {
if (true === $aggregatorData['enabled']) {
yield $alias => $this->getAggregator($alias);
}
}
}
/**
* @param mixed $data
*
* @return string[]
*/
private function retrieveUsedAggregatorsType($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;
}
/**
* @param type $data the data from the filter key of the ExportType
*/
private function retrieveUsedFilters($data)
{
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.
*
* @param mixed $data the data from the `filters` key of the ExportType
*
* @return array an array with types
*/
private function retrieveUsedFiltersType($data)
{
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.
*
* @param mixed $data
*
* @return string[]
*/
private function retrieveUsedModifiers($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);
}
}