<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\CoreBundle\EventListener\DcGeneral\Table\Attribute;

use ContaoCommunityAlliance\DcGeneral\Contao\RequestScopeDeterminator;
use ContaoCommunityAlliance\DcGeneral\Contao\View\Contao2BackendView\Event\EncodePropertyValueFromWidgetEvent;
use MetaModels\Attribute\IAttributeFactory;
use MetaModels\Helper\TableManipulator;
use MetaModels\IFactory;

/**
 * This class takes care of validating the column name of an attribute.
 */
class ColNameValidationListener extends BaseListener
{
    /**
     * The table manipulator.
     *
     * @var TableManipulator
     */
    private $tableManipulator;

    /**
     * Create a new instance.
     *
     * @param RequestScopeDeterminator $scopeDeterminator The scope determinator.
     * @param IAttributeFactory        $attributeFactory  The attribute factory.
     * @param IFactory                 $factory           The MetaModel factory.
     * @param TableManipulator         $tableManipulator  The table manipulator.
     */
    public function __construct(
        RequestScopeDeterminator $scopeDeterminator,
        IAttributeFactory $attributeFactory,
        IFactory $factory,
        TableManipulator $tableManipulator
    ) {
        parent::__construct($scopeDeterminator, $attributeFactory, $factory);
        $this->tableManipulator = $tableManipulator;
    }

    /**
     * Validate the column name and ensure that the column does not exist already.
     *
     * @param EncodePropertyValueFromWidgetEvent $event The event.
     *
     * @return void
     *
     * @throws \RuntimeException When the column name is illegal or duplicate.
     */
    public function handle(EncodePropertyValueFromWidgetEvent $event)
    {
        if (!parent::wantToHandle($event) || ($event->getProperty() !== 'colname')) {
            return;
        }

        $oldColumnName = $event->getModel()->getProperty($event->getProperty());
        $columnName    = $event->getValue();
        $metaModel     = $this->getMetaModelByModelPid($event->getModel());

        if ((!$columnName) || $oldColumnName !== $columnName) {
            $this->tableManipulator->checkColumnDoesNotExist($metaModel->getTableName(), $columnName);

            $colNames = array_keys($metaModel->getAttributes());
            if (in_array($columnName, $colNames)) {
                throw new \RuntimeException(
                    sprintf(
                        $event->getEnvironment()->getTranslator()->translate('columnExists', 'ERR'),
                        $columnName,
                        $metaModel->getTableName()
                    )
                );
            }
        }
    }
}


<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\CoreBundle\EventListener\DcGeneral\DefinitionBuilder;

use ContaoCommunityAlliance\DcGeneral\Contao\DataDefinition\Definition\Contao2BackendViewDefinition;
use ContaoCommunityAlliance\DcGeneral\Contao\DataDefinition\Definition\Contao2BackendViewDefinitionInterface;
use ContaoCommunityAlliance\DcGeneral\DataDefinition\Definition\View\Command;
use ContaoCommunityAlliance\DcGeneral\DataDefinition\Definition\View\CommandCollectionInterface;
use ContaoCommunityAlliance\DcGeneral\DataDefinition\Definition\View\CommandInterface;
use ContaoCommunityAlliance\DcGeneral\DataDefinition\Definition\View\CopyCommand;
use ContaoCommunityAlliance\DcGeneral\DataDefinition\Definition\View\CutCommand;
use ContaoCommunityAlliance\DcGeneral\DataDefinition\Definition\View\SelectCommand;
use ContaoCommunityAlliance\DcGeneral\Exception\DcGeneralInvalidArgumentException;
use MetaModels\CoreBundle\Assets\IconBuilder;
use MetaModels\DcGeneral\DataDefinition\IMetaModelDataDefinition;
use MetaModels\DcGeneral\Events\MetaModel\BuildMetaModelOperationsEvent;
use MetaModels\IFactory;
use MetaModels\IMetaModel;
use MetaModels\ViewCombination\ViewCombination;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Translation\TranslatorInterface;

/**
 * This class builds the commands.
 */
class CommandBuilder
{
    use MetaModelDefinitionBuilderTrait;

    /**
     * The event dispatcher.
     *
     * @var EventDispatcherInterface
     */
    private $dispatcher;

    /**
     * The view combinations.
     *
     * @var ViewCombination
     */
    private $viewCombination;

    /**
     * The MetaModels factory.
     *
     * @var IFactory
     */
    private $factory;

    /**
     * The container (only set during build phase).
     *
     * @var IMetaModelDataDefinition
     */
    private $container;

    /**
     * The input screen (only set during build phase).
     *
     * @var array
     */
    private $inputScreen;

    /**
     * The icon builder.
     *
     * @var IconBuilder
     */
    private $iconBuilder;

    /**
     * The translator.
     *
     * @var TranslatorInterface
     */
    private $translator;

    /**
     * Create a new instance.
     *
     * @param EventDispatcherInterface $dispatcher      The event dispatcher.
     * @param ViewCombination          $viewCombination The view combinations.
     * @param IFactory                 $factory         The MetaModels factory.
     * @param IconBuilder              $iconBuilder     The icon builder.
     * @param TranslatorInterface      $translator      The translator.
     */
    public function __construct(
        EventDispatcherInterface $dispatcher,
        ViewCombination $viewCombination,
        IFactory $factory,
        IconBuilder $iconBuilder,
        TranslatorInterface $translator
    ) {
        $this->dispatcher      = $dispatcher;
        $this->viewCombination = $viewCombination;
        $this->factory         = $factory;
        $this->iconBuilder     = $iconBuilder;
        $this->translator      = $translator;
    }

    /**
     * Parse and build the backend view definition for the old Contao2 backend view.
     *
     * @param IMetaModelDataDefinition $container The data container.
     *
     * @throws DcGeneralInvalidArgumentException When the contained view definition is of invalid type.
     *
     * @return void
     */
    protected function build(IMetaModelDataDefinition $container)
    {
        if ($container->hasDefinition(Contao2BackendViewDefinitionInterface::NAME)) {
            $view = $container->getDefinition(Contao2BackendViewDefinitionInterface::NAME);
        } else {
            $view = new Contao2BackendViewDefinition();
            $container->setDefinition(Contao2BackendViewDefinitionInterface::NAME, $view);
        }

        if (!$view instanceof Contao2BackendViewDefinitionInterface) {
            throw new DcGeneralInvalidArgumentException(
                'Configured BackendViewDefinition does not implement Contao2BackendViewDefinitionInterface.'
            );
        }

        $this->container   = $container;
        $this->inputScreen = $inputScreen = $this->viewCombination->getScreen($container->getName());
        if (null === $inputScreen) {
            return;
        }
        $this->addEditMultipleCommand($view);
        $this->parseModelOperations($view);
        $this->container   = null;
        $this->inputScreen = null;

        if ($this->dispatcher->hasListeners(BuildMetaModelOperationsEvent::NAME)) {
            // @codingStandardsIgnoreStart
            @trigger_error(
                'Event "' . BuildMetaModelOperationsEvent::NAME . '" is deprecated and will get removed',
                E_USER_DEPRECATED
            );
            // @codingStandardsIgnoreEnd
            $event = new BuildMetaModelOperationsEvent(
                $this->factory->getMetaModel($container->getName()),
                $container,
                $inputScreen
            );
            $this->dispatcher->dispatch($event::NAME, $event);
        }
    }

    /**
     * Add the select command to the backend view definition.
     *
     * @param Contao2BackendViewDefinitionInterface $view The backend view definition.
     *
     * @return void
     */
    private function addEditMultipleCommand(Contao2BackendViewDefinitionInterface $view)
    {
        $definition = $this->container->getBasicDefinition();
        // No actions allowed. Don't add the select command button.
        if (!$definition->isEditable() && !$definition->isDeletable() && !$definition->isCreatable()) {
            return;
        }

        $commands = $view->getGlobalCommands();
        $command  = new SelectCommand();
        $command
            ->setName('all')
            ->setLabel('MSC.all.0')
            ->setDescription('MSC.all.1');

        $parameters        = $command->getParameters();
        $parameters['act'] = 'select';
        $extra             = $command->getExtra();
        $extra['class']    = 'header_edit_all';

        $commands->addCommand($command);
    }

    /**
     * Parse the defined model scoped operations and populate the definition.
     *
     * @param Contao2BackendViewDefinitionInterface $view The backend view information.
     *
     * @return void
     */
    private function parseModelOperations(Contao2BackendViewDefinitionInterface $view)
    {
        $collection = $view->getModelCommands();

        $scrOffsetAttributes = ['attributes' => 'onclick="Backend.getScrollOffset();"'];
        $this->createCommand($collection, 'edit', ['act' => 'edit'], 'edit.svg');
        $this->createCommand($collection, 'copy', ['act' => ''], 'copy.svg', $scrOffsetAttributes);
        $this->createCommand($collection, 'cut', ['act' => 'paste', 'mode' => 'cut'], 'cut.svg', $scrOffsetAttributes);
        $this->createCommand(
            $collection,
            'delete',
            ['act' => 'delete'],
            'delete.svg',
            [
                'attributes' => sprintf(
                    'onclick="if (!confirm(\'%s\')) return false; Backend.getScrollOffset();"',
                    $this->translator->trans('MSC.deleteConfirm', [], 'contao_default')
                )
            ]
        );
        $this->createCommand($collection, 'show', ['act' => 'show'], 'show.svg');

        if ($this->factory->getMetaModel($this->container->getName())->hasVariants()) {
            $this->createCommand(
                $collection,
                'createvariant',
                ['act' => 'createvariant'],
                'bundles/metamodelscore/images/icons/variants.png'
            );
        }

        // Check if we have some children.
        foreach ($this->viewCombination->getChildrenOf($this->container->getName()) as $tableName => $screen) {
            $metaModel = $this->factory->getMetaModel($tableName);
            $caption   = $this->getChildModelCaption($metaModel, $screen);

            $this->createCommand(
                $collection,
                'edit_' . $tableName,
                ['table' => $tableName],
                $this->iconBuilder->getBackendIcon($screen['meta']['backendicon']),
                [
                    'label'       => $caption[0],
                    'description' => $caption[1],
                    'idparam'     => 'pid'
                ]
            );
        }
    }

