688 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
declare(strict_types=1);
/*
* 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.
*/
namespace Chill\MainBundle\Export;
use Chill\MainBundle\Form\Type\Export\ExportType;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Doctrine\ORM\QueryBuilder;
use Generator;
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 array|AggregatorInterface[]
*/
private array $aggregators = [];
/**
* Collected Exports, injected by DI.
*
* @var array|ExportInterface[]
*/
private array $exports = [];
/**
* The collected filters, injected by DI.
*
* @var array|FilterInterface[]
*/
private array $filters = [];
/**
* Collected Formatters, injected by DI.
*
* @var array|FormatterInterface[]
*/
private array $formatters = [];
private readonly string|\Stringable|\Symfony\Component\Security\Core\User\UserInterface $user;
public function __construct(
private readonly LoggerInterface $logger,
private readonly AuthorizationCheckerInterface $authorizationChecker,
private readonly AuthorizationHelperInterface $authorizationHelper,
TokenStorageInterface $tokenStorage,
iterable $exports,
iterable $aggregators,
iterable $filters
//iterable $formatters,
//iterable $exportElementProvider
) {
$this->user = $tokenStorage->getToken()->getUser();
$this->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): iterable
{
if ($export instanceof DirectExportInterface) {
return;
}
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 null|iterable<string, AggregatorInterface> a \Generator that contains aggretagors. The key is the filter's alias
*/
public function &getAggregatorsApplyingOn(DirectExportInterface|ExportInterface $export, ?array $centers = null): ?iterable
{
if ($export instanceof ListInterface || $export instanceof DirectExportInterface) {
return;
}
foreach ($this->aggregators as $alias => $aggregator) {
if (
in_array($aggregator->applyOn(), $export->supportsModifiers(), true)
&& $this->isGrantedForElement($aggregator, $export, $centers)
) {
yield $alias => $aggregator;
}
}
}
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
*
* @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];
}
/**
* @return iterable<string, AggregatorInterface>
*/
public function getAggregators(array $aliases): iterable
{
foreach ($aliases as $alias) {
yield $alias => $this->getAggregator($alias);
}
}
/**
* Get the types for known exports
*
* @return list<string> 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<string, ExportInterface|DirectExportInterface> 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<string, array<string, ExportInterface|DirectExportInterface>> 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];
}
/**
* get all filters.
*
* @param array<string> $aliases
* @return iterable<string, FilterInterface> $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<string, FormatterInterface>
*/
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<string>
*/
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,
\Chill\MainBundle\Export\DirectExportInterface|\Chill\MainBundle\Export\ExportInterface $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->user,
$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 = [];
foreach ($centers as $center) {
$r[] = [
'center' => $center,
'circles' => $this->authorizationHelper->getReachableScopes(
$this->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 ($this->isGrantedForElement($aggregator, $export, $center) === false) {
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 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,
mixed $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']);
}
}
/**
* @return iterable<string, AggregatorInterface>
*/
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;
}
/**
* @param mixed $data the data from the filter key of the ExportType
*/
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.
*
* @param mixed $data the data from the `filters` key of the ExportType
*
* @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);
}
}