Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions src/CoreBundle/Controller/ListControllerTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
use MetaModels\Filter\FilterUrl;
use MetaModels\Filter\FilterUrlBuilder;
use MetaModels\Filter\Setting\IFilterSettingFactory;
use MetaModels\FrontendIntegration\FrontendFilterOptions;
use MetaModels\Filter\Setting\ParameterTypes;
use MetaModels\Helper\SortingLinkGenerator;
use MetaModels\IFactory;
use MetaModels\IItem;
Expand Down Expand Up @@ -343,20 +343,21 @@ private function getResponseInternal(Template $template, Model $model, Request $
private function getFilterParameters(FilterUrl $filterUrl, ItemList $itemRenderer): array
{
$filterSetting = $itemRenderer->getFilterSettings();
/** @var array<string, string> $wantedByType */
$wantedByType = [];
// FIXME: improve this call - it does too much.
foreach (
$filterSetting->getParameterFilterWidgets([], [], new FrontendFilterOptions()) as $widgetName => $widget
) {
$wantedByType[$widgetName] = (string) ($widget['param_type'] ?? 'slugNget');
}
// Obtain the types from the filter settings themselves - filter rules without frontend filter widget
// (i.e. the usual detail page rules) do not render a widget but still define a parameter type.
$wantedByType = ParameterTypes::fromSetting($filterSetting);

$result = [];
foreach ($filterSetting->getParameters() as $name) {
if (null !== $value = $this->tryReadFromSlugOrGet($filterUrl, $name, $wantedByType[$name] ?? 'slugNget')) {
$result[$name] = $value;
$paramType = $wantedByType[$name] ?? ParameterTypes::LEGACY_TYPE;
$value = $this->tryReadFromSlugOrGet($filterUrl, $name, $paramType);
if (null === $value) {
// Either not passed at all or passed via another URL type than the configured one - in both cases
// the parameter simply stays unused. It has been marked as used in tryReadFromSlugOrGet() so a
// slug of the wrong type does not end up in a 404 for unused route arguments.
continue;
}
$result[$name] = $value;
}

return $result;
Expand Down
15 changes: 15 additions & 0 deletions src/Filter/Setting/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,21 @@ public function getParameters()
return [] === $parameters ? [] : \array_merge(...$parameters);
}

/**
* Retrieve the URL parameter type for all registered parameters of all contained settings.
*
* @return array<string, string> The parameter types as array. parametername => type
*/
public function getParameterTypes()
{
$types = [];
foreach ($this->arrSettings as $objSetting) {
$types[] = ParameterTypes::fromSetting($objSetting);
}

return [] === $types ? [] : \array_merge(...$types);
}

/**
* {@inheritdoc}
*/
Expand Down
15 changes: 15 additions & 0 deletions src/Filter/Setting/CustomSql.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
use function array_intersect_key;
use function array_key_exists;
use function array_keys;
use function array_fill_keys;
use function array_map;
use function array_merge;
use function array_reduce;
Expand Down Expand Up @@ -234,6 +235,20 @@ public function getParameters()
return $arrParams;
}

/**
* Retrieve the URL parameter type for all registered parameters from the setting.
*
* @return array<string, string> The parameter types as array. parametername => type
*/
public function getParameterTypes()
{
// Legacy settings without a value keep the lenient behaviour of accepting both variants.
return array_fill_keys(
$this->getParameters(),
(string) ($this->get('param_type') ?: ParameterTypes::LEGACY_TYPE)
);
}

/**
* {@inheritdoc}
*/
Expand Down
15 changes: 15 additions & 0 deletions src/Filter/Setting/ExpressionRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,21 @@ public function getParameters(): array
return array_merge(...$parameters);
}

/**
* Retrieve the URL parameter type for all registered parameters from the setting.
*
* @return array<string, string> The parameter types as array. parametername => type
*/
public function getParameterTypes(): array
{
$types = [];
foreach ($this->children as $child) {
$types[] = ParameterTypes::fromSetting($child);
}

return array_merge([], ...$types);
}