    /**
     * Build a command into the the command collection.
     *
     * @param CommandCollectionInterface $collection      The command collection.
     * @param string                     $operationName   The operation name.
     * @param array                      $queryParameters The query parameters for the operation.
     * @param string                     $icon            The icon to use in the backend.
     * @param array                      $extraValues     The extra values for the command.
     *
     * @return void
     */
    private function createCommand(
        CommandCollectionInterface $collection,
        $operationName,
        $queryParameters,
        $icon,
        $extraValues = []
    ) {
        $command    = $this->getCommandInstance($collection, $operationName);
        $parameters = $command->getParameters();
        foreach ($queryParameters as $name => $value) {
            if (!isset($parameters[$name])) {
                $parameters[$name] = $value;
            }
        }

        if (!$command->getLabel()) {
            $command->setLabel($operationName . '.0');
            if (isset($extraValues['label'])) {
                $command->setLabel($extraValues['label']);
            }
        }

        if (!$command->getDescription()) {
            $command->setDescription($operationName . '.1');
            if (isset($extraValues['description'])) {
                $command->setDescription($extraValues['description']);
            }
        }

        $extra         = $command->getExtra();
        $extra['icon'] = $icon;

        foreach ($extraValues as $name => $value) {
            if (!isset($extra[$name])) {
                $extra[$name] = $value;
            }
        }
    }

    /**
     * Retrieve or create a command instance of the given name.
     *
     * @param CommandCollectionInterface $collection    The command collection.
     *
     * @param string                     $operationName The name of the operation.
     *
     * @return CommandInterface
     */
    private function getCommandInstance(CommandCollectionInterface $collection, $operationName)
    {
        if ($collection->hasCommandNamed($operationName)) {
            $command = $collection->getCommandNamed($operationName);
        } else {
            switch ($operationName) {
                case 'cut':
                    $command = new CutCommand();
                    break;

                case 'copy':
                    $command = new CopyCommand();
                    break;

                default:
                    $command = new Command();
            }

            $command->setName($operationName);
            $collection->addCommand($command);
        }

        return $command;
    }

    /**
     * Create the caption text for the child model.
     *
     * @param IMetaModel $metaModel The child model.
     * @param array      $screen    The input screen.
     *
     * @return array
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     * @SuppressWarnings(PHPMD.CamelCaseVariableName)
     */
    private function getChildModelCaption($metaModel, $screen)
    {
        $caption = [
            '',
            sprintf(
                $GLOBALS['TL_LANG']['MSC']['metamodel_edit_as_child']['label'],
                $metaModel->getName()
            )
        ];

        foreach ($screen['label'] as $langCode => $label) {
            if (!empty($label) && $langCode === $GLOBALS['TL_LANGUAGE']) {
                $caption = [
                    $screen['description'][$langCode],
                    $label
                ];
            }
        }

        return $caption;
    }
}


<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @author     Richard Henkenjohann <richardhenkenjohann@googlemail.com>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\CoreBundle\Contao\Hooks;

use Contao\BackendUser;
use Contao\StringUtil;
use MetaModels\ViewCombination\ViewCombination;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Translation\TranslatorInterface;

/**
 * This registers the backend navigation of MetaModels.
 */
class RegisterBackendNavigation
{
    /**
     * The request stack.
     *
     * @var RequestStack
     */
    protected $requestStack;

    /**
     * The URL generator.
     *
     * @var UrlGeneratorInterface
     */
    private $urlGenerator;

    /**
     * The translator in use.
     *
     * @var TranslatorInterface
     */
    private $translator;

    /**
     * The translator in use.
     *
     * @var ViewCombination
     */
    private $viewCombination;

    /**
     * The token storage.
     *
     * @var TokenStorage
     */
    private $tokenStorage;

    /**
     * Create a new instance.
     *
     * @param TranslatorInterface   $translator      The translator.
     * @param RequestStack          $requestStack    The request stack.
     * @param UrlGeneratorInterface $urlGenerator    The url generator.
     * @param ViewCombination       $viewCombination The view combination.
     * @param TokenStorage          $tokenStorage    The token storage.
     */
    public function __construct(
        TranslatorInterface $translator,
        RequestStack $requestStack,
        UrlGeneratorInterface $urlGenerator,
        ViewCombination $viewCombination,
        TokenStorage $tokenStorage
    ) {
        $this->requestStack    = $requestStack;
        $this->urlGenerator    = $urlGenerator;
        $this->translator      = $translator;
        $this->viewCombination = $viewCombination;
        $this->tokenStorage    = $tokenStorage;
    }

    /**
     * Hook function
     *
     * @param array $modules The backend navigation.
     *
     * @return array
     */
    public function onGetUserNavigation($modules)
    {
        if (null === $request = $this->requestStack->getCurrentRequest()) {
            return $modules;
        }
        $this->addBackendCss();

        if (null !== ($user = $this->tokenStorage->getToken())) {
            $userRights = $this->extractUserRights($user);
        }
        $isAdmin = \in_array('ROLE_ADMIN', array_map(function (Role $role) {
            return $role->getRole();
        }, $user->getRoles()), true);

        if ($isAdmin || isset($userRights['support_metamodels'])) {
            $this->addMenu(
                $modules,
                'metamodels',
                'support_screen',
                [
                    'label' => $this->translator->trans('MOD.support_metamodels.0', [], 'contao_modules'),
                    'title' => $this->translator->trans('MOD.support_metamodels.1', [], 'contao_modules'),
                    'route' => 'metamodels.support_screen',
                    'param' => [],
                ],
                $request
            );
        }

        $locale = $request->getLocale();
        foreach ($this->viewCombination->getStandalone() as $metaModelName => $screen) {
            $moduleName = 'metamodel_' . $metaModelName;
            if (!$isAdmin && !isset($userRights[$moduleName])) {
                continue;
            }
            $this->addMenu(
                $modules,
                $screen['meta']['backendsection'],
                $moduleName,
                [
                    'label' => $this->extractLanguageValue($screen['label'], $locale),
                    'title' => $this->extractLanguageValue($screen['description'], $locale),
                    'route' => 'contao_backend',
                    'param' => ['do' => 'metamodel_' . $metaModelName],
                ],
                $request
            );
        }

        return $modules;
    }

    /**
     * Extract the permissions from the Contao backend user.
     *
     * @param TokenInterface $token The token.
     *
     * @return array
     */
    private function extractUserRights(TokenInterface $token)
    {
        $beUser = $token->getUser();
        if (!($beUser instanceof BackendUser)) {
            return [];
        }

        $allowedModules = $beUser->modules;
        switch (true) {
            case \is_string($allowedModules):
                $allowedModules = unserialize($allowedModules, ['allowed_classes' => false]);
                break;
            case null === $allowedModules:
                $allowedModules = [];
                break;
            default:
        }
        return array_flip($allowedModules);
    }

    /**
     * Extract the language value.
     *
     * @param string[] $values The values.
     *
     * @param string   $locale The current locale.
     *
     * @return string
     */
    private function extractLanguageValue($values, $locale)
    {
        if (isset($values[$locale])) {
            return $values[$locale];
        }

        return $values[''];
    }

    /**
     * Build a menu section.
     *
     * @param string  $groupName The group name.
     * @param Request $request   The current request.
     *
     * @return array
     */
    private function buildBackendMenuSection($groupName, Request $request)
    {
        $strRefererId = $request->attributes->get('_contao_referer_id');

        $label = $this->translator->trans('MOD.' . $groupName, [], 'contao_modules');

        if (\is_array($label)) {
            $label = $label[0];
        }

        return [
            'class'   => ' node-expanded',
            'title'   => StringUtil::specialchars($this->translator->trans('MSC.collapseNode', [], 'contao_modules')),
            'label'   => $label,
            'href'    => $this->urlGenerator->generate(
                'contao_backend',
                ['do' => $request->get('do'), 'mtg' => $groupName, 'ref' => $strRefererId]
            ),
            'ajaxUrl' => $this->urlGenerator->generate('contao_backend'),
            // backwards compatibility with e.g. EasyThemes
            'icon'    => 'modPlus.gif',
            'modules'  => [],
        ];
    }

    /**
     * Add a module to the modules list.
     *
     * @param array   $modules The modules list.
     * @param string  $section The section to add to.
     * @param string  $name    The name of the module.
     * @param array   $module  The module.
     * @param Request $request The current request.
     *
     * @return void
     */
    private function addMenu(&$modules, $section, $name, $module, Request $request)
    {
        if (!isset($modules[$section])) {
            $modules[$section] = $this->buildBackendMenuSection($section, $request);
        }

        $active = $this->isActive($module['route'], $module['param'], $request);
        $class  = 'navigation ' . $name;
        if (isset($module['class'])) {
            $class .= ' ' . $module['class'];
        }
        if ($active) {
            $class .= ' active';
        }
        if ($request->query->has('ref')) {
            $module['param']['ref'] = $request->query->get('ref');
        }

        $modules[$section]['modules'][$name] = [
            'label'    => $module['label'],
            'title'    => $module['title'],
            'class'    => $class,
            'isActive' => $active,
            'href'     => $this->urlGenerator->generate($module['route'], $module['param']),
        ];
    }

    /**
     * Determine if is active.
     *
     * @param string  $route   The route name.
     * @param array   $params  The route parameters.
     * @param Request $request The current request.
     *
     * @return bool
     */
    private function isActive($route, $params, Request $request)
    {
        if ('/contao' === $request->getPathInfo()
            || !($request->attributes->get('_route') === $route)
        ) {
            return false;
        }

        $attributes = $request->attributes->get('_route_params');
        $query      = $request->query;
        foreach ($params as $param => $value) {
            if (isset($attributes[$param]) && ($value !== $request->attributes['_route_params'][$param])) {
                return false;
            }
            if ($query->has($param) && ($value !== $query->get($param))) {
                return false;
            }
        }

        return true;
    }

    /**
     * Add the CSS files for the backend.
     *
     * @return void
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     * @SuppressWarnings(PHPMD.CamelCaseVariableName)
     */
    private function addBackendCss(): void
    {
        // BE group icon.
        $GLOBALS['TL_CSS']['metamodels'] = 'bundles/metamodelscore/css/be_logo_svg.css';
    }
}


<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @author     Richard Henkenjohann <richardhenkenjohann@googlemail.com>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\ViewCombination;

use Contao\BackendUser;
use Contao\FrontendUser;
use Doctrine\Common\Cache\Cache;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

