From 043cd353b759a8923c4a4096d29366ab13f0adda Mon Sep 17 00:00:00 2001 From: Andreas Schempp Date: Mon, 24 Jul 2017 09:36:04 +0200 Subject: [PATCH] Rewrite the DCA picker (see #950). --- CHANGELOG.md | 5 +- src/ContaoCoreBundle.php | 4 +- src/Controller/BackendController.php | 26 +- src/DataContainer/DcaFilterInterface.php | 29 -- ...roviderPass.php => PickerProviderPass.php} | 11 +- .../ContaoCoreExtension.php | 6 +- src/Menu/AbstractMenuProvider.php | 169 --------- src/Menu/ArticlePickerProvider.php | 79 ---- src/Menu/FilePickerProvider.php | 136 ------- src/Menu/PagePickerProvider.php | 79 ---- src/Menu/PickerMenuBuilder.php | 130 ------- src/Menu/PickerMenuBuilderInterface.php | 58 --- src/Menu/PickerMenuProviderInterface.php | 76 ---- src/Picker/AbstractPickerProvider.php | 130 +++++++ src/Picker/ArticlePickerProvider.php | 89 +++++ src/Picker/DcaPickerProviderInterface.php | 48 +++ src/Picker/FilePickerProvider.php | 197 ++++++++++ src/Picker/PagePickerProvider.php | 113 ++++++ src/Picker/Picker.php | 122 ++++++ src/Picker/PickerBuilder.php | 143 +++++++ src/Picker/PickerBuilderInterface.php | 58 +++ src/Picker/PickerConfig.php | 185 +++++++++ src/Picker/PickerInterface.php | 49 +++ src/Picker/PickerProviderInterface.php | 64 ++++ src/Resources/config/services.yml | 73 ++-- src/Resources/contao/classes/Ajax.php | 4 - src/Resources/contao/classes/Backend.php | 50 +-- .../contao/classes/DataContainer.php | 153 +++----- .../contao/controllers/BackendMain.php | 23 +- src/Resources/contao/dca/tl_content.php | 2 +- src/Resources/contao/drivers/DC_Folder.php | 112 +++--- src/Resources/contao/drivers/DC_Table.php | 67 ++-- .../templates/backend/be_tinyFlash.html5 | 3 +- .../contao/templates/backend/be_tinyMCE.html5 | 3 +- .../templates/backend/be_tinyNews.html5 | 3 +- src/Resources/contao/widgets/FileTree.php | 89 ++--- src/Resources/contao/widgets/PageTree.php | 107 +++--- src/Resources/public/core.js | 21 +- src/Resources/public/core.min.js | 5 +- tests/Controller/BackendControllerTest.php | 89 ++++- .../Compiler/PickerMenuProviderPassTest.php | 77 ---- .../ContaoCoreExtensionTest.php | 168 +++++---- tests/Menu/AbstractMenuProviderTest.php | 156 -------- tests/Menu/ArticlePickerProviderTest.php | 124 ------ tests/Menu/FilePickerProviderTest.php | 184 --------- tests/Menu/PagePickerProviderTest.php | 124 ------ tests/Menu/PickerMenuBuilderTest.php | 142 ------- tests/Picker/ArticlePickerProviderTest.php | 256 +++++++++++++ tests/Picker/FilePickerProviderTest.php | 355 ++++++++++++++++++ tests/Picker/PagePickerProviderTest.php | 278 ++++++++++++++ tests/Picker/PickerBuilderTest.php | 191 ++++++++++ tests/Picker/PickerConfigTest.php | 160 ++++++++ tests/Picker/PickerTest.php | 151 ++++++++ 53 files changed, 3114 insertions(+), 2062 deletions(-) delete mode 100644 src/DataContainer/DcaFilterInterface.php rename src/DependencyInjection/Compiler/{PickerMenuProviderPass.php => PickerProviderPass.php} (72%) delete mode 100644 src/Menu/AbstractMenuProvider.php delete mode 100644 src/Menu/ArticlePickerProvider.php delete mode 100644 src/Menu/FilePickerProvider.php delete mode 100644 src/Menu/PagePickerProvider.php delete mode 100644 src/Menu/PickerMenuBuilder.php delete mode 100644 src/Menu/PickerMenuBuilderInterface.php delete mode 100644 src/Menu/PickerMenuProviderInterface.php create mode 100644 src/Picker/AbstractPickerProvider.php create mode 100644 src/Picker/ArticlePickerProvider.php create mode 100644 src/Picker/DcaPickerProviderInterface.php create mode 100644 src/Picker/FilePickerProvider.php create mode 100644 src/Picker/PagePickerProvider.php create mode 100644 src/Picker/Picker.php create mode 100644 src/Picker/PickerBuilder.php create mode 100644 src/Picker/PickerBuilderInterface.php create mode 100644 src/Picker/PickerConfig.php create mode 100644 src/Picker/PickerInterface.php create mode 100644 src/Picker/PickerProviderInterface.php delete mode 100644 tests/DependencyInjection/Compiler/PickerMenuProviderPassTest.php delete mode 100644 tests/Menu/AbstractMenuProviderTest.php delete mode 100644 tests/Menu/ArticlePickerProviderTest.php delete mode 100644 tests/Menu/FilePickerProviderTest.php delete mode 100644 tests/Menu/PagePickerProviderTest.php delete mode 100644 tests/Menu/PickerMenuBuilderTest.php create mode 100644 tests/Picker/ArticlePickerProviderTest.php create mode 100644 tests/Picker/FilePickerProviderTest.php create mode 100644 tests/Picker/PagePickerProviderTest.php create mode 100644 tests/Picker/PickerBuilderTest.php create mode 100644 tests/Picker/PickerConfigTest.php create mode 100644 tests/Picker/PickerTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 832fa7a865..3a341fb809 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,12 @@ ### DEV - * Eliminate nested paths when deleting folders in the file manager (see #941). - * Do not generate duplicate form IDs (see contao/core#8538). + * Rewrite the DCA picker (see #950). ### 4.4.1 (2017-07-12) * Prevent arbitrary PHP file inclusions in the back end (see CVE-2017-10993). - * Correctly handle subpalettes in "edit multiple" mode (see #946). + * Correctly handle subpalettes in "edit multiple" mode (see #946). * Correctly show the DCA picker in the site structure (see #906). * Correctly update the style sheets if a format definition is enabled/disabled (see #893). * Always show the "show from" and "show until" fields (see #908). diff --git a/src/ContaoCoreBundle.php b/src/ContaoCoreBundle.php index 1834cdcf7c..8370e827fa 100644 --- a/src/ContaoCoreBundle.php +++ b/src/ContaoCoreBundle.php @@ -15,7 +15,7 @@ use Contao\CoreBundle\DependencyInjection\Compiler\AddResourcesPathsPass; use Contao\CoreBundle\DependencyInjection\Compiler\AddSessionBagsPass; use Contao\CoreBundle\DependencyInjection\Compiler\DoctrineMigrationsPass; -use Contao\CoreBundle\DependencyInjection\Compiler\PickerMenuProviderPass; +use Contao\CoreBundle\DependencyInjection\Compiler\PickerProviderPass; use Contao\CoreBundle\DependencyInjection\ContaoCoreExtension; use Symfony\Component\Console\Application; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -63,6 +63,6 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new AddResourcesPathsPass()); $container->addCompilerPass(new AddImagineClassPass()); $container->addCompilerPass(new DoctrineMigrationsPass()); - $container->addCompilerPass(new PickerMenuProviderPass()); + $container->addCompilerPass(new PickerProviderPass()); } } diff --git a/src/Controller/BackendController.php b/src/Controller/BackendController.php index b71dd238c5..83c71d0d84 100644 --- a/src/Controller/BackendController.php +++ b/src/Controller/BackendController.php @@ -21,11 +21,13 @@ use Contao\BackendPopup; use Contao\BackendPreview; use Contao\BackendSwitch; +use Contao\CoreBundle\Picker\PickerConfig; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; /** * Handles the Contao backend routes. @@ -214,18 +216,36 @@ public function alertsAction() } /** - * Handles the picker redirect. + * Redirects the user to the Contao back end and includes the picker query parameter. It will determine + * the current provider URL based on the value (usually read dynamically via JavaScript). * * @param Request $request * + * @throws BadRequestHttpException + * * @return RedirectResponse * * @Route("/_contao/picker", name="contao_backend_picker") */ public function pickerAction(Request $request) { - $pickerBuilder = $this->container->get('contao.menu.picker_menu_builder'); + $extras = []; + + if ($request->query->has('extras')) { + $extras = $request->query->get('extras'); + + if (!is_array($extras)) { + throw new BadRequestHttpException('Invalid picker extras'); + } + } + + $config = new PickerConfig($request->query->get('context'), $extras, $request->query->get('value')); + $picker = $this->container->get('contao.picker.builder')->create($config); + + if (null === $picker) { + throw new BadRequestHttpException('Unsupported picker context'); + } - return new RedirectResponse($pickerBuilder->getPickerUrl($request)); + return new RedirectResponse($picker->getCurrentUrl()); } } diff --git a/src/DataContainer/DcaFilterInterface.php b/src/DataContainer/DcaFilterInterface.php deleted file mode 100644 index aec1ecde74..0000000000 --- a/src/DataContainer/DcaFilterInterface.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ -interface DcaFilterInterface -{ - /** - * Returns the filter array. - * - * @return array - * - * @see DataContainer::setDcaFilter() - * @see DC_Folder::setDcaFilter() - */ - public function getDcaFilter(); -} diff --git a/src/DependencyInjection/Compiler/PickerMenuProviderPass.php b/src/DependencyInjection/Compiler/PickerProviderPass.php similarity index 72% rename from src/DependencyInjection/Compiler/PickerMenuProviderPass.php rename to src/DependencyInjection/Compiler/PickerProviderPass.php index d865ffa721..40819112de 100644 --- a/src/DependencyInjection/Compiler/PickerMenuProviderPass.php +++ b/src/DependencyInjection/Compiler/PickerProviderPass.php @@ -15,11 +15,12 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; /** - * Registers the picker menu providers. + * Registers the picker providers. * * @author Leo Feyer + * @author Andreas Schempp */ -class PickerMenuProviderPass implements CompilerPassInterface +class PickerProviderPass implements CompilerPassInterface { use PriorityTaggedServiceTrait; @@ -28,12 +29,12 @@ class PickerMenuProviderPass implements CompilerPassInterface */ public function process(ContainerBuilder $container) { - if (!$container->has('contao.menu.picker_menu_builder')) { + if (!$container->has('contao.picker.builder')) { return; } - $definition = $container->findDefinition('contao.menu.picker_menu_builder'); - $references = $this->findAndSortTaggedServices('contao.picker_menu_provider', $container); + $definition = $container->findDefinition('contao.picker.builder'); + $references = $this->findAndSortTaggedServices('contao.picker_provider', $container); foreach ($references as $reference) { $definition->addMethodCall('addProvider', [$reference]); diff --git a/src/DependencyInjection/ContaoCoreExtension.php b/src/DependencyInjection/ContaoCoreExtension.php index d0cf907ca3..d18fc7b1ee 100644 --- a/src/DependencyInjection/ContaoCoreExtension.php +++ b/src/DependencyInjection/ContaoCoreExtension.php @@ -10,7 +10,7 @@ namespace Contao\CoreBundle\DependencyInjection; -use Contao\CoreBundle\Menu\PickerMenuProviderInterface; +use Contao\CoreBundle\Picker\PickerProviderInterface; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; @@ -90,8 +90,8 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container $this->overwriteImageTargetDir($mergedConfig, $container); $container - ->registerForAutoconfiguration(PickerMenuProviderInterface::class) - ->addTag('contao.picker_menu_provider') + ->registerForAutoconfiguration(PickerProviderInterface::class) + ->addTag('contao.picker_provider') ; } diff --git a/src/Menu/AbstractMenuProvider.php b/src/Menu/AbstractMenuProvider.php deleted file mode 100644 index 19678e55aa..0000000000 --- a/src/Menu/AbstractMenuProvider.php +++ /dev/null @@ -1,169 +0,0 @@ - - */ -abstract class AbstractMenuProvider -{ - /** - * @var RouterInterface - */ - protected $router; - - /** - * @var RequestStack - */ - protected $requestStack; - - /** - * @var TokenStorageInterface - */ - protected $tokenStorage; - - /** - * @var array - */ - private $keys = ['do', 'context', 'target', 'value', 'popup']; - - /** - * Constructor. - * - * @param RouterInterface $router - * @param RequestStack $requestStack - * @param TokenStorageInterface|null $tokenStorage - */ - public function __construct(RouterInterface $router, RequestStack $requestStack, TokenStorageInterface $tokenStorage = null) - { - $this->router = $router; - $this->requestStack = $requestStack; - $this->tokenStorage = $tokenStorage; - } - - /** - * Returns the back end user object. - * - * @throws \RuntimeException - * - * @return BackendUser - */ - protected function getUser() - { - if (null === $this->tokenStorage) { - throw new \RuntimeException('No token storage provided'); - } - - $token = $this->tokenStorage->getToken(); - - if (null === $token) { - throw new \RuntimeException('No token provided'); - } - - $user = $token->getUser(); - - if (null === $user) { - throw new \RuntimeException('The token does not contain a user'); - } - - return $user; - } - - /** - * Generates a route. - * - * @param string $name - * @param array $params - * - * @return bool|string - */ - protected function route($name, array $params = []) - { - return $this->router->generate($name, $params); - } - - /** - * Adds a menu item. - * - * @param ItemInterface $menu - * @param FactoryInterface $factory - * @param string $do - * @param string $key - * @param string $class - */ - protected function addMenuItem(ItemInterface $menu, FactoryInterface $factory, $do, $key, $class) - { - $request = $this->requestStack->getCurrentRequest(); - - if (null === $request) { - return; - } - - $params = $this->getParametersFromRequest($request); - - $item = $factory->createItem( - $key, - ['uri' => $this->route('contao_backend', array_merge($params, ['do' => $do]))] - ); - - $item->setLabel($this->getLabel($key)); - $item->setLinkAttribute('class', $class); - $item->setCurrent(isset($params['do']) && $do === $params['do']); - - $menu->addChild($item); - } - - /** - * Returns the filtered request parameters. - * - * @param Request $request - * - * @return array - */ - protected function getParametersFromRequest(Request $request) - { - $params = []; - - foreach ($this->keys as $key) { - if ($request->query->has($key)) { - $params[$key] = $request->query->get($key); - } - } - - return $params; - } - - /** - * Returns a label. - * - * @param $key - * - * @return string - */ - protected function getLabel($key) - { - if (isset($GLOBALS['TL_LANG']['MSC'][$key])) { - return $GLOBALS['TL_LANG']['MSC'][$key]; - } - - return $key; - } -} diff --git a/src/Menu/ArticlePickerProvider.php b/src/Menu/ArticlePickerProvider.php deleted file mode 100644 index 4bb3a76b43..0000000000 --- a/src/Menu/ArticlePickerProvider.php +++ /dev/null @@ -1,79 +0,0 @@ - - */ -class ArticlePickerProvider extends AbstractMenuProvider implements PickerMenuProviderInterface -{ - /** - * {@inheritdoc} - */ - public function supports($context) - { - return 'link' === $context; - } - - /** - * {@inheritdoc} - */ - public function createMenu(ItemInterface $menu, FactoryInterface $factory) - { - $user = $this->getUser(); - - if ($user->hasAccess('article', 'modules')) { - $this->addMenuItem($menu, $factory, 'article', 'articlePicker', 'articles'); - } - } - - /** - * {@inheritdoc} - */ - public function supportsTable($table) - { - return 'tl_article' === $table; - } - - /** - * {@inheritdoc} - */ - public function processSelection($value) - { - return sprintf('{{article_url::%s}}', $value); - } - - /** - * {@inheritdoc} - */ - public function canHandle(Request $request) - { - return $request->query->has('value') && false !== strpos($request->query->get('value'), '{{article_url::'); - } - - /** - * {@inheritdoc} - */ - public function getPickerUrl(Request $request) - { - $params = $request->query->all(); - $params['do'] = 'article'; - $params['value'] = str_replace(['{{article_url::', '}}'], '', $params['value']); - - return $this->route('contao_backend', $params); - } -} diff --git a/src/Menu/FilePickerProvider.php b/src/Menu/FilePickerProvider.php deleted file mode 100644 index fd2fd4bb73..0000000000 --- a/src/Menu/FilePickerProvider.php +++ /dev/null @@ -1,136 +0,0 @@ - - */ -class FilePickerProvider extends AbstractMenuProvider implements PickerMenuProviderInterface, FrameworkAwareInterface -{ - use FrameworkAwareTrait; - - /** - * @var string - */ - private $uploadPath; - - /** - * Constructor. - * - * @param RouterInterface $router - * @param RequestStack $requestStack - * @param TokenStorageInterface $tokenStorage - * @param string $uploadPath - */ - public function __construct(RouterInterface $router, RequestStack $requestStack, TokenStorageInterface $tokenStorage, $uploadPath) - { - parent::__construct($router, $requestStack, $tokenStorage); - - $this->uploadPath = $uploadPath; - } - - /** - * {@inheritdoc} - */ - public function supports($context) - { - return 'file' === $context || 'link' === $context; - } - - /** - * {@inheritdoc} - */ - public function createMenu(ItemInterface $menu, FactoryInterface $factory) - { - $user = $this->getUser(); - - if ($user->hasAccess('files', 'modules')) { - $this->addMenuItem($menu, $factory, 'files', 'filePicker', 'filemounts'); - } - } - - /** - * {@inheritdoc} - */ - public function supportsTable($table) - { - return 'tl_files' === $table; - } - - /** - * {@inheritdoc} - */ - public function processSelection($value) - { - $value = rawurldecode($value); - - /** @var FilesModel $adapter */ - $adapter = $this->framework->getAdapter(FilesModel::class); - - if (($model = $adapter->findByPath($value)) instanceof FilesModel) { - return json_encode([ - 'content' => $value, - 'tag' => sprintf('{{file::%s}}', StringUtil::binToUuid($model->uuid)), - ]); - } - - return $value; - } - - /** - * {@inheritdoc} - */ - public function canHandle(Request $request) - { - if (!$request->query->has('value')) { - return false; - } - - $value = $request->query->get('value'); - - return 0 === strpos($value, $this->uploadPath.'/') || false !== strpos($value, '{{file::'); - } - - /** - * {@inheritdoc} - */ - public function getPickerUrl(Request $request) - { - $params = $request->query->all(); - $params['do'] = 'files'; - - if (isset($params['value']) && 0 === strpos($params['value'], '{{')) { - $value = str_replace(['{{file::', '}}'], '', $params['value']); - - /** @var FilesModel $adapter */ - $adapter = $this->framework->getAdapter(FilesModel::class); - - if (($model = $adapter->findByUuid($value)) instanceof FilesModel) { - $params['value'] = $model->path; - } - } - - return $this->route('contao_backend', $params); - } -} diff --git a/src/Menu/PagePickerProvider.php b/src/Menu/PagePickerProvider.php deleted file mode 100644 index dcb6882a06..0000000000 --- a/src/Menu/PagePickerProvider.php +++ /dev/null @@ -1,79 +0,0 @@ - - */ -class PagePickerProvider extends AbstractMenuProvider implements PickerMenuProviderInterface -{ - /** - * {@inheritdoc} - */ - public function supports($context) - { - return 'page' === $context || 'link' === $context; - } - - /** - * {@inheritdoc} - */ - public function createMenu(ItemInterface $menu, FactoryInterface $factory) - { - $user = $this->getUser(); - - if ($user->hasAccess('page', 'modules')) { - $this->addMenuItem($menu, $factory, 'page', 'pagePicker', 'pagemounts'); - } - } - - /** - * {@inheritdoc} - */ - public function supportsTable($table) - { - return 'tl_page' === $table; - } - - /** - * {@inheritdoc} - */ - public function processSelection($value) - { - return sprintf('{{link_url::%s}}', $value); - } - - /** - * {@inheritdoc} - */ - public function canHandle(Request $request) - { - return $request->query->has('value') && false !== strpos($request->query->get('value'), '{{link_url::'); - } - - /** - * {@inheritdoc} - */ - public function getPickerUrl(Request $request) - { - $params = $request->query->all(); - $params['do'] = 'page'; - $params['value'] = str_replace(['{{link_url::', '}}'], '', $params['value']); - - return $this->route('contao_backend', $params); - } -} diff --git a/src/Menu/PickerMenuBuilder.php b/src/Menu/PickerMenuBuilder.php deleted file mode 100644 index 0209018400..0000000000 --- a/src/Menu/PickerMenuBuilder.php +++ /dev/null @@ -1,130 +0,0 @@ - - */ -class PickerMenuBuilder implements PickerMenuBuilderInterface -{ - /** - * @var FactoryInterface - */ - private $factory; - - /** - * @var RendererInterface - */ - private $renderer; - - /** - * @var RouterInterface - */ - private $router; - - /** - * @var PickerMenuProviderInterface[] - */ - private $providers = []; - - /** - * Constructor. - * - * @param FactoryInterface $factory - * @param RendererInterface $renderer - * @param RouterInterface $router - */ - public function __construct(FactoryInterface $factory, RendererInterface $renderer, RouterInterface $router) - { - $this->factory = $factory; - $this->renderer = $renderer; - $this->router = $router; - } - - /** - * Adds a picker menu provider. - * - * @param PickerMenuProviderInterface $provider - */ - public function addProvider(PickerMenuProviderInterface $provider) - { - $this->providers[] = $provider; - } - - /** - * {@inheritdoc} - */ - public function createMenu($context) - { - $menu = $this->factory->createItem('picker'); - - foreach ($this->providers as $provider) { - if ($provider->supports($context)) { - $provider->createMenu($menu, $this->factory); - } - } - - if ($menu->count() > 1) { - return $this->renderer->render($menu); - } - - return ''; - } - - /** - * {@inheritdoc} - */ - public function supportsTable($table) - { - foreach ($this->providers as $provider) { - if ($provider->supportsTable($table)) { - return true; - } - } - - return false; - } - - /** - * {@inheritdoc} - */ - public function processSelection($table, $value) - { - foreach ($this->providers as $provider) { - if ($provider->supportsTable($table)) { - return $provider->processSelection($value); - } - } - - return $value; - } - - /** - * {@inheritdoc} - */ - public function getPickerUrl(Request $request) - { - foreach ($this->providers as $provider) { - if ($provider->canHandle($request)) { - return $provider->getPickerUrl($request); - } - } - - return $this->router->generate('contao_backend', array_merge(['do' => 'page'], $request->query->all())); - } -} diff --git a/src/Menu/PickerMenuBuilderInterface.php b/src/Menu/PickerMenuBuilderInterface.php deleted file mode 100644 index df8574dba8..0000000000 --- a/src/Menu/PickerMenuBuilderInterface.php +++ /dev/null @@ -1,58 +0,0 @@ - - */ -interface PickerMenuBuilderInterface -{ - /** - * Creates the menu. - * - * @param string $context - * - * @return string - */ - public function createMenu($context); - - /** - * Checks if a table is supported. - * - * @param string $table - * - * @return bool - */ - public function supportsTable($table); - - /** - * Processes the selected value. - * - * @param $table - * @param $value - * - * @return string - */ - public function processSelection($table, $value); - - /** - * Returns the picker URL. - * - * @param Request $request - * - * @return string - */ - public function getPickerUrl(Request $request); -} diff --git a/src/Menu/PickerMenuProviderInterface.php b/src/Menu/PickerMenuProviderInterface.php deleted file mode 100644 index a3d19dd776..0000000000 --- a/src/Menu/PickerMenuProviderInterface.php +++ /dev/null @@ -1,76 +0,0 @@ - - */ -interface PickerMenuProviderInterface -{ - /** - * Checks if a context is supported. - * - * @param string $context - * - * @return bool - */ - public function supports($context); - - /** - * Creates the menu. - * - * @param ItemInterface $menu - * @param FactoryInterface $factory - */ - public function createMenu(ItemInterface $menu, FactoryInterface $factory); - - /** - * Checks if a table is supported. - * - * @param string $table - * - * @return bool - */ - public function supportsTable($table); - - /** - * Processes the selected value. - * - * @param string $value - * - * @return string - */ - public function processSelection($value); - - /** - * Checks if a value can be handled. - * - * @param Request $request - * - * @return bool - */ - public function canHandle(Request $request); - - /** - * Returns the picker URL. - * - * @param Request $request - * - * @return string - */ - public function getPickerUrl(Request $request); -} diff --git a/src/Picker/AbstractPickerProvider.php b/src/Picker/AbstractPickerProvider.php new file mode 100644 index 0000000000..8ba5189793 --- /dev/null +++ b/src/Picker/AbstractPickerProvider.php @@ -0,0 +1,130 @@ + + */ +abstract class AbstractPickerProvider implements PickerProviderInterface +{ + /** + * @var FactoryInterface + */ + private $menuFactory; + + /** + * @var TokenStorageInterface + */ + private $tokenStorage; + + /** + * Constructor. + * + * @param FactoryInterface $menuFactory + */ + public function __construct(FactoryInterface $menuFactory) + { + $this->menuFactory = $menuFactory; + } + + /** + * {@inheritdoc} + */ + public function createMenuItem(PickerConfig $config) + { + $name = $this->getName(); + + $params = array_merge( + ['popup' => '1'], + $this->getRouteParameters($config), + ['picker' => $config->cloneForCurrent($name)->urlEncode()] + ); + + return $this->menuFactory->createItem( + $name, + [ + 'label' => $GLOBALS['TL_LANG']['MSC'][$name] ?: $name, + 'linkAttributes' => ['class' => $this->getLinkClass()], + 'current' => $this->isCurrent($config), + 'route' => 'contao_backend', + 'routeParameters' => $params, + ] + ); + } + + /** + * Sets the security token storage. + * + * @param TokenStorageInterface $tokenStorage + */ + public function setTokenStorage(TokenStorageInterface $tokenStorage) + { + $this->tokenStorage = $tokenStorage; + } + + /** + * {@inheritdoc} + */ + public function isCurrent(PickerConfig $config) + { + return $config->getCurrent() === $this->getName(); + } + + /** + * Returns the back end user object. + * + * @throws \RuntimeException + * + * @return BackendUser + */ + protected function getUser() + { + if (null === $this->tokenStorage) { + throw new \RuntimeException('No token storage provided'); + } + + $token = $this->tokenStorage->getToken(); + + if (null === $token) { + throw new \RuntimeException('No token provided'); + } + + $user = $token->getUser(); + + if (!($user instanceof BackendUser)) { + throw new \RuntimeException('The token does not contain a back end user object'); + } + + return $user; + } + + /** + * Returns the link class for the picker menu item. + * + * @return string + */ + abstract protected function getLinkClass(); + + /** + * Returns the routing parameters for the backend picker. + * + * @param PickerConfig $config + * + * @return array + */ + abstract protected function getRouteParameters(PickerConfig $config); +} diff --git a/src/Picker/ArticlePickerProvider.php b/src/Picker/ArticlePickerProvider.php new file mode 100644 index 0000000000..ab3f4d58a1 --- /dev/null +++ b/src/Picker/ArticlePickerProvider.php @@ -0,0 +1,89 @@ + + */ +class ArticlePickerProvider extends AbstractPickerProvider implements DcaPickerProviderInterface +{ + /** + * {@inheritdoc} + */ + public function getName() + { + return 'articlePicker'; + } + + /** + * {@inheritdoc} + */ + public function supportsContext($context) + { + return 'link' === $context && $this->getUser()->hasAccess('article', 'modules'); + } + + /** + * {@inheritdoc} + */ + public function supportsValue(PickerConfig $config) + { + return false !== strpos($config->getValue(), '{{article_url::'); + } + + /** + * {@inheritdoc} + */ + public function getDcaTable() + { + return 'tl_article'; + } + + /** + * {@inheritdoc} + */ + public function getDcaAttributes(PickerConfig $config) + { + $attributes = ['fieldType' => 'radio']; + + if ($this->supportsValue($config)) { + $attributes['value'] = str_replace(['{{article_url::', '}}'], '', $config->getValue()); + } + + return $attributes; + } + + /** + * {@inheritdoc} + */ + public function convertDcaValue(PickerConfig $config, $value) + { + return '{{article_url::'.$value.'}}'; + } + + /** + * {@inheritdoc} + */ + protected function getLinkClass() + { + return 'articles'; + } + + /** + * {@inheritdoc} + */ + protected function getRouteParameters(PickerConfig $config) + { + return ['do' => 'article']; + } +} diff --git a/src/Picker/DcaPickerProviderInterface.php b/src/Picker/DcaPickerProviderInterface.php new file mode 100644 index 0000000000..5ed1532e73 --- /dev/null +++ b/src/Picker/DcaPickerProviderInterface.php @@ -0,0 +1,48 @@ + + */ +interface DcaPickerProviderInterface extends PickerProviderInterface +{ + /** + * Returns the DCA table for this provider. + * + * @return bool + */ + public function getDcaTable(); + + /** + * Returns the attributes for the DataContainer. + * + * @param PickerConfig $config + * + * @return array + */ + public function getDcaAttributes(PickerConfig $config); + + /** + * Converts the DCA value for the picker selection. + * + * @param PickerConfig $config + * @param mixed $value + * + * @return mixed + */ + public function convertDcaValue(PickerConfig $config, $value); +} diff --git a/src/Picker/FilePickerProvider.php b/src/Picker/FilePickerProvider.php new file mode 100644 index 0000000000..b480f7336a --- /dev/null +++ b/src/Picker/FilePickerProvider.php @@ -0,0 +1,197 @@ + + */ +class FilePickerProvider extends AbstractPickerProvider implements DcaPickerProviderInterface, FrameworkAwareInterface +{ + use FrameworkAwareTrait; + + /** + * @var string + */ + private $uploadPath; + + /** + * Constructor. + * + * @param FactoryInterface $menuFactory + * @param string $uploadPath + */ + public function __construct(FactoryInterface $menuFactory, $uploadPath) + { + parent::__construct($menuFactory); + + $this->uploadPath = $uploadPath; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'filePicker'; + } + + /** + * {@inheritdoc} + */ + public function supportsContext($context) + { + return in_array($context, ['file', 'link'], true) && $this->getUser()->hasAccess('files', 'modules'); + } + + /** + * {@inheritdoc} + */ + public function supportsValue(PickerConfig $config) + { + if ('file' === $config->getContext()) { + return Validator::isUuid($config->getValue()); + } + + return false !== strpos($config->getValue(), '{{file::') + || 0 === strpos($config->getValue(), $this->uploadPath) + ; + } + + /** + * {@inheritdoc} + */ + public function getDcaTable() + { + return 'tl_files'; + } + + /** + * {@inheritdoc} + */ + public function getDcaAttributes(PickerConfig $config) + { + $value = $config->getValue(); + + if ('file' === $config->getContext()) { + $attributes = array_intersect_key( + $config->getExtras(), + array_flip(['fieldType', 'files', 'filesOnly', 'path', 'extensions']) + ); + + if ($value) { + $attributes['value'] = []; + + foreach (explode(',', $value) as $v) { + $attributes['value'][] = $this->urlEncode($this->convertValueToPath($v)); + } + } + + return $attributes; + } + + $attributes = [ + 'fieldType' => 'radio', + 'filesOnly' => true, + ]; + + if ($value) { + if (false !== strpos($value, '{{file::')) { + $value = str_replace(['{{file::', '}}'], '', $value); + } + + if (0 === strpos($value, $this->uploadPath.'/')) { + $attributes['value'] = $this->urlEncode($value); + } else { + $attributes['value'] = $this->urlEncode($this->convertValueToPath($value)); + } + } + + return $attributes; + } + + /** + * {@inheritdoc} + */ + public function convertDcaValue(PickerConfig $config, $value) + { + if ('file' === $config->getContext()) { + return $value; + } + + /** @var FilesModel $filesAdapter */ + $filesAdapter = $this->framework->getAdapter(FilesModel::class); + $filesModel = $filesAdapter->findByPath(rawurldecode($value)); + + if ($filesModel instanceof FilesModel) { + return '{{file::'.StringUtil::binToUuid($filesModel->uuid).'}}'; + } + + return $value; + } + + /** + * {@inheritdoc} + */ + protected function getLinkClass() + { + return 'filemounts'; + } + + /** + * {@inheritdoc} + */ + protected function getRouteParameters(PickerConfig $config) + { + return ['do' => 'files']; + } + + /** + * Converts the UUID value to a file path if possible. + * + * @param mixed $value + * + * @return string + */ + private function convertValueToPath($value) + { + /** @var FilesModel $filesAdapter */ + $filesAdapter = $this->framework->getAdapter(FilesModel::class); + + if (Validator::isUuid($value) && ($filesModel = $filesAdapter->findByUuid($value)) instanceof FilesModel) { + return $filesModel->path; + } + + return $value; + } + + /** + * Urlencodes a file path preserving slashes. + * + * @param string $strPath + * + * @return string + * + * @see \Contao\System::urlEncode() + */ + private function urlEncode($strPath) + { + return str_replace('%2F', '/', rawurlencode($strPath)); + } +} diff --git a/src/Picker/PagePickerProvider.php b/src/Picker/PagePickerProvider.php new file mode 100644 index 0000000000..a80fcd0476 --- /dev/null +++ b/src/Picker/PagePickerProvider.php @@ -0,0 +1,113 @@ + + */ +class PagePickerProvider extends AbstractPickerProvider implements DcaPickerProviderInterface +{ + /** + * {@inheritdoc} + */ + public function getName() + { + return 'pagePicker'; + } + + /** + * {@inheritdoc} + */ + public function supportsContext($context) + { + return in_array($context, ['page', 'link'], true) && $this->getUser()->hasAccess('page', 'modules'); + } + + /** + * {@inheritdoc} + */ + public function supportsValue(PickerConfig $config) + { + if ('page' === $config->getContext()) { + return is_numeric($config->getValue()); + } + + return false !== strpos($config->getValue(), '{{link_url::'); + } + + /** + * {@inheritdoc} + */ + public function getDcaTable() + { + return 'tl_page'; + } + + /** + * {@inheritdoc} + */ + public function getDcaAttributes(PickerConfig $config) + { + $value = $config->getValue(); + + if ('page' === $config->getContext()) { + $attributes = ['fieldType' => $config->getExtra('fieldType')]; + + if (is_array($rootNodes = $config->getExtra('rootNodes'))) { + $attributes['rootNodes'] = $rootNodes; + } + + if ($value) { + $attributes['value'] = array_map('intval', explode(',', $value)); + } + + return $attributes; + } + + $attributes = ['fieldType' => 'radio']; + + if ($value && false !== strpos($value, '{{link_url::')) { + $attributes['value'] = str_replace(['{{link_url::', '}}'], '', $value); + } + + return $attributes; + } + + /** + * {@inheritdoc} + */ + public function convertDcaValue(PickerConfig $config, $value) + { + if ('page' === $config->getContext()) { + return (int) $value; + } + + return '{{link_url::'.$value.'}}'; + } + + /** + * {@inheritdoc} + */ + protected function getLinkClass() + { + return 'pagemounts'; + } + + /** + * {@inheritdoc} + */ + protected function getRouteParameters(PickerConfig $config) + { + return ['do' => 'page']; + } +} diff --git a/src/Picker/Picker.php b/src/Picker/Picker.php new file mode 100644 index 0000000000..0e0f1b74f4 --- /dev/null +++ b/src/Picker/Picker.php @@ -0,0 +1,122 @@ + + */ +class Picker implements PickerInterface +{ + /** + * @var FactoryInterface + */ + private $menuFactory; + + /** + * @var PickerProviderInterface[] + */ + private $providers; + + /** + * @var PickerConfig + */ + private $config; + + /** + * @var ItemInterface + */ + private $menu; + + /** + * Constructor. + * + * @param FactoryInterface $menuFactory + * @param PickerProviderInterface[] $providers + * @param PickerConfig $config + */ + public function __construct(FactoryInterface $menuFactory, array $providers, PickerConfig $config) + { + $this->menuFactory = $menuFactory; + $this->providers = $providers; + $this->config = $config; + } + + /** + * {@inheritdoc} + */ + public function getConfig() + { + return $this->config; + } + + /** + * {@inheritdoc} + */ + public function getMenu() + { + if (null !== $this->menu) { + return $this->menu; + } + + $this->menu = $this->menuFactory->createItem('picker'); + + foreach ($this->providers as $provider) { + $item = $provider->createMenuItem($this->config); + $item->setExtra('provider', $provider); + + $this->menu->addChild($item); + } + + return $this->menu; + } + + /** + * {@inheritdoc} + */ + public function getCurrentProvider() + { + foreach ($this->providers as $provider) { + if ($provider->isCurrent($this->config)) { + return $provider; + } + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function getCurrentUrl() + { + $menu = $this->getMenu(); + + if (!$menu->count()) { + throw new \RuntimeException('No picker menu items found.'); + } + + /** @var ItemInterface[] $menu */ + foreach ($menu as $item) { + $picker = $item->getExtra('provider'); + + if ($picker instanceof PickerProviderInterface && $picker->supportsValue($this->config)) { + return $item->getUri(); + } + } + + return $menu->getFirstChild()->getUri(); + } +} diff --git a/src/Picker/PickerBuilder.php b/src/Picker/PickerBuilder.php new file mode 100644 index 0000000000..5f7366d523 --- /dev/null +++ b/src/Picker/PickerBuilder.php @@ -0,0 +1,143 @@ + + */ +class PickerBuilder implements PickerBuilderInterface +{ + /** + * @var FactoryInterface + */ + private $menuFactory; + + /** + * @var RouterInterface + */ + private $router; + + /** + * @var RequestStack + */ + private $requestStack; + + /** + * @var PickerProviderInterface[] + */ + private $providers = []; + + /** + * Constructor. + * + * @param FactoryInterface $menuFactory + * @param RouterInterface $router + * @param RequestStack $requestStack + */ + public function __construct(FactoryInterface $menuFactory, RouterInterface $router, RequestStack $requestStack) + { + $this->menuFactory = $menuFactory; + $this->router = $router; + $this->requestStack = $requestStack; + } + + /** + * Adds a picker provider. + * + * @param PickerProviderInterface $provider + */ + public function addProvider(PickerProviderInterface $provider) + { + $this->providers[$provider->getName()] = $provider; + } + + /** + * {@inheritdoc} + */ + public function create(PickerConfig $config) + { + $providers = $this->providers; + + if (is_array($allowed = $config->getExtra('providers'))) { + $providers = array_intersect_key($providers, array_flip($allowed)); + } + + $providers = array_filter( + $providers, + function (PickerProviderInterface $provider) use ($config) { + return $provider->supportsContext($config->getContext()); + } + ); + + if (empty($providers)) { + return null; + } + + return new Picker($this->menuFactory, $providers, $config); + } + + /** + * {@inheritdoc} + */ + public function createFromData($data) + { + try { + $config = PickerConfig::urlDecode($data); + } catch (\InvalidArgumentException $e) { + return null; + } + + return $this->create($config); + } + + /** + * {@inheritdoc} + */ + public function supportsContext($context, array $allowed = null) + { + $providers = $this->providers; + + if (null !== $allowed) { + $providers = array_intersect_key($providers, array_flip($allowed)); + } + + foreach ($providers as $provider) { + if ($provider->supportsContext($context)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getUrl($context, array $extras = [], $value = '') + { + $providers = (isset($extras['providers']) && is_array($extras['providers'])) ? $extras['providers'] : null; + + if (!$this->supportsContext($context, $providers)) { + return ''; + } + + return $this->router->generate( + 'contao_backend_picker', + ['context' => $context, 'extras' => $extras, 'value' => $value] + ); + } +} diff --git a/src/Picker/PickerBuilderInterface.php b/src/Picker/PickerBuilderInterface.php new file mode 100644 index 0000000000..9353a850bc --- /dev/null +++ b/src/Picker/PickerBuilderInterface.php @@ -0,0 +1,58 @@ + + */ +interface PickerBuilderInterface +{ + /** + * Returns a picker or null if the context is not supported. + * + * @param PickerConfig $config + * + * @return PickerInterface|null + */ + public function create(PickerConfig $config); + + /** + * Returns a picker object from encoded URL data. + * + * @param string $data + * + * @return PickerInterface|null + */ + public function createFromData($data); + + /** + * Returns whether the given context is supported. + * + * @param string $context + * @param array|null $allowed + * + * @return bool + */ + public function supportsContext($context, array $allowed = null); + + /** + * Returns the picker URL for the given context and configuration. + * + * @param string $context + * @param array $extras + * @param string $value + * + * @return string + */ + public function getUrl($context, array $extras = [], $value = ''); +} diff --git a/src/Picker/PickerConfig.php b/src/Picker/PickerConfig.php new file mode 100644 index 0000000000..aa93a1cb75 --- /dev/null +++ b/src/Picker/PickerConfig.php @@ -0,0 +1,185 @@ + + */ +class PickerConfig implements \JsonSerializable +{ + /** + * @var string + */ + private $context; + + /** + * @var array + */ + private $extras = []; + + /** + * @var string + */ + private $value; + + /** + * @var string + */ + private $current; + + /** + * Constructor. + * + * @param string $context + * @param array $extras + * @param string $value + * @param string $current + */ + public function __construct($context, array $extras = [], $value = '', $current = '') + { + $this->context = $context; + $this->extras = $extras; + $this->value = $value; + $this->current = $current; + } + + /** + * Returns the context. + * + * @return string + */ + public function getContext() + { + return $this->context; + } + + /** + * Returns the extras. + * + * @return array + */ + public function getExtras() + { + return $this->extras; + } + + /** + * Returns the value. + * + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * Returns the alias of the current picker. + * + * @return string + */ + public function getCurrent() + { + return $this->current; + } + + /** + * Returns an extra by name. + * + * @param string $name + * + * @return mixed + */ + public function getExtra($name) + { + return isset($this->extras[$name]) ? $this->extras[$name] : null; + } + + /** + * Sets an extra. + * + * @param string $name + * @param mixed $value + */ + public function setExtra($name, $value) + { + $this->extras[$name] = $value; + } + + /** + * Duplicates the configuration and overrides the current picker alias. + * + * @param string $current + * + * @return PickerConfig + */ + public function cloneForCurrent($current) + { + return new self($this->context, $this->extras, $this->value, $current); + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() + { + return [ + 'context' => $this->context, + 'extras' => $this->extras, + 'current' => $this->current, + 'value' => $this->value, + ]; + } + + /** + * Encodes the picker configuration for the URL. + * + * @return string + */ + public function urlEncode() + { + $data = json_encode($this); + + if (function_exists('gzencode') && false !== ($encoded = @gzencode($data))) { + $data = $encoded; + } + + return base64_encode($data); + } + + /** + * Initializes the object from the URL data. + * + * @param string $data + * + * @throws \InvalidArgumentException + * + * @return PickerConfig + */ + public static function urlDecode($data) + { + $data = base64_decode($data, true); + + if (function_exists('gzdecode') && false !== ($uncompressed = @gzdecode($data))) { + $data = $uncompressed; + } + + $data = @json_decode($data, true); + + if (null === $data) { + throw new \InvalidArgumentException('Invalid JSON data'); + } + + return new self($data['context'], $data['extras'], $data['value'], $data['current']); + } +} diff --git a/src/Picker/PickerInterface.php b/src/Picker/PickerInterface.php new file mode 100644 index 0000000000..b4bc5aef1c --- /dev/null +++ b/src/Picker/PickerInterface.php @@ -0,0 +1,49 @@ + + */ +interface PickerInterface +{ + /** + * Returns the picker configuration. + * + * @return PickerConfig + */ + public function getConfig(); + + /** + * Returns the picker menu. + * + * @return ItemInterface + */ + public function getMenu(); + + /** + * Returns the current provider. + * + * @return PickerProviderInterface|null + */ + public function getCurrentProvider(); + + /** + * Returns the URL to the current picker tab. + * + * @return string + */ + public function getCurrentUrl(); +} diff --git a/src/Picker/PickerProviderInterface.php b/src/Picker/PickerProviderInterface.php new file mode 100644 index 0000000000..21111ebf60 --- /dev/null +++ b/src/Picker/PickerProviderInterface.php @@ -0,0 +1,64 @@ + + */ +interface PickerProviderInterface +{ + /** + * Returns the unique name for this picker. + * + * @return string + */ + public function getName(); + + /** + * Creates the menu item for this picker. + * + * @param PickerConfig $config + * + * @return ItemInterface + */ + public function createMenuItem(PickerConfig $config); + + /** + * Returns whether the picker supports the given context. + * + * @param string $context + * + * @return bool + */ + public function supportsContext($context); + + /** + * Returns whether the picker supports the given value. + * + * @param PickerConfig $config + * + * @return bool + */ + public function supportsValue(PickerConfig $config); + + /** + * Returns whether the picker is currently active. + * + * @param PickerConfig $config + * + * @return bool + */ + public function isCurrent(PickerConfig $config); +} diff --git a/src/Resources/config/services.yml b/src/Resources/config/services.yml index 18c8093b06..31180218e9 100644 --- a/src/Resources/config/services.yml +++ b/src/Resources/config/services.yml @@ -133,70 +133,67 @@ services: class: Knp\Menu\Matcher\Matcher public: false - contao.menu.picker_menu_builder: - class: Contao\CoreBundle\Menu\PickerMenuBuilder + contao.menu.renderer: + class: Knp\Menu\Renderer\ListRenderer arguments: - - "@knp_menu.factory" - - "@contao.menu.renderer" - - "@router" - tags: - - { name: knp_menu.menu_builder, method: createMenu, alias: picker } + - "@contao.menu.matcher" - contao.menu.page_picker_provider: - class: Contao\CoreBundle\Menu\PagePickerProvider + contao.monolog.handler: + class: Contao\CoreBundle\Monolog\ContaoTableHandler public: false arguments: - - "@router" - - "@request_stack" - - "@security.token_storage" + - debug + - false tags: - - { name: contao.picker_menu_provider, priority: 192 } + - { name: monolog.logger, channel: contao } - contao.menu.file_picker_provider: - class: Contao\CoreBundle\Menu\FilePickerProvider + contao.monolog.processor: + class: Contao\CoreBundle\Monolog\ContaoTableProcessor public: false arguments: - - "@router" - "@request_stack" - "@security.token_storage" - - "%contao.upload_path%" + - "@contao.routing.scope_matcher" tags: - - { name: contao.picker_menu_provider, priority: 160 } + - { name: monolog.processor } - contao.menu.article_picker_provider: - class: Contao\CoreBundle\Menu\ArticlePickerProvider - public: false + contao.picker.builder: + class: Contao\CoreBundle\Picker\PickerBuilder arguments: + - "@knp_menu.factory" - "@router" - "@request_stack" - - "@security.token_storage" - tags: - - { name: contao.picker_menu_provider } - contao.menu.renderer: - class: Knp\Menu\Renderer\ListRenderer + contao.picker.page_provider: + class: Contao\CoreBundle\Picker\PagePickerProvider public: false arguments: - - "@contao.menu.matcher" + - "@knp_menu.factory" + calls: + - [setTokenStorage, ["@security.token_storage"]] + tags: + - { name: contao.picker_provider, priority: 192 } - contao.monolog.handler: - class: Contao\CoreBundle\Monolog\ContaoTableHandler + contao.picker.file_provider: + class: Contao\CoreBundle\Picker\FilePickerProvider public: false arguments: - - debug - - false + - "@knp_menu.factory" + - "%contao.upload_path%" + calls: + - [setTokenStorage, ["@security.token_storage"]] tags: - - { name: monolog.logger, channel: contao } + - { name: contao.picker_provider, priority: 160 } - contao.monolog.processor: - class: Contao\CoreBundle\Monolog\ContaoTableProcessor + contao.picker.article_provider: + class: Contao\CoreBundle\Picker\ArticlePickerProvider public: false arguments: - - "@request_stack" - - "@security.token_storage" - - "@contao.routing.scope_matcher" + - "@knp_menu.factory" + calls: + - [setTokenStorage, ["@security.token_storage"]] tags: - - { name: monolog.processor } + - { name: contao.picker_provider } contao.referer_id.manager: class: Symfony\Component\Security\Csrf\CsrfTokenManager diff --git a/src/Resources/contao/classes/Ajax.php b/src/Resources/contao/classes/Ajax.php index cbfd3830de..e9873e6972 100644 --- a/src/Resources/contao/classes/Ajax.php +++ b/src/Resources/contao/classes/Ajax.php @@ -433,10 +433,6 @@ public function executePostActions(DataContainer $dc) throw new NoContentResponseException(); - // DCA picker - case 'processPickerSelection': - throw new ResponseException(new Response(\System::getContainer()->get('contao.menu.picker_menu_builder')->processSelection(\Input::post('table'), \Input::post('value')))); - // DropZone file upload case 'fileupload': $dc->move(true); diff --git a/src/Resources/contao/classes/Backend.php b/src/Resources/contao/classes/Backend.php index 7ed2ad637b..1a988a7758 100644 --- a/src/Resources/contao/classes/Backend.php +++ b/src/Resources/contao/classes/Backend.php @@ -12,6 +12,7 @@ use Contao\CoreBundle\Exception\AccessDeniedException; use Contao\CoreBundle\Exception\ResponseException; +use Contao\CoreBundle\Picker\PickerInterface; use Contao\Database\Result; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Response; @@ -285,13 +286,14 @@ public static function handleRunOnce() /** * Open a back end module and return it as HTML * - * @param string $module + * @param string $module + * @param PickerInterface|null $picker * * @return string * * @throws AccessDeniedException */ - protected function getBackendModule($module) + protected function getBackendModule($module, PickerInterface $picker = null) { $arrModule = array(); @@ -403,6 +405,11 @@ protected function getBackendModule($module) /** @var DataContainer $dc */ $dc = new $dataContainer($strTable, $arrModule); + + if ($picker !== null && $dc instanceof DataContainer) + { + $dc->initPicker($picker); + } } // Wrap the existing headline @@ -1077,49 +1084,42 @@ public static function convertLayoutSectionIdsToAssociativeArray($arrSections) /** * Generate the DCA picker wizard * - * @param boolean|array $config + * @param boolean|array $extras * @param string $table * @param string $field - * @param integer $id - * @param string $value * @param string $inputName * * @return string */ - public static function getDcaPickerWizard($config, $table, $field, $id, $value, $inputName) + public static function getDcaPickerWizard($extras, $table, $field, $inputName) { - $params = array(); + $context = 'link'; + $extras = is_array($extras) ? $extras : array(); + $providers = (isset($extras['providers']) && is_array($extras['providers'])) ? $extras['providers'] : null; - if (is_array($config) && isset($config['do'])) + if (isset($extras['context'])) { - $params['do'] = $config['do']; + $context = 'link'; + unset($extras['context']); } - $params['context'] = 'link'; - $params['target'] = $table.'.'.$field.'.'.$id; - $params['value'] = $value; - $params['popup'] = 1; + $factory = \System::getContainer()->get('contao.picker.builder'); - if (is_array($config) && isset($config['context'])) + if (!$factory->supportsContext($context, $providers)) { - $params['context'] = $config['context']; + return ''; } - return ' ' . \Image::getHtml((is_array($config) && isset($config['icon']) ? $config['icon'] : 'pickpage.svg'), $GLOBALS['TL_LANG']['MSC']['pagepicker']) . ' + return ' ' . \Image::getHtml((is_array($extras) && isset($extras['icon']) ? $extras['icon'] : 'pickpage.svg'), $GLOBALS['TL_LANG']['MSC']['pagepicker']) . ' '; } - $return .= (($this->strPickerField && $this->strPickerFieldType == 'radio') ? ' + $return .= ($this->strPickerFieldType == 'radio' ? '
- +
' : '').' '; @@ -4710,11 +4705,11 @@ protected function listView()
' : '').' -
'.((\Input::get('act') == 'select' || ($this->strPickerField && $this->strPickerFieldType == 'checkbox')) ? ' +
'.((\Input::get('act') == 'select' || $this->strPickerFieldType == 'checkbox') ? '
' : '').' -getPickerAttributes() . '>'; +
'; // Automatically add the "order by" field as last column if we do not have group headers if ($GLOBALS['TL_DCA'][$this->strTable]['list']['label']['showColumns']) @@ -4929,15 +4924,15 @@ protected function listView() // Buttons ($row, $table, $root, $blnCircularReference, $childs, $previous, $next) $return .= ((\Input::get('act') == 'select') ? ' ' : ' - ') . ' + ') . ' '; } // Close the table $return .= ' -
'.$this->generateButtons($row, $this->strTable, $this->root).($this->strPickerField ? $this->getPickerInputField($row['id']) : '').''.$this->generateButtons($row, $this->strTable, $this->root).($this->strPickerFieldType ? $this->getPickerInputField($row['id']) : '').'
'.(($this->strPickerField && $this->strPickerFieldType == 'radio') ? ' +'.($this->strPickerFieldType == 'radio' ? '
- +
' : '').'
'; @@ -6123,4 +6118,34 @@ protected function formatGroupHeader($field, $value, $mode, $row) return $group; } + + /** + * {@inheritdoc} + */ + public function initPicker(PickerInterface $picker) + { + $attributes = parent::initPicker($picker); + + if (null === $attributes) + { + return null; + } + + // Predefined node set (see #3563) + if (isset($attributes['rootNodes'])) + { + $arrRoot = (array) $attributes['rootNodes']; + + // Allow only those roots that are allowed in root nodes + if (!empty($this->root)) + { + $arrRoot = array_intersect($arrRoot, $this->Database->getChildRecords($GLOBALS['TL_DCA'][$this->strTable]['list']['sorting']['root'], $this->strTable)); + $arrRoot = $this->eliminateNestedPages($arrRoot); + } + + $this->root = $arrRoot; + } + + return $attributes; + } } diff --git a/src/Resources/contao/templates/backend/be_tinyFlash.html5 b/src/Resources/contao/templates/backend/be_tinyFlash.html5 index e12182c36c..f1e1e87ece 100644 --- a/src/Resources/contao/templates/backend/be_tinyFlash.html5 +++ b/src/Resources/contao/templates/backend/be_tinyFlash.html5 @@ -16,6 +16,7 @@ setTimeout(function() { forced_root_block: false, document_base_url: '', entities: '160,nbsp,60,lt,62,gt,173,shy', + branding: false, setup: function(editor) { editor.getElement().removeAttribute('required'); }, @@ -28,7 +29,7 @@ setTimeout(function() { file_browser_callback: function(field_name, url, type, win) { Backend.openModalBrowser(field_name, url, type, win); }, - branding: false, + file_browser_callback_types: fileBrowserTypes) ?>, plugins: 'autosave charmap code fullscreen image legacyoutput link lists paste searchreplace tabfocus visualblocks', browser_spellcheck: true, tabfocus_elements: ':prev,:next', diff --git a/src/Resources/contao/templates/backend/be_tinyMCE.html5 b/src/Resources/contao/templates/backend/be_tinyMCE.html5 index 94a9f53c66..29dbfdd237 100644 --- a/src/Resources/contao/templates/backend/be_tinyMCE.html5 +++ b/src/Resources/contao/templates/backend/be_tinyMCE.html5 @@ -15,6 +15,7 @@ setTimeout(function() { element_format: 'html', document_base_url: '', entities: '160,nbsp,60,lt,62,gt,173,shy', + branding: false, setup: function(editor) { editor.getElement().removeAttribute('required'); }, @@ -27,7 +28,7 @@ setTimeout(function() { file_browser_callback: function(field_name, url, type, win) { Backend.openModalBrowser(field_name, url, type, win); }, - branding: false, + file_browser_callback_types: fileBrowserTypes) ?>, plugins: 'autosave charmap code fullscreen image importcss link lists paste searchreplace tabfocus table visualblocks', browser_spellcheck: true, tabfocus_elements: ':prev,:next', diff --git a/src/Resources/contao/templates/backend/be_tinyNews.html5 b/src/Resources/contao/templates/backend/be_tinyNews.html5 index af2e4d758b..f5564d6c11 100644 --- a/src/Resources/contao/templates/backend/be_tinyNews.html5 +++ b/src/Resources/contao/templates/backend/be_tinyNews.html5 @@ -15,6 +15,7 @@ setTimeout(function() { element_format: 'html', document_base_url: '', entities: '160,nbsp,60,lt,62,gt,173,shy', + branding: false, setup: function(editor) { editor.getElement().removeAttribute('required'); }, @@ -27,7 +28,7 @@ setTimeout(function() { file_browser_callback: function(field_name, url, type, win) { Backend.openModalBrowser(field_name, url, type, win); }, - branding: false, + file_browser_callback_types: fileBrowserTypes) ?>, doctype: '', plugins: 'autosave charmap code fullscreen image link lists paste searchreplace tabfocus table visualblocks', browser_spellcheck: true, diff --git a/src/Resources/contao/widgets/FileTree.php b/src/Resources/contao/widgets/FileTree.php index 4153906e33..e6ad228b84 100644 --- a/src/Resources/contao/widgets/FileTree.php +++ b/src/Resources/contao/widgets/FileTree.php @@ -10,9 +10,6 @@ namespace Contao; -use Contao\CoreBundle\DataContainer\DcaFilterInterface; - - /** * Provide methods to handle input field "file tree". * @@ -28,7 +25,7 @@ * * @author Leo Feyer */ -class FileTree extends \Widget implements DcaFilterInterface +class FileTree extends \Widget { /** @@ -83,46 +80,6 @@ public function __construct($arrAttributes=null) } - /** - * {@inheritdoc} - */ - public function getDcaFilter() - { - $arrFilters = array(); - - // Show files in file tree - if ($this->files) - { - $arrFilters['files'] = true; - } - - // Only files can be selected - if ($this->filesOnly) - { - $arrFilters['filesOnly'] = true; - } - - // Only files within a custom path can be selected - if ($this->path) - { - $arrFilters['root'] = array($this->path); - } - - // Only certain file types can be selected - if ($this->extensions) - { - $arrFilters['extensions'] = $this->extensions; - } - - if ($this->fieldType) - { - $arrFilters['fieldType'] = $this->fieldType; - } - - return $arrFilters; - } - - /** * Return an array if the "multiple" attribute is set * @@ -406,14 +363,46 @@ public function generate() $return .= '
  • '.$v.'
  • '; } - $return .= ' -

    '.$GLOBALS['TL_LANG']['MSC']['changeSelection'].'

    + $return .= ''; + + if (!\System::getContainer()->get('contao.picker.builder')->supportsContext('file')) + { + $return .= ' +

    '; + } + else + { + $extras = array('fieldType'=>$this->fieldType); + + if ($this->files) + { + $extras['files'] = (bool) $this->files; + } + + if ($this->filesOnly) + { + $extras['filesOnly'] = (bool) $this->filesOnly; + } + + if ($this->path) + { + $extras['path'] = (string) $this->path; + } + + if ($this->extensions) + { + $extras['extensions'] = (string) $this->extensions; + } + + $return .= ' +

    '.$GLOBALS['TL_LANG']['MSC']['changeSelection'].'

    ' . ($blnHasOrder ? ' - ' : '') . ' -
    '; + ' : ''); + } - $return = '
    ' . $return . '
    '; + $return = '
    ' . $return . '
    '; return $return; } diff --git a/src/Resources/contao/widgets/PageTree.php b/src/Resources/contao/widgets/PageTree.php index 6ae447eb64..574aff6e25 100644 --- a/src/Resources/contao/widgets/PageTree.php +++ b/src/Resources/contao/widgets/PageTree.php @@ -10,8 +10,6 @@ namespace Contao; -use Contao\CoreBundle\DataContainer\DcaFilterInterface; - /** * Provide methods to handle input field "page tree". @@ -23,7 +21,7 @@ * * @author Leo Feyer */ -class PageTree extends \Widget implements DcaFilterInterface +class PageTree extends \Widget { /** @@ -78,44 +76,6 @@ public function __construct($arrAttributes=null) } - /** - * {@inheritdoc} - */ - public function getDcaFilter() - { - $arrFilters = array(); - - // Predefined node set (see #3563) - if (is_array($this->rootNodes)) - { - // Allow only those roots that are allowed in root nodes - if (!empty($GLOBALS['TL_DCA']['tl_page']['list']['sorting']['root'])) - { - $root = array_intersect(array_merge($this->rootNodes, $this->Database->getChildRecords($this->rootNodes, 'tl_page')), $GLOBALS['TL_DCA']['tl_page']['list']['sorting']['root']); - - if (empty($root)) - { - $root = $this->rootNodes; - $GLOBALS['TL_DCA']['tl_page']['list']['sorting']['breadcrumb'] = ''; // hide the breadcrumb menu - } - - $arrFilters['root'] = $this->eliminateNestedPages($root); - } - else - { - $arrFilters['root'] = $this->eliminateNestedPages($this->rootNodes); - } - } - - if ($this->fieldType) - { - $arrFilters['fieldType'] = $this->fieldType; - } - - return $arrFilters; - } - - /** * Return an array if the "multiple" attribute is set * @@ -260,30 +220,47 @@ public function generate() $return .= '
  • '.$v.'
  • '; } - $return .= ' -

    '.$GLOBALS['TL_LANG']['MSC']['changeSelection'].'

    - ' . ($blnHasOrder ? ' - ' : '') . ' - '; - - $return = '
    ' . $return . '
    '; + $return .= ''; + + if (!\System::getContainer()->get('contao.picker.builder')->supportsContext('page')) + { + $return .= ' +

    '; + } + else + { + $extras = ['fieldType' => $this->fieldType]; + + if (is_array($this->rootNodes)) + { + $extras['rootNodes'] = array_values($this->rootNodes); + } + + $return .= ' +

    '.$GLOBALS['TL_LANG']['MSC']['changeSelection'].'

    + ' . ($blnHasOrder ? ' + ' : ''); + } + + $return = '
    ' . $return . '
    '; return $return; } diff --git a/src/Resources/public/core.js b/src/Resources/public/core.js index 9bf4db533c..9a7f94c7c9 100644 --- a/src/Resources/public/core.js +++ b/src/Resources/public/core.js @@ -968,23 +968,18 @@ var Backend = /** * Open a TinyMCE file browser in a modal window * - * @param {string} field_name The field name - * @param {string} url The URL - * @param {string} type The picker type - * @param {object} win The window object - * @param {string} [reference] An optional reference field + * @param {string} field_name The field name + * @param {string} url The URL + * @param {string} type The picker type + * @param {object} win The window object */ - openModalBrowser: function(field_name, url, type, win, reference) { + openModalBrowser: function(field_name, url, type, win) { Backend.openModalSelector({ + 'id': 'tl_listing', 'title': win.document.getElement('div.mce-title').get('text'), - 'url': document.location.pathname.replace('/contao', '/_contao') + '/picker?' + (type == 'file' ? 'do=page&context=link' : 'do=files&context=file') + '&target=' + (reference || 'tl_content.singleSRC') + '&value=' + url + '&popup=1', + 'url': document.location.pathname.replace('/contao', '/_contao') + '/picker?context=' + (type == 'file' ? 'link' : 'file') + '&extras[fieldType]=radio&extras[filesOnly]=true&value=' + url + '&popup=1', 'callback': function(table, value) { - new Request.Contao({ - evalScripts: false, - onSuccess: function(txt, json) { - win.document.getElementById(field_name).value = json.content; - } - }).post({'action':'processPickerSelection', 'table':table, 'value':value.join(','), 'REQUEST_TOKEN':Contao.request_token}); + win.document.getElementById(field_name).value = value.join(','); } }); }, diff --git a/src/Resources/public/core.min.js b/src/Resources/public/core.min.js index adcb0ae8e9..2d5ffa6ea3 100644 --- a/src/Resources/public/core.min.js +++ b/src/Resources/public/core.min.js @@ -1,3 +1,2 @@ -/* Contao Open Source CMS, (c) 2005-2017 Leo Feyer, LGPL-3.0+ */ -var AjaxRequest={themePath:Contao.script_url+"system/themes/"+Contao.theme+"/",toggleNavigation:function(e,t,n){e.blur();var a=$(t),o=$(e).getParent("li");return!!a&&(o.hasClass("node-collapsed")?(a.setStyle("display",null),o.removeClass("node-collapsed").addClass("node-expanded"),$(e).store("tip:title",Contao.lang.collapse),new Request.Contao({url:n}).post({action:"toggleNavigation",id:t,state:1,REQUEST_TOKEN:Contao.request_token})):(a.setStyle("display","none"),o.removeClass("node-expanded").addClass("node-collapsed"),$(e).store("tip:title",Contao.lang.expand),new Request.Contao({url:n}).post({action:"toggleNavigation",id:t,state:0,REQUEST_TOKEN:Contao.request_token})),!1)},toggleStructure:function(e,t,n,a){e.blur();var o=$(t),i=$(e).getFirst("img");return o?("none"==o.getStyle("display")?(o.setStyle("display",null),i.src=AjaxRequest.themePath+"icons/folMinus.svg",$(e).store("tip:title",Contao.lang.collapse),new Request.Contao({field:e}).post({action:"toggleStructure",id:t,state:1,REQUEST_TOKEN:Contao.request_token})):(o.setStyle("display","none"),i.src=AjaxRequest.themePath+"icons/folPlus.svg",$(e).store("tip:title",Contao.lang.expand),new Request.Contao({field:e}).post({action:"toggleStructure",id:t,state:0,REQUEST_TOKEN:Contao.request_token})),!1):(new Request.Contao({field:e,evalScripts:!0,onRequest:AjaxRequest.displayBox(Contao.lang.loading+" …"),onSuccess:function(o){var l=new Element("li",{id:t,class:"parent",styles:{display:"inline"}});if(new Element("ul",{class:"level_"+n,html:o}).inject(l,"bottom"),5==a)l.inject($(e).getParent("li"),"after");else{for(var s,c=!1,r=$(e).getParent("li");"element"==typeOf(r)&&(s=r.getNext("li"));)if(r=s,r.hasClass("tl_folder")){c=!0;break}c?l.inject(r,"before"):l.inject(r,"after")}l.getElements("a").each(function(e){e.href=e.href.replace(/&ref=[a-f0-9]+/,"&ref="+Contao.referer_id)}),$(e).store("tip:title",Contao.lang.collapse),i.src=AjaxRequest.themePath+"icons/folMinus.svg",window.fireEvent("structure"),AjaxRequest.hideBox(),window.fireEvent("ajax_change")}}).post({action:"loadStructure",id:t,level:n,state:1,REQUEST_TOKEN:Contao.request_token}),!1)},toggleFileManager:function(e,t,n,a){e.blur();var o=$(t),i=$(e).getFirst("img");return o?("none"==o.getStyle("display")?(o.setStyle("display",null),i.src=AjaxRequest.themePath+"icons/folMinus.svg",$(e).store("tip:title",Contao.lang.collapse),new Request.Contao({field:e}).post({action:"toggleFileManager",id:t,state:1,REQUEST_TOKEN:Contao.request_token})):(o.setStyle("display","none"),i.src=AjaxRequest.themePath+"icons/folPlus.svg",$(e).store("tip:title",Contao.lang.expand),new Request.Contao({field:e}).post({action:"toggleFileManager",id:t,state:0,REQUEST_TOKEN:Contao.request_token})),!1):(new Request.Contao({field:e,evalScripts:!0,onRequest:AjaxRequest.displayBox(Contao.lang.loading+" …"),onSuccess:function(n){var o=new Element("li",{id:t,class:"parent",styles:{display:"inline"}});new Element("ul",{class:"level_"+a,html:n}).inject(o,"bottom"),o.inject($(e).getParent("li"),"after"),o.getElements("a").each(function(e){e.href=e.href.replace(/&ref=[a-f0-9]+/,"&ref="+Contao.referer_id)}),$(e).store("tip:title",Contao.lang.collapse),i.src=AjaxRequest.themePath+"icons/folMinus.svg",AjaxRequest.hideBox(),window.fireEvent("ajax_change")}}).post({action:"loadFileManager",id:t,level:a,folder:n,state:1,REQUEST_TOKEN:Contao.request_token}),!1)},togglePagetree:function(e,t,n,a,o){e.blur(),Backend.getScrollOffset();var i=$(t),l=$(e).getFirst("img");return i?("none"==i.getStyle("display")?(i.setStyle("display",null),l.src=AjaxRequest.themePath+"icons/folMinus.svg",$(e).store("tip:title",Contao.lang.collapse),new Request.Contao({field:e}).post({action:"togglePagetree",id:t,state:1,REQUEST_TOKEN:Contao.request_token})):(i.setStyle("display","none"),l.src=AjaxRequest.themePath+"icons/folPlus.svg",$(e).store("tip:title",Contao.lang.expand),new Request.Contao({field:e}).post({action:"togglePagetree",id:t,state:0,REQUEST_TOKEN:Contao.request_token})),!1):(new Request.Contao({field:e,evalScripts:!0,onRequest:AjaxRequest.displayBox(Contao.lang.loading+" …"),onSuccess:function(n){var a=new Element("li",{id:t,class:"parent",styles:{display:"inline"}});new Element("ul",{class:"level_"+o,html:n}).inject(a,"bottom"),a.inject($(e).getParent("li"),"after"),a.getElements("a").each(function(e){e.href=e.href.replace(/&ref=[a-f0-9]+/,"&ref="+Contao.referer_id)}),$(e).store("tip:title",Contao.lang.collapse),l.src=AjaxRequest.themePath+"icons/folMinus.svg",AjaxRequest.hideBox(),window.fireEvent("ajax_change")}}).post({action:"loadPagetree",id:t,level:o,field:n,name:a,state:1,REQUEST_TOKEN:Contao.request_token}),!1)},toggleFiletree:function(e,t,n,a,o,i){e.blur(),Backend.getScrollOffset();var l=$(t),s=$(e).getFirst("img");return l?("none"==l.getStyle("display")?(l.setStyle("display",null),s.src=AjaxRequest.themePath+"icons/folMinus.svg",$(e).store("tip:title",Contao.lang.collapse),new Request.Contao({field:e}).post({action:"toggleFiletree",id:t,state:1,REQUEST_TOKEN:Contao.request_token})):(l.setStyle("display","none"),s.src=AjaxRequest.themePath+"icons/folPlus.svg",$(e).store("tip:title",Contao.lang.expand),new Request.Contao({field:e}).post({action:"toggleFiletree",id:t,state:0,REQUEST_TOKEN:Contao.request_token})),!1):(new Request.Contao({field:e,evalScripts:!0,onRequest:AjaxRequest.displayBox(Contao.lang.loading+" …"),onSuccess:function(n){var a=new Element("li",{id:t,class:"parent",styles:{display:"inline"}});new Element("ul",{class:"level_"+i,html:n}).inject(a,"bottom"),a.inject($(e).getParent("li"),"after"),a.getElements("a").each(function(e){e.href=e.href.replace(/&ref=[a-f0-9]+/,"&ref="+Contao.referer_id)}),$(e).store("tip:title",Contao.lang.collapse),s.src=AjaxRequest.themePath+"icons/folMinus.svg",AjaxRequest.hideBox(),window.fireEvent("ajax_change")}}).post({action:"loadFiletree",id:t,folder:n,level:i,field:a,name:o,state:1,REQUEST_TOKEN:Contao.request_token}),!1)},toggleSubpalette:function(e,t,n){e.blur();var a=$(t);return a?void(e.value?(e.value="",e.checked="",a.setStyle("display","none"),a.getElements("[required]").each(function(e){e.set("required",null).set("data-required","")}),new Request.Contao({field:e}).post({action:"toggleSubpalette",id:t,field:n,state:0,REQUEST_TOKEN:Contao.request_token})):(e.value=1,e.checked="checked",a.setStyle("display",null),a.getElements("[data-required]").each(function(e){e.set("required","").set("data-required",null)}),new Request.Contao({field:e}).post({action:"toggleSubpalette",id:t,field:n,state:1,REQUEST_TOKEN:Contao.request_token}))):void new Request.Contao({field:e,evalScripts:!1,onRequest:AjaxRequest.displayBox(Contao.lang.loading+" …"),onSuccess:function(n,a){var o=new Element("div",{id:t,class:"subpal",html:n,styles:{display:"block"}}).inject($(e).getParent("div").getParent("div"),"after");a.javascript&&(document.write=function(e){var t="";e.replace(/