#[Override]
public function getParameterDCA(): array
{
Expand Down
9 changes: 9 additions & 0 deletions src/Filter/Setting/ICollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@

/**
* This interface handles all filter setting abstraction.
*
* "getParameterTypes()" returns the URL parameter type for all registered parameters (parametername => type) of all
* contained filter settings, see ISimple for the possible types.
*
* Not implementing "getParameterTypes()" is deprecated, the method will get added to this interface in
* MetaModels 3.0. Until then, collections not providing it are treated as "slugNget" (the lenient legacy
* behaviour), see ParameterTypes::fromSetting().
*
* @method array<string, string> getParameterTypes() Retrieve the URL parameter type for all parameters.
*/
interface ICollection
{
Expand Down
11 changes: 11 additions & 0 deletions src/Filter/Setting/ISimple.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@

/**
* This interface handles the abstraction for a single filter setting.
*
* "getParameterTypes()" returns the URL parameter type for all registered parameters (parametername => type). The
* type determines from where the value of a parameter may get read and how the URL for it has to be built. Valid
* types are "slug" (key/value in the URL path), "get" (key=value in the query string) and the deprecated "slugNget"
* (both of them). See Simple::getParameterTypes() for the default implementation.
*
* Not implementing "getParameterTypes()" is deprecated, the method will get added to this interface in
* MetaModels 3.0. Until then, settings not providing it are treated as "slugNget" (the lenient legacy behaviour),
* see ParameterTypes::fromSetting().
*
* @method array<string, string> getParameterTypes() Retrieve the URL parameter type for all parameters.
*/
interface ISimple
{
Expand Down
75 changes: 75 additions & 0 deletions src/Filter/Setting/ParameterTypes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

/**
* This file is part of MetaModels/core.
*
* (c) 2012-2026 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 Ingolf Steinhardt <info@e-spin.de>
* @copyright 2012-2026 The MetaModels team.
* @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
* @filesource
*/

declare(strict_types=1);

namespace MetaModels\Filter\Setting;

use function array_fill_keys;
use function method_exists;

/**
* Helper to obtain the URL parameter types from a filter setting or a filter setting collection.
*
* This provides the backwards compatibility layer for implementations not (yet) providing
* "getParameterTypes()" - the method will get added to ISimple and ICollection in MetaModels 3.0. Adding it to the
* interfaces before would break every implementation out there, therefore it is only announced via "@method" there.
*
* @internal
*/
final class ParameterTypes
{
/**
* The lenient legacy type, accepting both slug and GET.
*/
public const LEGACY_TYPE = 'slugNget';

/**
* Obtain the URL parameter types of the passed filter setting or filter setting collection.
*
* @param ICollection|ISimple $setting The filter setting to obtain the types from.
*
* @return array<string, string> The parameter types as array. parametername => type
*/
public static function fromSetting(ICollection|ISimple $setting): array
{
if (!method_exists($setting, 'getParameterTypes')) {
// Settings without any parameter can not be affected - stay silent for them.
if ([] === ($parameters = $setting->getParameters())) {
return [];
}

// @codingStandardsIgnoreStart
@trigger_error(
'Filter setting "' . $setting::class . '" does not implement "getParameterTypes()". ' .
'The parameters are treated as "' . self::LEGACY_TYPE . '". ' .
'The method will be required in MetaModels 3.0.',
E_USER_DEPRECATED
);
// @codingStandardsIgnoreEnd

return array_fill_keys($parameters, self::LEGACY_TYPE);
}

/** @var array<string, string> $types */
$types = $setting->getParameterTypes();

return $types;
}
}
14 changes: 14 additions & 0 deletions src/Filter/Setting/Simple.php
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,20 @@ public function getParameters()
return [];
}

/**
* Retrieve the URL parameter type for all registered parameters from the setting.
*
* @return array<string, string> The parameter types as array. parametername => type
*/
public function getParameterTypes()
{
// Legacy settings without a value keep the lenient behaviour of accepting both variants.
return \array_fill_keys(
$this->getParameters(),
(string) ($this->get('param_type') ?: ParameterTypes::LEGACY_TYPE)
);
}

/**
* {@inheritdoc}
*/
Expand Down
13 changes: 13 additions & 0 deletions src/Filter/Setting/WithChildren.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,19 @@ public function getParameters()
return $arrParams;
}

/**
* {@inheritdoc}
*/
#[\Override]
public function getParameterTypes()
{
$arrTypes = [];
foreach ($this->arrChildren as $objSetting) {
$arrTypes = array_merge($arrTypes, ParameterTypes::fromSetting($objSetting));
}
return $arrTypes;
}

/**
* {@inheritdoc}
*/
Expand Down
14 changes: 3 additions & 11 deletions src/FrontendIntegration/FrontendFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
use ContaoCommunityAlliance\Contao\Bindings\ContaoEvents;
use ContaoCommunityAlliance\Contao\Bindings\Events\Controller\RedirectEvent;
use Contao\CoreBundle\Csrf\ContaoCsrfTokenManager;
use Contao\CoreBundle\Exception\PageNotFoundException;
use Contao\CoreBundle\Exception\RedirectResponseException;
use Contao\FrontendTemplate;
use Contao\Input;
Expand Down Expand Up @@ -435,10 +434,10 @@ protected function getFilters()
);