/**
 * This class serves as central information endpoint for view combinations.
 */
class ViewCombination
{
    /**
     * The cache.
     *
     * @var Cache
     */
    private $cache;

    /**
     * The token storage.
     *
     * @var TokenStorageInterface
     */
    private $tokenStorage;

    /**
     * The combination builder.
     *
     * @var ViewCombinationBuilder
     */
    private $builder;

    /**
     * The input screen information builder.
     *
     * @var InputScreenInformationBuilder
     */
    private $inputScreens;

    /**
     * Create a new instance.
     *
     * @param Cache                         $cache        The cache.
     * @param TokenStorageInterface         $tokenStorage The token storage.
     * @param ViewCombinationBuilder        $builder      The combination builder.
     * @param InputScreenInformationBuilder $inputScreens The input screen information builder.
     */
    public function __construct(
        Cache $cache,
        TokenStorageInterface $tokenStorage,
        ViewCombinationBuilder $builder,
        InputScreenInformationBuilder $inputScreens
    ) {
        $this->cache        = $cache;
        $this->tokenStorage = $tokenStorage;
        $this->builder      = $builder;
        $this->inputScreens = $inputScreens;
    }

    /**
     * Obtain the combinations for the current user.
     *
     * @return array|null
     */
    public function getCombinations()
    {
        $user = $this->getUser();

        switch (true) {
            case ($user instanceof BackendUser):
                $mode = 'be';
                // Try to get the group(s)
                // there might be a NULL in there as BE admins have no groups and
                // user might have one but it is not mandatory.
                // I would prefer a default group for both, fe and be groups.
                $groups = $user->groups;
                // Special case in combinations, admins have the implicit group id -1.
                if ($user->admin) {
                    $groups[] = -1;
                }

                break;
            case ($user instanceof FrontendUser):
                $mode   = 'fe';
                $groups = $user->groups;
                // Special case in combinations, anonymous frontend users have the implicit group id -1.
                if (!$this->getUser()->id) {
                    $groups = [-1];
                }

                break;
            default:
                // Default handled as frontend anonymous.
                $mode   = 'fe';
                $groups = [-1];
        }

        $groups = array_filter($groups);

        if ($this->cache->contains($cacheKey = 'combinations_' . $mode . '_' . implode(',', $groups))) {
            return $this->cache->fetch($cacheKey);
        }

        $combinations = $this->builder->getCombinationsForUser($groups, $mode);

        $this->cache->save($cacheKey, $combinations);

        return $combinations;
    }

    /**
     * Retrieve a combination for a table.
     *
     * @param string $tableName The table name.
     *
     * @return array|null
     */
    public function getCombination($tableName)
    {
        $combinations = $this->getCombinations();
        if (isset($combinations['byName'][$tableName])) {
            return $combinations['byName'][$tableName];
        }

        return null;
    }

    /**
     * Obtain stand alone input screens.
     *
     * @return array
     */
    public function getStandalone()
    {
        $inputScreens = array_filter($this->getInputScreens(), function ($inputScreen) {
            return $inputScreen['meta']['rendertype'] === 'standalone';
        });

        return $inputScreens;
    }

    /**
     * Obtain parented input screens.
     *
     * @return array
     */
    public function getParented()
    {
        $inputScreens = array_filter($this->getInputScreens(), function ($inputScreen) {
            return $inputScreen['meta']['rendertype'] === 'ctable';
        });

        return $inputScreens;
    }

    /**
     * Obtain child input screens of the passed parent.
     *
     * @param string $parentTable The parent table to assemble the children of.
     *
     * @return array
     */
    public function getChildrenOf($parentTable)
    {
        $inputScreens = array_filter($this->getInputScreens(), function ($inputScreen) use ($parentTable) {
            return ($inputScreen['meta']['rendertype'] === 'ctable')
                   && ($inputScreen['meta']['ptable'] === $parentTable);
        });

        return $inputScreens;
    }

    /**
     * Obtain parented input screens.
     *
     * @param string $tableName The table name.
     *
     * @return array|null
     */
    public function getScreen($tableName)
    {
        $inputScreens = $this->getInputScreens();
        if (isset($inputScreens[$tableName])) {
            return $inputScreens[$tableName];
        }

        return null;
    }

    /**
     * Retrieve the input screens.
     *
     * @return array
     */
    private function getInputScreens()
    {
        $combinations = $this->getCombinations();
        if (null === $combinations) {
            return [];
        }

        $screenIds = array_map(function ($combination) {
            return $combination['dca_id'];
        }, $combinations['byName']);

        if ($this->cache->contains($cacheKey = 'screens_' . implode(',', $screenIds))) {
            return $this->cache->fetch($cacheKey);
        }

        $screens = $this->inputScreens->fetchInputScreens($screenIds);
        $this->cache->save($cacheKey, $screens);

        return $screens;
    }

    /**
     * The user.
     *
     * @return mixed
     */
    private function getUser()
    {
        if (null === $token = $this->tokenStorage->getToken()) {
            return null;
        }
        return $token->getUser();
    }
}


<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     David Maack <david.maack@arcor.de>
 * @author     Stefan Heimes <stefan_heimes@hotmail.com>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\Filter\Setting;

use MetaModels\FrontendIntegration\FrontendFilterOptions;
use MetaModels\IItem;
use MetaModels\Filter\IFilter;
use MetaModels\IMetaModel;
use MetaModels\Render\Setting\ICollection as IRenderSettings;

/**
 * This is the ICollection reference implementation.
 */
class Collection implements ICollection
{
    /**
     * The additional meta data for this filter setting collection.
     *
     * @var array
     */
    protected $arrData = array();

    /**
     * The filter settings contained.
     *
     * @var ISimple[]
     */
    protected $arrSettings = array();

    /**
     * The attached MetaModel.
     *
     * @var IMetaModel
     */
    protected $metaModel;

    /**
     * Create a new instance.
     *
     * @param array $arrData The meta data for this filter setting collection.
     */
    public function __construct($arrData)
    {
        $this->arrData = $arrData;
    }

    /**
     * {@inheritdoc}
     */
    public function get($key)
    {
        return isset($this->arrData[$key]) ? $this->arrData[$key] : null;
    }

    /**
     * Set the MetaModel.
     *
     * @param IMetaModel $metaModel The MetaModel instance.
     *
     * @return Collection
     */
    public function setMetaModel($metaModel)
    {
        $this->metaModel = $metaModel;

        return $this;
    }

    /**
     * Retrieve the MetaModel this filter belongs to.
     *
     * @return IMetaModel
     *
     * @throws \RuntimeException When the MetaModel can not be determined.
     */
    public function getMetaModel()
    {
        if ($this->metaModel) {
            return $this->metaModel;
        }

        throw new \RuntimeException(
            sprintf('Error: Filter setting %d not attached to a MetaModel', $this->arrData['id'])
        );
    }