// DAMN Contao - we have to "mark" the keys in the Input class as used as we get an 404 otherwise.
// This is also done for parameters passed via another URL type than the configured one. Their value is
// not used for filtering (see buildParameters()), but they must not end up in a 404 either.
foreach ($wantedNames as $name) {
if ($all->hasSlug($name)) {
Input::get($name);
}
Input::get($name);
}

$values = \array_merge($all->getSlugParameters(), $all->getGetParameters());
Expand All @@ -450,13 +449,6 @@ protected function getFilters()
$filterOptions
);

// 404 if a get-only filter parameter is accessed via slug.
foreach ($arrWidgets as $widgetName => $widget) {
if ('get' === ($widget['param_type'] ?? 'slug') && $all->hasSlug($widgetName)) {
throw new PageNotFoundException();
}
}

// If we have POST data, we need to redirect now.
if (Input::post('FORM_SUBMIT') === $this->formId) {
foreach ($wantedNames as $widgetName) {
Expand Down
16 changes: 12 additions & 4 deletions src/Render/Setting/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use MetaModels\Filter\FilterUrl;
use MetaModels\Filter\FilterUrlBuilder;
use MetaModels\Filter\Setting\IFilterSettingFactory;
use MetaModels\Filter\Setting\ParameterTypes;
use MetaModels\IItem;
use MetaModels\IMetaModel;
use MetaModels\ITranslatedMetaModel;
Expand Down Expand Up @@ -339,18 +340,25 @@ public function buildJumpToUrlFor(IItem $item /**, ?int $referenceType */)

if (!empty($information['filterSetting'])) {
/** @var \MetaModels\Filter\Setting\ICollection $filterSetting */
$filterSetting = $information['filterSetting'];
$parameterList = $filterSetting->generateFilterUrlFrom($item, $this);
$filterSetting = $information['filterSetting'];
$parameterList = $filterSetting->generateFilterUrlFrom($item, $this);
$parameterTypes = ParameterTypes::fromSetting($filterSetting);

foreach ($parameterList as $strKey => $strValue) {
// Sadly the filter values are currently encoded due to legacy reasons.
// For MetaModels 3, they should be passed around decoded everywhere.
$filterUrl->setSlug($strKey, \rawurldecode($strValue))->setGet($strKey, '');
$strValue = \rawurldecode($strValue);
// Build the URL as configured in the filter setting - "slugNget" and anything else use the slug.
if ('get' === ($parameterTypes[$strKey] ?? 'slug')) {
$filterUrl->setGet($strKey, $strValue)->setSlug($strKey, '');
continue;
}
$filterUrl->setSlug($strKey, $strValue)->setGet($strKey, '');
}
}

$result['params'] = $parameterList;
$result['deep'] = !empty($filterUrl->getSlugParameters());
$result['deep'] = !empty($filterUrl->getSlugParameters()) || !empty($filterUrl->getGetParameters());

$result['url'] = $this->filterUrlBuilder->generate(
$filterUrl,
Expand Down
Loading