    /**
     * Add a setting to the collection.
     *
     * @param ISimple|IWithChildren $setting The setting to add.
     *
     * @return Collection
     */
    public function addSetting($setting)
    {
        $this->arrSettings[] = $setting;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function addRules(IFilter $objFilter, $arrFilterUrl, $arrIgnoredFilter = array())
    {
        foreach ($this->arrSettings as $objSetting) {
            // If the setting is on the ignore list skip it.
            if (in_array($objSetting->get('id'), $arrIgnoredFilter)) {
                continue;
            }

            $objSetting->prepareRules($objFilter, $arrFilterUrl);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function generateFilterUrlFrom(IItem $objItem, IRenderSettings $objRenderSetting)
    {
        $arrFilterUrl = array();
        foreach ($this->arrSettings as $objSetting) {
            $arrFilterUrl = array_merge($arrFilterUrl, $objSetting->generateFilterUrlFrom($objItem, $objRenderSetting));
        }
        return $arrFilterUrl;
    }

    /**
     * {@inheritdoc}
     */
    public function getParameters()
    {
        $arrParams = array();
        foreach ($this->arrSettings as $objSetting) {
            $arrParams = array_merge($arrParams, $objSetting->getParameters());
        }
        return $arrParams;
    }

    /**
     * {@inheritdoc}
     */
    public function getParameterDCA()
    {
        $arrParams = array();
        foreach ($this->arrSettings as $objSetting) {
            $arrParams = array_merge($arrParams, $objSetting->getParameterDCA());
        }
        return $arrParams;
    }

    /**
     * {@inheritdoc}
     */
    public function getParameterFilterNames()
    {
        $arrParams = array();

        foreach ($this->arrSettings as $objSetting) {
            $arrParams = array_merge($arrParams, $objSetting->getParameterFilterNames());
        }
        return $arrParams;
    }

    /**
     * {@inheritdoc}
     */
    public function getParameterFilterWidgets(
        $arrFilterUrl,
        $arrJumpTo,
        FrontendFilterOptions $objFrontendFilterOptions
    ) {
        $arrParams = array();

        // Get the id with all enabled filter.
        $objFilter = $this->getMetaModel()->getEmptyFilter();
        $this->addRules($objFilter, $arrFilterUrl);

        $arrBaseIds = $objFilter->getMatchingIds();

        foreach ($this->arrSettings as $objSetting) {
            if ($objSetting->get('skipfilteroptions')) {
                $objFilter = $this->getMetaModel()->getEmptyFilter();
                $this->addRules($objFilter, $arrFilterUrl, array($objSetting->get('id')));
                $arrIds = $objFilter->getMatchingIds();
            } else {
                $arrIds = $arrBaseIds;
            }

            $arrParams = array_merge(
                $arrParams,
                $objSetting->getParameterFilterWidgets($arrIds, $arrFilterUrl, $arrJumpTo, $objFrontendFilterOptions)
            );
        }

        return $arrParams;
    }

    /**
     * {@inheritdoc}
     */
    public function getReferencedAttributes()
    {
        $arrAttributes = array();

        foreach ($this->arrSettings as $objSetting) {
            $arrAttributes = array_merge($arrAttributes, $objSetting->getReferencedAttributes());
        }

        return $arrAttributes;
    }
}


<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\CoreBundle\EventListener\DcGeneral\Table\DcaSettingCondition;

use ContaoCommunityAlliance\DcGeneral\Clipboard\Filter;
use ContaoCommunityAlliance\DcGeneral\Clipboard\ItemInterface;
use ContaoCommunityAlliance\DcGeneral\Contao\View\Contao2BackendView\Event\GetPasteButtonEvent;
use ContaoCommunityAlliance\DcGeneral\Controller\ModelCollector;
use ContaoCommunityAlliance\DcGeneral\Controller\RelationshipManager;
use ContaoCommunityAlliance\DcGeneral\Data\ModelId;
use ContaoCommunityAlliance\DcGeneral\Data\ModelInterface;

/**
 * This handles the type options for conditions.
 */
class PasteButtonListener extends AbstractConditionFactoryUsingListener
{
    /**
     * Generate the paste button.
     *
     * @param GetPasteButtonEvent $event The event.
     *
     * @return void
     */
    public function handle(GetPasteButtonEvent $event)
    {
        if (!$this->wantToHandle($event)) {
            return;
        }

        $environment = $event->getEnvironment();
        $model       = $event->getModel();
        $clipboard   = $environment->getClipboard();
        // Disable all buttons if there is a circular reference.
        if ($clipboard->fetch(
            Filter::create()->andActionIs(ItemInterface::CUT)->andModelIs(ModelId::fromModel($model))
        )) {
            $event
                ->setPasteAfterDisabled(true)
                ->setPasteIntoDisabled(true);

            return;
        }

        $typeName = $model->getProperty('type');
        // If setting does not support children, omit them.
        if ($model->getId() && !$this->conditionFactory->supportsNesting($typeName)) {
            $event->setPasteIntoDisabled(true);
            return;
        }

        $collector = new ModelCollector($environment);
        if (!$this->acceptsAnotherChild($model, $collector)) {
            $event->setPasteIntoDisabled(true);
        }

        $definition    = $environment->getDataDefinition();
        $mode          = $definition->getBasicDefinition()->getMode();
        $relationships = new RelationshipManager($definition->getModelRelationshipDefinition(), $mode);
        if (!$relationships->isRoot($model)
            && ($parent = $collector->searchParentOf($model))
            && !$this->acceptsAnotherChild($parent, $collector)) {
            $event->setPasteAfterDisabled(true);
        }
    }

    /**
     * Test if a model accepts another child.
     *
     * @param ModelInterface $model     The model that shall be checked.
     * @param ModelCollector $collector The collector to use.
     *
     * @return bool
     */
    public function acceptsAnotherChild(ModelInterface $model, ModelCollector $collector)
    {
        $conditionType = $model->getProperty('type');
        if (!$this->conditionFactory->supportsNesting($conditionType)) {
            return false;
        }
        if (-1 === ($max = $this->conditionFactory->maxChildren($conditionType))) {
            return true;
        }

        return \count($collector->collectDirectChildrenOf($model)) < $max;
    }
}


<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\CoreBundle\EventListener\DcGeneral\Table\DcaSetting;

use Contao\StringUtil;
use ContaoCommunityAlliance\DcGeneral\Contao\View\Contao2BackendView\Event\BuildWidgetEvent;
use ContaoCommunityAlliance\DcGeneral\Contao\View\Contao2BackendView\Event\DecodePropertyValueForWidgetEvent;
use ContaoCommunityAlliance\DcGeneral\Contao\View\Contao2BackendView\Event\EncodePropertyValueFromWidgetEvent;
use ContaoCommunityAlliance\DcGeneral\DataDefinition\Definition\Properties\PropertyInterface;
use ContaoCommunityAlliance\DcGeneral\Event\AbstractEnvironmentAwareEvent;
use MetaModels\Dca\Helper;

/**
 * This handles the serialization and deserialization as well as the building of the title widget.
 */
class LegendTitleListener extends AbstractListener
{
    /**
     * Decode the title value.
     *
     * @param DecodePropertyValueForWidgetEvent $event The event.
     *
     * @return void
     */
    public function decodeValue(DecodePropertyValueForWidgetEvent $event)
    {
        if (!$this->wantToHandle($event)) {
            return;
        }

        $metaModel = $this->getMetaModelFromModel($event->getModel());

        $values = Helper::decodeLangArray($event->getValue(), $metaModel);

        $event->setValue(unserialize($values));
    }

    /**
     * Encode the title value.
     *
     * @param EncodePropertyValueFromWidgetEvent $event The event.
     *
     * @return void
     */
    public function encodeValue(EncodePropertyValueFromWidgetEvent $event)
    {
        if (!$this->wantToHandle($event)) {
            return;
        }

        $metaModel = $this->getMetaModelFromModel($event->getModel());

        $values = Helper::encodeLangArray($event->getValue(), $metaModel);

        $event->setValue($values);
    }

    /**
     * Generate the widget.
     *
     * @param BuildWidgetEvent $event The event.
     *
     * @return void
     */
    public function buildWidget(BuildWidgetEvent $event)
    {
        if (!$this->wantToHandle($event)) {
            return;
        }

        $metaModel = $this->getMetaModelFromModel($event->getModel());

        Helper::prepareLanguageAwareWidget(
            $event->getEnvironment(),
            $event->getProperty(),
            $metaModel,
            $event->getEnvironment()->getTranslator()->translate('name_langcode', 'tl_metamodel_dcasetting'),
            $event->getEnvironment()->getTranslator()->translate('name_value', 'tl_metamodel_dcasetting'),
            false,
            StringUtil::deserialize($event->getModel()->getProperty('legendtitle'), true)
        );
    }

    /**
     * {@inheritDoc}
     */
    protected function wantToHandle(AbstractEnvironmentAwareEvent $event)
    {
        if (!parent::wantToHandle($event)) {
            return false;
        }
        if (method_exists($event, 'getPropertyName') && ('legendtitle' !== $event->getPropertyName())) {
            return false;
        }
        if (method_exists($event, 'getProperty')) {
            $property = $event->getProperty();
            if ($property instanceof PropertyInterface) {
                $property = $property->getName();
            }
            if ('legendtitle' !== $property) {
                return false;
            }
        }

        return true;
    }
}


<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Stefan Heimes <stefan_heimes@hotmail.com>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @author     David Molineus <david.molineus@netzmacht.de>
 * @author     Richard Henkenjohann <richardhenkenjohann@googlemail.com>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\FrontendIntegration;

use Contao\StringUtil;
use Contao\Input;
use Contao\System;
use Doctrine\DBAL\Connection;
use MetaModels\Filter\Rules\StaticIdList;
use MetaModels\IMetaModel;
use MetaModels\IMetaModelsServiceContainer;
use MetaModels\ItemList;

/**
 * This class handles the replacement of all MetaModels insert tags.
 *
 * @codingStandardsIgnoreStart
 * Available insert tags:
 *
 * -- Total Count --
 * mm::total::mod::[id]
 * mm::total::ce::[id]
 *
 * -- Item --
 * mm::item::[MM Name|ID]::[Item ID|ID,ID,ID]::[ID render setting](::[Output raw|text|html|..])
 * mm::detail::[MM Name|ID]::[Item ID]::[ID render setting](::[Output raw|text|html|..])
 *
 * -- Attribute --
 * mm::attribute::[MM Name|ID]::[Item ID]::[Attribute Name|ID](::[Output raw|text|html|..])
 *
 * -- JumpTo --
 * mm::jumpTo::[MM Name|ID]::[Item ID]::[ID render setting](::[Parameter (Default:url)|label|page|params.attname])
 *
 * @codingStandardsIgnoreEnd
 */
class InsertTags
{
    /**
     * Database connection.
     *
     * @var Connection
     */
    private $connection;

    /**
     * FrontendFilter constructor.
     *
     * @param Connection $connection Database connection.
     */
    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }

    /**
     * Retrieve the service container.
     *
     * @return IMetaModelsServiceContainer
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     * @SuppressWarnings(PHPMD.CamelCaseVariableName)
     */
    public function getServiceContainer()
    {
        return $GLOBALS['container']['metamodels-service-container'];
    }

    /**
     * Evaluate an insert tag.
     *
     * @param string $strTag The tag to evaluate.
     *
     * @return bool|string
     */
    public function replaceTags($strTag)
    {
        $arrElements = explode('::', $strTag);

        // Check if we have the mm tags.
        if ($arrElements[0] != 'mm') {
            return false;
        }

        try {
            // Call the fitting function.
            switch ($arrElements[1]) {
                // Count for mod or ce elements.
                case 'total':
                    return $this->getCount($arrElements[2], $arrElements[3]);

                // Get value from an attribute.
                case 'attribute':
                    return $this->getAttribute($arrElements[2], $arrElements[3], $arrElements[4], $arrElements[5]);

                // Get item.
                case 'item':
                    return $this->getItem($arrElements[2], $arrElements[3], $arrElements[4], $arrElements[5]);

                case 'jumpTo':
                    return $this->jumpTo($arrElements[2], $arrElements[3], $arrElements[4], $arrElements[5]);

                default:
            }
        } catch (\Exception $exc) {
            System::log('Error by replace tags: ' . $exc->getMessage(), __CLASS__ . ' | ' . __FUNCTION__, TL_ERROR);
        }

        return false;
    }

    /**
     * Get the jumpTo for a chosen value.
     *
     * @param string|int $mixMetaModel       ID or name of MetaModels.
     *
     * @param int        $mixDataId          ID of the data row.
     *
     * @param int        $intIdRenderSetting ID of render setting.
     *
     * @param string     $strParam           Name of parameter - Default:url|label|page|params.[attrname].
     *
     * @return boolean|string Return false when nothing was found for the requested value.
     */
    protected function jumpTo($mixMetaModel, $mixDataId, $intIdRenderSetting, $strParam = 'url')
    {
        // Set the param to url if empty.
        if (empty($strParam)) {
            $strParam = 'url';
        }

        // Get the MetaModel. Return if we can not find one.
        $objMetaModel = $this->loadMetaModel($mixMetaModel);
        if ($objMetaModel == null) {
            return false;
        }

        // Get the render setting.
        $objRenderSettings = $this
            ->getServiceContainer()
            ->getRenderSettingFactory()
            ->createCollection($objMetaModel, $intIdRenderSetting);
        if ($objRenderSettings == null) {
            return false;
        }

        // Get the data row.
        $objItem = $objMetaModel->findById($mixDataId);
        if ($objItem == null) {
            return false;
        }

        // Render the item and check if we have a jump to.
        $arrRenderedItem = $objItem->parseValue('text', $objRenderSettings);
        if (!isset($arrRenderedItem['jumpTo'])) {
            return false;
        }

        // Check if someone want the sub params.
        if (stripos($strParam, 'params.') !== false) {
            $mixAttName = StringUtil::trimsplit('.', $strParam);
            $mixAttName = array_pop($mixAttName);

            if (isset($arrRenderedItem['jumpTo']['params'][$mixAttName])) {
                return $arrRenderedItem['jumpTo']['params'][$mixAttName];
            }
        } elseif (isset($arrRenderedItem['jumpTo'][$strParam])) {
            // Else just return the ask param.
            return $arrRenderedItem['jumpTo'][$strParam];
        }

        // Nothing hit the output. Return false.
        return false;
    }

    /**
     * Get an item.
     *
     * @param string|int $metaModelIdOrName  ID or name of MetaModels.
     *
     * @param string|int $mixDataId          ID of the data row.
     *
     * @param int        $intIdRenderSetting ID of render setting.
     *
     * @param string     $strOutput          Name of output. Default:null (fallback to htmlfynf)|text|html5|xhtml|...
     *
     * @return boolean|string Return false when nothing was found or return the value.
     */
    protected function getItem($metaModelIdOrName, $mixDataId, $intIdRenderSetting, $strOutput = null)
    {
        // Get the MetaModel. Return if we can not find one.
        $objMetaModel = $this->loadMetaModel($metaModelIdOrName);
        if ($objMetaModel == null) {
            return false;
        }

        // Set output to default if not set.
        if (empty($strOutput)) {
            $strOutput = 'html5';
        }

        $objMetaModelList = new ItemList();
        $objMetaModelList
            ->setServiceContainer($this->getServiceContainer())
            ->setMetaModel($objMetaModel->get('id'), $intIdRenderSetting)
            ->overrideOutputFormat($strOutput);

        // Handle a set of ids.
        $arrIds = StringUtil::trimsplit(',', $mixDataId);

        // Check each id if published.
        foreach ($arrIds as $intKey => $intId) {
            if (!$this->isPublishedItem($objMetaModel, $intId)) {
                unset($arrIds[$intKey]);
            }
        }

        // Render an empty insert tag rather than displaying a list with an empty.
        // result information. do not return false here because the insert tag itself is correct.
        if (count($arrIds) < 1) {
            return '';
        }

        $objMetaModelList->addFilterRule(new StaticIdList($arrIds));
        return $objMetaModelList->render(false, $this);
    }

    /**
     * Get from MM X the item with the id Y and parse the attribute Z and return it.
     *
     * @param string|int $metaModelIdOrName ID or name of MetaModels.
     *
     * @param int        $intDataId         ID of the data row.
     *
     * @param string     $strAttributeName  Name of the attribute.
     *
     * @param string     $strOutput         Name of output. Default:raw|text|html5|xhtml|...
     *
     * @return boolean|string Return false when nothing was found or return the value.
     *
     * @throws \RuntimeException If $intDataId does not provide an existingMetaModel ID.
     */
    protected function getAttribute($metaModelIdOrName, $intDataId, $strAttributeName, $strOutput = 'raw')
    {
        // Get the MM.
        $objMM = $this->loadMetaModel($metaModelIdOrName);
        if (null === $objMM) {
            return false;
        }

        // Set output to default if not set.
        if (empty($strOutput)) {
            $strOutput = 'raw';
        }

        // Get item.
        $objMetaModelItem = $objMM->findById($intDataId);
        if (null === $objMetaModelItem) {
            throw new \RuntimeException('MetaModel item not found: ' . $intDataId);
        }

        // Parse attribute.
        $arrAttr = $objMetaModelItem->parseAttribute($strAttributeName);

        return $arrAttr[$strOutput];
    }

    /**
     * Get count from a module or content element of a mm.
     *
     * @param string $strType Type of element like mod or ce.
     *
     * @param int    $intID   ID of content element or module.
     *
     * @return boolean|string Return false when nothing was found or the count value.
     */
    protected function getCount($strType, $intID)
    {
        switch ($strType) {
            // From module, can be a MetaModel list or filter.
            case 'mod':
                $objMetaModelResult = $this->getMetaModelDataFrom('tl_module', $intID);
                break;

            // From content element, can be a MetaModel list or filter.
            case 'ce':
                $objMetaModelResult = $this->getMetaModelDataFrom('tl_content', $intID);
                break;

            // Unknown element type.
            default:
                return false;
        }

        // Check if we have data.
        if ($objMetaModelResult != null) {
            return $this->getCountFor($objMetaModelResult->metamodel, $objMetaModelResult->metamodel_filtering);
        }

        return false;
    }

    /**
     * Try to load the MetaModel by id or name.
     *
     * @param mixed $nameOrId Name or id of the MetaModel.
     *
     * @return IMetaModel|null
     */
    protected function loadMetaModel($nameOrId)
    {
        if (is_numeric($nameOrId)) {
            // ID.
            $tableName = $this->getServiceContainer()->getFactory()->translateIdToMetaModelName($nameOrId);
        } elseif (is_string($nameOrId)) {
            // Name.
            $tableName = $nameOrId;
        }

        if (isset($tableName)) {
            return $this->getServiceContainer()->getFactory()->getMetaModel($tableName);
        }

        // Unknown.
        return null;
    }

    /**
     * Get the MetaModel id and the filter id.
     *
     * @param string $strTable Name of table.
     *
     * @param int    $intID    ID of the filter.
     *
     * @return null|\stdClass Returns null when nothing was found or a \Database\Result with the chosen information.
     *
     * @throws \Doctrine\DBAL\DBALException When an database error occur.
     */
    protected function getMetaModelDataFrom($strTable, $intID)
    {
        // Check if we know the table.
        if (!$this->connection->getSchemaManager()->tablesExist([$strTable])) {
            return null;
        }

        // Get all information form table or return null if we have no data.
        $statement = $this->connection
            ->prepare('SELECT metamodel, metamodel_filtering FROM ' . $strTable . ' WHERE id=? LIMIT 0,1');

        $statement->bindValue(1, $intID);
        $statement->execute();

        // Check if we have some data.
        if ($statement->rowCount() < 1) {
            return null;
        }

        return $statement->fetch(\PDO::FETCH_OBJ);
    }

    /**
     * Get count form one MM for chosen filter.
     *
     * @param int $intMetaModelId ID of the metamodels.
     *
     * @param int $intFilterId    ID of the filter.
     *
     * @return boolean|int False for no data or integer for the count result.
     */
    protected function getCountFor($intMetaModelId, $intFilterId)
    {
        $metaModel = $this->loadMetaModel($intMetaModelId);
        if ($metaModel == null) {
            return false;
        }

        $objFilter = $metaModel->getEmptyFilter();
        if ($intFilterId) {
            $collection = $this->getServiceContainer()->getFilterFactory()->createCollection($intFilterId);
            $values     = [];

            foreach ($collection->getParameters() as $key) {
                $values[$key] = Input::get($key);
            }

            $collection->addRules($objFilter, $values);
        }

        return $metaModel->getCount($objFilter);
    }

    /**
     * Check if the item is published.
     *
     * @param IMetaModel $objMetaModel Current metamodels.
     * @param int        $intItemId    Id of the item.
     *
     * @return boolean True => Published | False => Not published
     *
     * @throws \Doctrine\DBAL\DBALException When a database error occur.
     */
    protected function isPublishedItem($objMetaModel, $intItemId)
    {
        // Check publish state of an item.
        $statement = $this->connection
            ->prepare('SELECT colname FROM tl_metamodel_attribute WHERE pid=? AND check_publish=1 LIMIT 0,1');

        $statement->bindValue(1, $objMetaModel->get('id'));
        $statement->execute();

        if ($statement->rowCount() > 0) {
            $objAttrCheckPublish = $statement->fetch(\PDO::FETCH_OBJ);
            $objItem             = $objMetaModel->findById($intItemId);

            if (!$objItem->get($objAttrCheckPublish->colname)) {
                return false;
            }
        }

        return true;
    }
}


<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @author     David Molineus <david.molineus@netzmacht.de>
 * @author     Cliff Parnitzky <github@cliff-parnitzky.de>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\Events;

use Contao\System;
use Doctrine\DBAL\Connection;
use MetaModels\Attribute\Events\CollectMetaModelAttributeInformationEvent;
use MetaModels\IMetaModel;
use MetaModels\IMetaModelsServiceContainer;
use MetaModels\MetaModel;
use MetaModels\MetaModelsServiceContainer;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * This is the information retriever database backend.
 */
class DatabaseBackedListener
{
    /**
     * The database connection.
     *
     * @var Connection
     */
    private $database;

    /**
     * The event dispatcher.
     *
     * @var EventDispatcherInterface
     */
    private $dispatcher;

    /**
     * All MetaModel instances created via this listener.
     *
     * Association: id => object
     *
     * @var IMetaModel[]
     */
    private $instancesById = [];

    /**
     * All MetaModel instances.
     *
     * Association: tableName => object
     *
     * @var IMetaModel[]
     */
    private $instancesByTable = [];

    /**
     * The table names.
     *
     * @var string[]
     */
    private $tableNames = [];

    /**
     * Flag if the table names have already been collected.
     *
     * @var bool
     */
    private $tableNamesCollected = false;

    /**
     * All attribute information.
     *
     * @var array[]
     */
    private $attributeInformation = [];

    /**
     * The system columns of MetaModels.
     *
     * @var string[]
     */
    private $systemColumns;

    /**
     * Create a new instance.
     *
     * @param Connection               $database      The database connection.
     * @param EventDispatcherInterface $dispatcher    The event dispatcher.
     * @param string[]                 $systemColumns The system columns.
     */
    public function __construct(Connection $database, EventDispatcherInterface $dispatcher, array $systemColumns)
    {
        $this->database      = $database;
        $this->dispatcher    = $dispatcher;
        $this->systemColumns = $systemColumns;
    }

    /**
     * Retrieve the service container.
     *
     * @return IMetaModelsServiceContainer
     *
     * @deprecated The service container is deprecated and should not be used anymore.
     */
    public function getServiceContainer()
    {
        return System::getContainer()->get(MetaModelsServiceContainer::class);
    }

    /**
     * Translate the id of a MetaModel to the correct name of the MetaModel.
     *
     * @param GetMetaModelNameFromIdEvent $event The event.
     *
     * @return void
     */
    public function getMetaModelNameFromId(GetMetaModelNameFromIdEvent $event)
    {
        $metaModelId = $event->getMetaModelId();
        if (array_key_exists($metaModelId, $this->instancesById)) {
            $event->setMetaModelName($this->instancesById[$metaModelId]->getTableName());

            return;
        }

        if (isset($this->tableNames[$metaModelId])) {
            $event->setMetaModelName($this->tableNames[$metaModelId]);

            return;
        }

        if (!$this->tableNamesCollected) {
            $table = $this
                ->database
                ->createQueryBuilder()
                ->select('*')
                ->from('tl_metamodel')
                ->where('id=:id')
                ->setParameter('id', $metaModelId)
                ->setMaxResults(1)
                ->execute()
                ->fetch(\PDO::FETCH_ASSOC);

            if ($table) {
                $this->tableNames[$metaModelId] = $table['tableName'];
                $event->setMetaModelName($this->tableNames[$metaModelId]);
            }
        }
    }

    /**
     * Determines the correct factory from a metamodel table name and creates an instance using the factory.
     *
     * @param CreateMetaModelEvent $event   The event.
     *
     * @param array                $arrData The meta information for the MetaModel.
     *
     * @return bool
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     * @SuppressWarnings(PHPMD.CamelCaseVariableName)
     */
    protected function createInstanceViaLegacyFactory(CreateMetaModelEvent $event, $arrData)
    {
        $name = $arrData['tableName'];
        if (!isset($GLOBALS['METAMODELS']['factories'][$name])) {
            return false;
        }

        // @codingStandardsIgnoreStart
        @trigger_error('Creating MetaModel instances via global factories is deprecated.', E_USER_DEPRECATED);
        // @codingStandardsIgnoreEnd

        $factoryClass = $GLOBALS['METAMODELS']['factories'][$name];
        $event->setMetaModel(call_user_func_array(array($factoryClass, 'createInstance'), array($arrData)));

        return $event->getMetaModel() !== null;
    }

    /**
     * Create a MetaModel instance with the given information.
     *
     * @param CreateMetaModelEvent $event   The event.
     *
     * @param array                $arrData The meta information for the MetaModel.
     *
     * @return void
     */
    protected function createInstance(CreateMetaModelEvent $event, $arrData)
    {
        if (!$this->createInstanceViaLegacyFactory($event, $arrData)) {
            $metaModel = new MetaModel($arrData, $this->dispatcher, $this->database);
            $metaModel->setServiceContainer(function () {
                return $this->getServiceContainer();
            }, false);
            $event->setMetaModel($metaModel);
        }

        if ($event->getMetaModel()) {
            $this->instancesByTable[$event->getMetaModelName()]     = $event->getMetaModel();
            $this->instancesById[$event->getMetaModel()->get('id')] = $event->getMetaModel();
        }
    }

    /**
     * Create a MetaModel instance.
     *
     * @param CreateMetaModelEvent $event The event.
     *
     * @return void
     */
    public function createMetaModel(CreateMetaModelEvent $event)
    {
        if ($event->getMetaModel() !== null) {
            return;
        }

        if (isset($this->instancesByTable[$event->getMetaModelName()])) {
            $event->setMetaModel($this->instancesByTable[$event->getMetaModelName()]);

            return;
        }

        $table = $this
            ->database
            ->createQueryBuilder()
            ->select('*')
            ->from('tl_metamodel')
            ->where('tableName=:tableName')
            ->setParameter('tableName', $event->getMetaModelName())
            ->setMaxResults(1)
            ->execute()
            ->fetch(\PDO::FETCH_ASSOC);

        if ($table) {
            $table['system_columns'] = $this->systemColumns;

            $this->createInstance($event, $table);
        }
    }

    /**
     * Collect the table names from the database.
     *
     * @param CollectMetaModelTableNamesEvent $event The event.
     *
     * @return void
     */
    public function collectMetaModelTableNames(CollectMetaModelTableNamesEvent $event)
    {
        if ($this->tableNamesCollected) {
            $event->addMetaModelNames($this->tableNames);

            return;
        }

        $tables = $this
            ->database
            ->createQueryBuilder()
            ->select('*')
            ->from('tl_metamodel')
            ->orderBy('sorting')
            ->execute()
            ->fetchAll(\PDO::FETCH_ASSOC);

        foreach ($tables as $table) {
            $this->tableNames[$table['id']] = $table['tableName'];
        }

        $event->addMetaModelNames($this->tableNames);
        $this->tableNamesCollected = true;
    }

    /**
     * Collect all attribute information from the database for the MetaModel.
     *
     * @param CollectMetaModelAttributeInformationEvent $event The event.
     *
     * @return void
     */
    public function collectMetaModelAttributeInformation(CollectMetaModelAttributeInformationEvent $event)
    {
        $metaModelName = $event->getMetaModel()->getTableName();
        if (!array_key_exists($metaModelName, $this->attributeInformation)) {
            $attributes = $this
                ->database
                ->createQueryBuilder()
                ->select('*')
                ->from('tl_metamodel_attribute')
                ->where('pid=:pid')
                ->setParameter('pid', $event->getMetaModel()->get('id'))
                ->orderBy('sorting')
                ->execute()
                ->fetchAll(\PDO::FETCH_ASSOC);

            $this->attributeInformation[$metaModelName] = [];
            foreach ($attributes as $attribute) {
                $colName = $attribute['colname'];

                $this->attributeInformation[$metaModelName][$colName] = $attribute;
            }
        }

        foreach ($this->attributeInformation[$metaModelName] as $name => $information) {
            $event->addAttributeInformation($name, $information);
        }
    }
}


<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\CoreBundle\EventListener\DcGeneral\Table\DcaSetting;

use ContaoCommunityAlliance\DcGeneral\Contao\View\Contao2BackendView\Event\GetPropertyOptionsEvent;
use MetaModels\Attribute\IInternal;

/**
 * This handles retrieving attributes as options.
 */
class AttributeOptionListener extends AbstractListener
{
    /**
     * Retrieve the options for the attributes.
     *
     * @param GetPropertyOptionsEvent $event The event.
     *
     * @return void
     */
    public function getAttributeOptions(GetPropertyOptionsEvent $event)
    {
        if (!$this->wantToHandle($event) || ($event->getPropertyName() !== 'attr_id')) {
            return;
        }

        $model     = $event->getModel();
        $metaModel = $this->getMetaModelFromModel($model);

        if (!$metaModel) {
            return;
        }

        $options = [];

        // Fetch all attributes that exist in other settings.
        $alreadyTaken = $this->connection->createQueryBuilder()
            ->select('attr_id')
            ->from('tl_metamodel_dcasetting')
            ->where('dcatype="attribute"')
            ->andWhere('pid=:pid')
            ->setParameter('pid', $model->getProperty('pid'));

        // If an attribute is selected, we want to keep it in the list.
        if (!empty($attributeId = $model->getProperty('attr_id'))) {
            $alreadyTaken
                ->andWhere('attr_id<>:id')
                ->setParameter('id', $attributeId);
        }
        $alreadyTaken = $alreadyTaken->execute()->fetchAll(\PDO::FETCH_COLUMN);

        foreach ($metaModel->getAttributes() as $attribute) {
            if ($attribute instanceof IInternal || in_array($attribute->get('id'), $alreadyTaken)) {
                continue;
            }
            $options[$attribute->get('id')] = sprintf(
                '%s [%s]',
                $attribute->getName(),
                $attribute->get('type')
            );
        }

        $event->setOptions($options);
    }
}


<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\Filter\Rules;

use MetaModels\Attribute\IAttribute;
use MetaModels\Attribute\IComplex;
use MetaModels\Attribute\ITranslated;
use MetaModels\Filter\FilterRule;

/**
 * This is the MetaModelFilterRule class for handling string value searches on attributes.
 */
class SearchAttribute extends FilterRule
{
    /**
     * The attribute to search in.
     *
     * @var IAttribute|ITranslated|IComplex
     */
    protected $objAttribute = null;

    /**
     * The value to search for.
     *
     * @var string
     */
    protected $strValue = null;

    /**
     * The valid languages to match (only used when searching a translated attribute).
     *
     * @var array
     */
    protected $arrValidLanguages = null;

    /**
     * Creates an instance of a simple query filter rule.
     *
     * @param IAttribute $objAttribute      The attribute to be searched.
     *
     * @param string     $strValue          The value to be searched for. Wildcards (* and ? allowed).
     *
     * @param array      $arrValidLanguages The list of valid languages to be searched in.
     */
    public function __construct($objAttribute, $strValue = '', $arrValidLanguages = array())
    {
        parent::__construct();
        $this->objAttribute      = $objAttribute;
        $this->strValue          = $strValue;
        $this->arrValidLanguages = $arrValidLanguages;
    }

    /**
     * {@inheritdoc}
     */
    public function getMatchingIds()
    {
        if ($this->objAttribute instanceof ITranslated) {
            return $this->objAttribute->searchForInLanguages($this->strValue, $this->arrValidLanguages);
        }

        return $this->objAttribute->searchFor($this->strValue);
    }
}


<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Richard Henkenjohann <richardhenkenjohann@googlemail.com>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\Helper;

/**
 * Helper class to calculate limit and offset and pagination.
 */
class PaginationLimitCalculator
{
    /**
     * Use limit.
     *
     * @var bool
     */
    private $applyLimitAndOffset = false;

    /**
     * Offset.
     *
     * @var int
     */
    private $offset = 0;

    /**
     * Limit.
     *
     * @var int
     */
    private $limit = 0;

    /**
     * The current page.
     *
     * @var int
     */
    private $currentPage = 1;

    /**
     * Pagination page break.
     *
     * @var int
     */
    private $perPage = 0;

    /**
     * The total amount of items.
     *
     * @var int
     */
    private $totalAmount = 0;

    /**
     * The maximum number of pagination links.
     *
     * @var int
     */
    private $maxPaginationLinks;

    /**
     * The calculated offset.
     *
     * @var int
     */
    private $calculatedOffset;

    /**
     * The calculated limit.
     *
     * @var int
     */
    private $calculatedLimit;

    /**
     * The calculated total amount.
     *
     * @var int
     */
    private $calculatedTotal;

    /**
     * Flag if the data needs to be recalculated.
     *
     * @var bool
     */
    private $isDirty = true;

    /**
     * Check if the object needs to be recalculated.
     *
     * @return boolean
     */
    public function isDirty()
    {
        return $this->isDirty;
    }

    /**
     * Mark the object dirty.
     *
     * @return PaginationLimitCalculator
     */
    public function markDirty()
    {
        $this->isDirty = true;

        return $this;
    }

    /**
     * Check if a limit shall be applied.
     *
     * @return boolean
     */
    public function isLimited()
    {
        return $this->applyLimitAndOffset && ($this->getLimit() || $this->getOffset());
    }

    /**
     * Set if a limit shall be applied.
     *
     * @param boolean $applyLimitAndOffset The flag.
     *
     * @return PaginationLimitCalculator
     */
    public function setApplyLimitAndOffset($applyLimitAndOffset)
    {
        $this->applyLimitAndOffset = (bool) $applyLimitAndOffset;

        return $this->markDirty();
    }

    /**
     * Retrieve the offset to use.
     *
     * @return int
     */
    public function getOffset()
    {
        return $this->offset;
    }

    /**
     * Set the offset to use.
     *
     * @param int $offset The new offset.
     *
     * @return PaginationLimitCalculator
     */
    public function setOffset($offset)
    {
        $this->offset = (int) $offset;

        return $this->markDirty();
    }

    /**
     * Retrieve the limit.
     *
     * @return int
     */
    public function getLimit()
    {
        return $this->limit;
    }

    /**
     * Set the limit.
     *
     * @param int $limit The limit.
     *
     * @return PaginationLimitCalculator
     */
    public function setLimit($limit)
    {
        $this->limit = (int) $limit;

        return $this->markDirty();
    }

    /**
     * Retrieve the current page (in pagination).
     *
     * @return int
     */
    public function getCurrentPage()
    {
        return $this->currentPage;
    }

    /**
     * Set the current page (for pagination).
     *
     * @param int $currentPage The current page.
     *
     * @return PaginationLimitCalculator
     */
    public function setCurrentPage($currentPage)
    {
        $this->currentPage = (int) $currentPage;

        return $this->markDirty();
    }

    /**
     * Set the pagination limit.
     *
     * @return int
     */
    public function getPerPage()
    {
        return $this->perPage;
    }

    /**
     * Set the pagination limit.
     *
     * @param int $perPage The pagination limit.
     *
     * @return PaginationLimitCalculator
     */
    public function setPerPage($perPage)
    {
        $this->perPage = $perPage;

        return $this->markDirty();
    }

    /**
     * Retrieve the total amount of items.
     *
     * @return int
     */
    public function getTotalAmount()
    {
        return $this->totalAmount;
    }

    /**
     * Set the total amount of items.
     *
     * @param int $totalAmount The total amount.
     *
     * @return PaginationLimitCalculator
     */
    public function setTotalAmount($totalAmount)
    {
        $this->totalAmount = $totalAmount;

        return $this->markDirty();
    }

    /**
     * Set the number of maximum pagination links.
     *
     * @return int
     */
    public function getMaxPaginationLinks()
    {
        if (null === $this->maxPaginationLinks) {
            $this->setMaxPaginationLinks(\Config::get('maxPaginationLinks'));
        }

        return $this->maxPaginationLinks;
    }

    /**
     * Get the number of maximum pagination links.
     *
     * @param int $maxPaginationLinks The maximum number of links.
     *
     * @return PaginationLimitCalculator
     */
    public function setMaxPaginationLinks($maxPaginationLinks)
    {
        $this->maxPaginationLinks = $maxPaginationLinks;

        return $this->markDirty();
    }

    /**
     * Render the pagination string.
     *
     * @return string
     */
    public function getPaginationString()
    {
        $this->calculate();

        if ($this->getPerPage() == 0) {
            return '';
        }

        // Add pagination menu.
        $objPagination = new \Pagination($this->calculatedTotal, $this->getPerPage(), $this->getMaxPaginationLinks());

        return $objPagination->generate("\n  ");
    }

    /**
     * Retrieve the calculated offset.
     *
     * @return int
     */
    public function getCalculatedOffset()
    {
        $this->calculate();

        return $this->calculatedOffset;
    }

    /**
     * Retrieve the calculated limit.
     *
     * @return int
     */
    public function getCalculatedLimit()
    {
        $this->calculate();

        return $this->calculatedLimit;
    }

    /**
     * Calculate the limit and offset with pagination.
     *
     * @return void
     */
    private function calculatePaginated()
    {
        $this->calculatedTotal = $this->getTotalAmount();

        // If a total limit has been defined, we need to honor that.
        if (($this->calculatedLimit !== null) && ($this->calculatedTotal > $this->calculatedLimit)) {
            $this->calculatedTotal -= $this->calculatedLimit;
        }
        $this->calculatedTotal -= $this->calculatedOffset;

        // Get the current page.
        $page = $this->getCurrentPage();

        if ($page > ($this->calculatedTotal / $this->getPerPage())) {
            $page = (int) ceil($this->calculatedTotal / $this->getPerPage());
        }

        // Set limit and offset.
        $pageOffset              = ((max($page, 1) - 1) * $this->getPerPage());
        $this->calculatedOffset += $pageOffset;
        if ($this->calculatedLimit === null) {
            $this->calculatedLimit = $this->getPerPage();
        } else {
            $this->calculatedLimit = min(($this->calculatedLimit - $this->calculatedOffset), $this->getPerPage());
        }
    }

    /**
     * Calculate the pagination based upon the offset, limit and total amount of items.
     *
     * @return void
     */
    protected function calculate()
    {
        if (!$this->isDirty()) {
            return;
        }

        $this->isDirty = false;

        $this->calculatedOffset = null;
        $this->calculatedLimit  = null;

        // If defined, we override the pagination here.
        if ($this->isLimited()) {
            if ($this->getLimit()) {
                $this->calculatedLimit = $this->getLimit();
            }
            if ($this->getOffset()) {
                $this->calculatedOffset = $this->getOffset();
            }
        }

        if ($this->getPerPage() > 0) {
            $this->calculatePaginated();

            return;
        }

        if ($this->calculatedLimit === null) {
            $this->calculatedLimit = 0;
        }
        if ($this->calculatedOffset === null) {
            $this->calculatedOffset = 0;
        }
    }
}


<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Stefan Heimes <stefan_heimes@hotmail.com>
 * @author     Danilo Benevides <danilobenevides01@gmail.com>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @author     Cliff Parnitzky <github@cliff-parnitzky.de>
 * @author     Ingolf Steinhardt <info@e-spin.de>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\Widgets;

use Contao\Date;
use Contao\StringUtil;
use Contao\System;
use Contao\Widget;
use ContaoCommunityAlliance\Contao\Bindings\ContaoEvents;
use ContaoCommunityAlliance\Contao\Bindings\Events\Image\GenerateHtmlEvent;
use ContaoCommunityAlliance\Contao\Bindings\Events\Widget\GetAttributesFromDcaEvent;
use Exception;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Provide methods to handle multiple widgets in one.
 */
class SubDcaWidget extends Widget
{
    /**
     * Submit user input.
     *
     * @var boolean
     */
    protected $blnSubmitInput = true;

    /**
     * Template.
     *
     * @var string
     */
    protected $strTemplate = 'be_subdca';

    /**
     * Options.
     *
     * @var array
     */
    protected $arrOptions = array();

    /**
     * SubFields.
     *
     * @var array
     */
    protected $arrSubFields = array();

    /**
     * Flag fields to be applied to each subfield.
     *
     * @var array
     */
    protected $arrFlagFields = array();

    /**
     * The prepared widgets.
     *
     * @var array
     */
    protected $arrWidgets = array();

    /**
     * Initialize the object.
     *
     * @param array|bool $attributes The attributes to apply to this widget (optional).
     */
    public function __construct($attributes = false)
    {
        parent::__construct();
        $this->addAttributes($attributes);
        // Input field callback.
        if (isset($attributes['getsubfields_callback']) && is_array($attributes['getsubfields_callback'])) {
            $arrCallback = $this->$attributes['getsubfields_callback'];
            if (!is_object($arrCallback[0])) {
                $this->import($arrCallback[0]);
            }
            $this->arrSubFields = $this->{$arrCallback[0]}->{$arrCallback[1]}($this, $attributes);
        }
    }

    /**
     * Add specific attribute magic setter.
     *
     * In addition to those supported by the Contao Widget class, this
     * widget does understand: 'options', 'subfields' and 'flagfields'.
     *
     * @param string $strKey   The key of the attribute to set.
     *
     * @param mixed  $varValue The value to use.
     *
     * @return void
     */
    public function __set($strKey, $varValue)
    {
        switch ($strKey) {
            case 'options':
                $this->arrOptions = StringUtil::deserialize($varValue);

                foreach ($this->arrOptions as $arrOptions) {
                    if ($arrOptions['default']) {
                        $this->varValue = $arrOptions['value'];
                    }
                }
                break;
            case 'subfields':
                $this->arrSubFields = StringUtil::deserialize($varValue);
                break;

            case 'flagfields':
                $this->arrFlagFields = StringUtil::deserialize($varValue);
                break;

            default:
                parent::__set($strKey, $varValue);
                break;
        }
    }

    /**
     * Retrieve the event dispatcher.
     *
     * @return EventDispatcherInterface
     */
    public function getEventDispatcher()
    {
        return System::getContainer()->get('event_dispatcher');
    }

    /**
     * Generate an help wizard if needed.
     *
     * @param string $key   The widget name.
     *
     * @param array  $field The field DCA - might get changed within this routine.
     *
     * @return string
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     * @SuppressWarnings(PHPMD.CamelCaseVariableName)
     */
    protected function getHelpWizard($key, $field)
    {
        // Add the help wizard.
        if (empty($field['eval']['helpwizard'])) {
            return '';
        }

        $event = new GenerateHtmlEvent(
            'about.svg',
            $GLOBALS['TL_LANG']['MSC']['helpWizard'],
            'style="vertical-align:text-bottom;"'
        );
        $this->getEventDispatcher()->dispatch(ContaoEvents::IMAGE_GET_HTML, $event);

        return sprintf(
            ' %s',
            TL_PATH . 'contao/',
            $this->strTable,
            $this->strName,
            $key,
            StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['helpWizard']),
            $event->getHtml()
        );
    }

    /**
     * Make fields mandatory if necessary.
     *
     * @param array  $field The field DCA.
     *
     * @param string $row   The setting name.
     *
     * @param string $key   The widget name.
     *
     * @return array
     */
    protected function makeMandatory($field, $row, $key)
    {
        $field['eval']['required'] = false;
        // Use strlen() here (see contao core issue #3277).
        if (empty($field['eval']['mandatory'])) {
            return $field;
        }

        if (is_array($this->varValue[$row][$key])) {
            if (empty($this->varValue[$row][$key])) {
                $field['eval']['required'] = true;
            }
        } else {
            if (!strlen($this->varValue[$row][$key])) {
                $field['eval']['required'] = true;
            }
        }

        return $field;
    }

    /**
     * Retrieve the widget class if it is valid.
     *
     * @param array $field The field information.
     *
     * @return string
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     * @SuppressWarnings(PHPMD.CamelCaseVariableName)
     */
    protected function getWidgetClass($field)
    {
        $className = $GLOBALS[(TL_MODE == 'BE' ? 'BE_FFL' : 'TL_FFL')][$field['inputType']];

        if (($className !== '') && class_exists($className)) {
            return $className;
        }

        return null;
    }

    /**
     * Handle the onload_callback.
     *
     * @param array $field The field information.
     *
     * @param mixed $value The value.
     *
     * @return mixed
     */
    protected function handleLoadCallback($field, $value)
    {
        // Load callback.
        if (isset($field['load_callback']) && is_array($field['load_callback'])) {
            foreach ($field['load_callback'] as $callback) {
                $this->import($callback[0]);
                $value = $this->{$callback[0]}->{$callback[1]}($value, $this);
            }
        }

        return $value;
    }

    /**
     * Initialize widget.
     *
     * Based on DataContainer::row() from Contao 2.10.1.
     *
     * @param array  $arrField The field DCA - might get changed within this routine.
     *
     * @param string $strRow   The setting name.
     *
     * @param string $strKey   The widget name.
     *
     * @param mixed  $varValue The widget value.
     *
     * @return Widget|null The widget on success, null otherwise.
     */
    protected function initializeWidget(&$arrField, $strRow, $strKey, $varValue)
    {
        $xlabel = $this->getHelpWizard($strKey, $arrField);

        // Input field callback.
        if (isset($arrField['input_field_callback']) && is_array($arrField['input_field_callback'])) {
            if (!is_object($this->$arrField['input_field_callback'][0])) {
                $this->import($arrField['input_field_callback'][0]);
            }

            return $this->{$arrField['input_field_callback'][0]}->$arrField['input_field_callback'][1]($this, $xlabel);
        }

        $strClass = $this->getWidgetClass($arrField);

        if (empty($strClass)) {
            return null;
        }

        $varValue = $this->handleLoadCallback($arrField, $varValue);
        $arrField = $this->makeMandatory($arrField, $strRow, $strKey);

        $arrField['name']              = $this->strName . '[' . $strRow . '][' . $strKey . ']';
        $arrField['id']                = $this->strId . '_' . $strRow . '_' . $strKey;
        $arrField['value']             = ($varValue !== '') ? $varValue : $arrField['default'];
        $arrField['eval']['tableless'] = true;

        $event = new GetAttributesFromDcaEvent(
            $arrField,
            $arrField['name'],
            $arrField['value'],
            null,
            $this->strTable,
            $this->objDca
        );

        $this->getEventDispatcher()->dispatch(ContaoEvents::WIDGET_GET_ATTRIBUTES_FROM_DCA, $event);

        $objWidget = new $strClass($event->getResult());

        $objWidget->strId       = $arrField['id'];
        $objWidget->storeValues = true;
        $objWidget->xlabel      = $xlabel;

        return $objWidget;
    }

    /**
     * Prepare all widgets and store them in the protected $arrWidgets property.
     *
     * @return void
     */
    protected function prepareWidgets()
    {
        if ($this->arrWidgets) {
            return;
        }

        $arrWidgets = array();
        foreach ($this->arrSubFields as $strFieldName => &$arrSubField) {
            $varValue  = $this->value[$strFieldName];
            $arrRow    = array();
            $objWidget = $this->initializeWidget(
                $arrSubField,
                $strFieldName,
                'value',
                $varValue['value']
            );

            if (!$objWidget) {
                continue;
            }
            $arrRow[] = $objWidget;
            foreach ($this->arrFlagFields as $strFlag => $arrFlagField) {
                $objWidget = $this->initializeWidget(
                    $arrFlagField,
                    $strFieldName,
                    $strFlag,
                    $varValue[$strFlag]
                );

                if ($objWidget) {
                    $arrRow[] = $objWidget;
                }
            }
            $arrWidgets[] = $arrRow;
        }
        $this->arrWidgets = $arrWidgets;
    }

    /**
     * Handle the onsave_callback for a widget.
     *
     * @param array  $field  The field DCA.
     *
     * @param Widget $widget The widget to validate.
     *
     * @param mixed  $value  The value.
     *
     * @return mixed
     */
    protected function handleSaveCallback($field, $widget, $value)
    {
        $newValue = $value;

        if (isset($field['save_callback']) && is_array($field['save_callback'])) {
            foreach ($field['save_callback'] as $callback) {
                $this->import($callback[0]);

                try {
                    $newValue = $this->{$callback[0]}->{$callback[1]}($newValue, $this);
                } catch (Exception $e) {
                    $widget->addError($e->getMessage());
                    $this->blnSubmitInput = false;

                    return $value;
                }
            }
        }

        return $newValue;
    }

    /**
     * Validate the value of the widget.
     *
     * Based on DataContainer::row() from Contao 2.10.1
     *
     * @param array  $arrField The field DCA.
     *
     * @param string $strRow   The setting name.
     *
     * @param string $strKey   The widget name.
     *
     * @param mixed  $varInput The overall input value.
     *
     * @return bool
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     * @SuppressWarnings(PHPMD.CamelCaseVariableName)
     */
    protected function validateWidget(&$arrField, $strRow, $strKey, &$varInput)
    {
        $varValue  = $varInput[$strRow][$strKey];
        $objWidget = $this->initializeWidget($arrField, $strRow, $strKey, $varValue);
        if (!is_object($objWidget)) {
            return false;
        }

        // Hack for checkboxes.
        if (($arrField['inputType'] == 'checkbox') && isset($varInput[$strRow][$strKey])) {
            $_POST[$objWidget->name] = $varValue;
        }

        $objWidget->validate();

        $varValue = $objWidget->value;

        // Convert date formats into timestamps (check the eval setting first -> #3063).
        $rgxp = $arrField['eval']['rgxp'];
        if (($rgxp == 'date' || $rgxp == 'time' || $rgxp == 'datim') && $varValue != '') {
            $objDate  = new Date($varValue, $GLOBALS['TL_CONFIG'][$rgxp . 'Format']);
            $varValue = $objDate->tstamp;
        }

        $varValue = $this->handleSaveCallback($arrField, $objWidget, $varValue);

        $varInput[$strRow][$strKey] = $varValue;

        // Do not submit if there are errors.
        if ($objWidget->hasErrors()) {
            return false;
        }

        return true;
    }

    /**
     * Validate the widget.
     *
     * @param mixed $varInput The value to validate.
     *
     * @return mixed The validated data.
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     * @SuppressWarnings(PHPMD.CamelCaseVariableName)
     */
    protected function validator($varInput)
    {
        $blnHasError = false;
        foreach ($this->arrSubFields as $strFieldName => &$arrSubField) {
            if (!$this->validateWidget($arrSubField, $strFieldName, 'value', $varInput)) {
                $blnHasError = true;
            }

            foreach ($this->arrFlagFields as $strFlag => $arrFlagField) {
                if (!$this->validateWidget($arrFlagField, $strFieldName, $strFlag, $varInput)) {
                    $blnHasError = true;
                }
            }
        }
        unset($arrSubField);

        if ($blnHasError) {
            $this->blnSubmitInput = false;
            $this->addError($GLOBALS['TL_LANG']['ERR']['general']);
        }
        return $varInput;
    }

    /**
     * Generate the help tag for a widget if needed.
     *
     * @param Widget $widget The widget.
     *
     * @return string
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     * @SuppressWarnings(PHPMD.CamelCaseVariableName)
     */
    protected function getHelpForWidget($widget)
    {
        if ($GLOBALS['TL_CONFIG']['showHelp'] && $widget->description) {
            return sprintf(
                '
%s
',
                $widget->tl_class,
                $widget->description
            );
        }

        return '';
    }

    /**
     *  Build the options for a widget.
     *
     * @return array.
     */
    protected function buildOptions()
    {
        $options = array();
        foreach ($this->arrWidgets as $widgetRow) {
            $columns = array();
            foreach ($widgetRow as $widget) {
                /** @var Widget $widget */
                $valign = ($widget->valign != '' ? ' valign="' . $widget->valign . '"' : '');
                $class  = ($widget->tl_class != '' ? ' class="' . $widget->tl_class . '"' : '');
                $style  = ($widget->style != '' ? ' style="' . $widget->style . '"' : '');
                $help   = $this->getHelpForWidget($widget);

                $columns[] = sprintf(
                    '%4$s%5$s	',
                    $valign,
                    $class,
                    $style,
                    $widget->parse(),
                    $help
                );
            }
            $options[] = implode('', $columns);
        }

        return $options;
    }

    /**
     * Generate the widget and return it as string.
     *
     * @return string
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     * @SuppressWarnings(PHPMD.CamelCaseVariableName)
     */
    public function generate()
    {
        $GLOBALS['TL_CSS'][] = 'bundles/metamodelscore/css/style.css';

        $this->prepareWidgets();

        $arrOptions = $this->buildOptions();

        // Add a "no entries found" message if there are no sub widgets.
        if (!count($arrOptions)) {
            $arrOptions[] = '
'.$GLOBALS['TL_LANG']['MSC']['noResult'].'
	';
        }

        $strHead = '';
        $strBody = sprintf('

%s

', implode("
\n
", $arrOptions));

        $strOutput = sprintf(
            '
%s%s
',
            (($this->style) ? ('style="' . $this->style . '"') : ('')),
            $this->strId,
            $strHead,
            $strBody
        );

        return sprintf(
            '
%s
',
            $this->strName,
            (strlen($this->strClass) ? ' ' . $this->strClass : ''),
            $strOutput
        );
    }
}