Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
77c1c41
feat: add parameter type `multi-list`
papaj-na-wrotkach Feb 6, 2026
a6cd0b6
fix: fix parameter validation for arrays
papaj-na-wrotkach Feb 6, 2026
c4f1880
test: validate `BridgeCard::getListInput` method for `multi-list`
papaj-na-wrotkach Feb 6, 2026
d573a65
test: handle `multi-list` parameter type in `BridgeImplementationTest`
papaj-na-wrotkach Feb 6, 2026
b6cace4
test: add tests for `multi-list` parameter type in `ParameterValidato…
papaj-na-wrotkach Feb 6, 2026
564420d
test: fix errors in tests
papaj-na-wrotkach Feb 7, 2026
4ac27cd
fix: handle `defaultValue` properly for `multi-list`
papaj-na-wrotkach Feb 7, 2026
6d2427d
fix: clarify the description in the docblock
papaj-na-wrotkach Feb 7, 2026
044d896
style: add braces to comply with PSR-12
papaj-na-wrotkach Feb 8, 2026
f1b3392
fix(ui): make `multi-list` parameters displayed correctly
papaj-na-wrotkach Feb 8, 2026
ad1738b
fix: make backported array functions behave correctly
papaj-na-wrotkach Feb 8, 2026
d84f7f2
fix: validate the parameter type correctly
papaj-na-wrotkach Feb 8, 2026
9b7cddf
fixup! fix: validate the parameter type correctly
papaj-na-wrotkach Feb 8, 2026
79ceb94
fixup! fix(ui): make `multi-list` parameters displayed correctly
papaj-na-wrotkach Feb 8, 2026
1f871e2
fixup! style: add braces to comply with PSR-12
papaj-na-wrotkach Feb 8, 2026
14f6329
Merge branch 'master' into feat/mult-list-parameter
papaj-na-wrotkach Feb 9, 2026
4843b0b
style: fix line length
papaj-na-wrotkach Feb 9, 2026
735b5d5
feat: support for scalar `defaultValue` for `multi-list`
papaj-na-wrotkach Feb 9, 2026
4c41ad5
fixup! feat: support for scalar `defaultValue` for `multi-list`
papaj-na-wrotkach Feb 10, 2026
b2ebe01
fix: detect empty values inside `getListInput`
papaj-na-wrotkach Feb 10, 2026
8b7bc26
Merge branch 'RSS-Bridge:master' into feat/mult-list-parameter
papaj-na-wrotkach Feb 10, 2026
7a4b7bf
Merge branch 'master' into feat/mult-list-parameter
papaj-na-wrotkach Feb 10, 2026
ea6ffcd
style: fix line exceeding character limit
papaj-na-wrotkach Feb 10, 2026
177ae0c
fix: properly handle empty default value
papaj-na-wrotkach Feb 11, 2026
e784e32
fix: do not access `defaultValue` if there are no values
papaj-na-wrotkach Feb 11, 2026
f57ce19
test: add test for multiple default values
papaj-na-wrotkach Feb 11, 2026
3de2325
Merge branch 'master' into feat/mult-list-parameter
papaj-na-wrotkach Feb 19, 2026
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
55 changes: 31 additions & 24 deletions actions/FrontpageAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ private static function renderForm(
}

if (!isset($parameter['defaultValue'])) {
$parameter['defaultValue'] = '';
$parameter['defaultValue'] = ($parameter['type'] ?? null) === 'multi-list' ? [] : '';
}

$idArg = 'arg-' . urlencode($bridgeClassName) . '-' . urlencode($contextName) . '-' . urlencode($id);
Expand All @@ -200,6 +200,8 @@ private static function renderForm(
$form .= self::getNumberInput($parameter, $idArg, $id) . "\n";
} elseif ($parameter['type'] === 'list') {
$form .= self::getListInput($parameter, $idArg, $id) . "\n";
} elseif ($parameter['type'] === 'multi-list') {
$form .= self::getListInput($parameter, $idArg, $id, true) . "\n";
} elseif ($parameter['type'] === 'checkbox') {
$form .= self::getCheckboxInput($parameter, $idArg, $id) . "\n";
} else {
Expand Down Expand Up @@ -288,32 +290,37 @@ public static function getCheckboxInput(array $parameter, string $id, string $na
]);
}

public static function getListInput(array $parameter, string $id, string $name): string
public static function getListInput(array $parameter, string $id, string $name, bool $isMulti = false): string
{
$list = sprintf('<select id="%s" name="%s">', $id, $name) . "\n";

foreach ($parameter['values'] as $name => $value) {
if (is_array($value)) {
$list .= '<optgroup label="' . htmlentities($name) . '">';
foreach ($value as $subname => $subvalue) {
if (
$parameter['defaultValue'] === $subname
|| $parameter['defaultValue'] === $subvalue
) {
$list .= html_option($subname, $subvalue, true) . "\n";
} else {
$list .= html_option($subname, $subvalue) . "\n";
$list = sprintf('<select id="%s" name="%s"%s>', $id, $name . ($isMulti ? '[]' : ''), $isMulti ? ' multiple' : '') . "\n";

if (!empty($parameter['values'])) {
$default = $parameter['defaultValue'];
// Cast to array, so scalars become single element arrays - `"default value"` becomes `["default value"]`.
// Flip, so the values become keys and we can access the values later with O(1) complexity.
if ($isMulti) {
$default = array_flip((array)($default));
}

foreach ($parameter['values'] as $name => $value) {
if (is_array($value)) {
$list .= '<optgroup label="' . htmlentities($name) . '">';
foreach ($value as $subname => $subvalue) {
if ($isMulti) {
$selected = isset($default[$subname]) || isset($default[$subvalue]);
} else {
$selected = $default === $subname || $default === $subvalue;
}
$list .= html_option($subname, $subvalue, $selected) . "\n";
}
}
$list .= '</optgroup>';
} else {
if (
$parameter['defaultValue'] === $name
|| $parameter['defaultValue'] === $value
) {
$list .= html_option($name, $value, true) . "\n";
$list .= '</optgroup>';
} else {
$list .= html_option($name, $value) . "\n";
if ($isMulti) {
$selected = isset($default[$name]) || isset($default[$value]);
} else {
$selected = $default === $name || $default === $value;
}
$list .= html_option($name, $value, $selected) . "\n";
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions lib/BridgeAbstract.php
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,12 @@ private function setInputWithContext(array $input, $queriedContext)
$this->inputs[$context][$name]['value'] = $parameter['defaultValue'];
}
break;
case 'multi-list':
if (isset($properties['defaultValue'])) {
// Casting to array makes scalar values, like 'my value', become arrays, like ['my value'].
$this->inputs[$context][$name]['value'] = (array)($properties['defaultValue'] ?? []);
}
break;
default:
if (isset($parameter['defaultValue'])) {
$this->inputs[$context][$name]['value'] = $parameter['defaultValue'];
Expand Down
31 changes: 31 additions & 0 deletions lib/ParameterValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ public function validateInput(array &$input, array $parameters): array
case 'list':
$input[$name] = $this->validateListValue($value, $contextParameters[$name]['values']);
break;
case 'multi-list':
$input[$name] = $this->validateMultiListValue($value, $contextParameters[$name]['values']);
break;
default:
case 'text':
if (isset($contextParameters[$name]['pattern'])) {
Expand Down Expand Up @@ -122,6 +125,9 @@ public function getQueriedContext(array $input, array $parameters)

private function validateTextValue($value, $pattern = null)
{
if (!is_scalar($value)) {
return null;
}
if (is_null($pattern)) {
// No filtering taking place
$filteredValue = filter_var($value);
Expand All @@ -136,6 +142,9 @@ private function validateTextValue($value, $pattern = null)

private function validateNumberValue($value)
{
if (!is_scalar($value)) {
return null;
}
$filteredValue = filter_var($value, FILTER_VALIDATE_INT);
if ($filteredValue === false) {
return null;
Expand All @@ -145,11 +154,17 @@ private function validateNumberValue($value)

private function validateCheckboxValue($value)
{
if (!is_scalar($value)) {
return null;
}
return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
}

private function validateListValue($value, $expectedValues)
{
if (!is_scalar($value)) {
return null;
}
$filteredValue = filter_var($value);
if ($filteredValue === false) {
return null;
Expand All @@ -165,4 +180,20 @@ private function validateListValue($value, $expectedValues)
}
return $filteredValue;
}

private function validateMultiListValue($values, $expectedValues)
{
if (!is_array($values)) {
return null;
}
$filteredValues = [];
foreach ($values as $v) {
$filteredValue = $this->validateListValue($v, $expectedValues);
if ($filteredValue === null) {
return null;
}
$filteredValues[] = $filteredValue;
}
return $filteredValues;
}
}
33 changes: 33 additions & 0 deletions lib/php8backports.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,36 @@ function array_is_list(array $arr)
return array_keys($arr) === range(0, count($arr) - 1);
}
}

if (!function_exists('array_find_key')) {
function array_find_key(array $array, callable $callback)
{
foreach ($array as $key => $value) {
if ($callback($value, $key)) {
return $key;
}
}
return null;
}
}

if (!function_exists('array_find')) {
function array_find(array $array, callable $callback)
{
return (($key = array_find_key($array, $callback)) !== null) ? $array[$key] : null;
}
}

if (!function_exists('array_any')) {
function array_any(array $array, callable $callback): bool
{
return array_find_key($array, $callback) !== null;
}
}

if (!function_exists('array_all')) {
function array_all(array $array, callable $callback): bool
{
return !array_any($array, fn($v, $k) => !$callback($v, $k));
}
}
11 changes: 6 additions & 5 deletions middlewares/SecurityMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@
declare(strict_types=1);

/**
* Make sure that only strings are allowed in GET parameters
* Make sure that only strings and arrays of strings are allowed in GET parameters
*/
class SecurityMiddleware implements Middleware
{
public function __invoke(Request $request, $next): Response
{
foreach ($request->toArray() as $key => $value) {
if (!is_string($value)) {
return new Response(render(__DIR__ . '/../templates/error.html.php', [
'message' => "Query parameter \"$key\" is not a string.",
]), 400);
if (is_string($value) || is_array($value) && array_all($value, fn($v, $k) => is_string($v))) {
continue;
}
return new Response(render(__DIR__ . '/../templates/error.html.php', [
'message' => "Query parameter \"$key\" is not a string or array of strings.",
]), 400);
}
return $next($request);
}
Expand Down
6 changes: 4 additions & 2 deletions static/rss-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ var rssbridge_feed_finder = (function() {
*/

// Start the Feed search
async function rssbridge_feed_search(event) {
async function rssbridge_feed_search(_) {
const input = document.getElementById('searchfield');
let content = encodeURIComponent(input.value);
if (content) {
Expand Down Expand Up @@ -92,7 +92,9 @@ var rssbridge_feed_finder = (function() {

// Now display every Feed parameter
for (const param in element.bridgeData) {
content += `<li>${element.bridgeData[param].name} : ${element.bridgeData[param].value}</li>`;
const paramMeta = element.bridgeMeta.parameters[element.bridgeParams.context]?.[param] ?? element.bridgeMeta.parameters['global']?.[param];
const value = paramMeta?.type === 'multi-list' ? element.bridgeData[param].value.join(', ') : element.bridgeData[param].value;
content += `<li>${element.bridgeData[param].name} : ${value}</li>`;
}
content += `</div>
</div>
Expand Down
26 changes: 26 additions & 0 deletions tests/BridgeCardTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public function test()
'values' => [],
];
$this->assertSame('<select id="id" name="name">' . "\n" . '</select>' . "\n", FrontpageAction::getListInput($entry, 'id', 'name'));
$this->assertSame('<select id="id" name="name[]" multiple>' . "\n" . '</select>' . "\n", FrontpageAction::getListInput($entry, 'id', 'name', true));

$entry = [
'defaultValue' => 2,
Expand All @@ -26,6 +27,10 @@ public function test()
'<select id="id" name="name">' . "\n" . '<option value="bar">foo</option>' . "\n" . '</select>' . "\n",
FrontpageAction::getListInput($entry, 'id', 'name')
);
$this->assertSame(
'<select id="id" name="name[]" multiple>' . "\n" . '<option value="bar">foo</option>' . "\n" . '</select>' . "\n",
FrontpageAction::getListInput($entry, 'id', 'name', true)
);

// optgroup
$entry = [
Expand All @@ -38,6 +43,27 @@ public function test()
'<select id="id" name="name">' . "\n" . '<optgroup label="kek"><option value="b">f</option>' . "\n" . '</optgroup></select>' . "\n",
FrontpageAction::getListInput($entry, 'id', 'name')
);
$this->assertSame(
'<select id="id" name="name[]" multiple>' . "\n" . '<optgroup label="kek"><option value="b">f</option>' . "\n" . '</optgroup></select>' . "\n",
FrontpageAction::getListInput($entry, 'id', 'name', true)
);

// multiple default values
$entry = [
'defaultValue' => [ 'a', 'c', ],
'values' => ['kek' => [
'f' => 'b',
'a' => 'g',
]],
];
$expected = <<<EOT
<select id="id" name="name[]" multiple>
<optgroup label="kek"><option value="b">f</option>
<option value="g" selected>a</option>
</optgroup></select>

EOT;
$this->assertSame($expected, FrontpageAction::getListInput($entry, 'id', 'name', true));
}

public function test2()
Expand Down
4 changes: 3 additions & 1 deletion tests/BridgeImplementationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public function testParameters($path)
'text',
'number',
'list',
'multi-list',
'checkbox',
];

Expand Down Expand Up @@ -99,7 +100,7 @@ public function testParameters($path)
$this->assertIsString($options['type'], $field . ': invalid type');
$this->assertContains($options['type'], $allowedTypes, $field . ': unknown type');

if ($options['type'] == 'list') {
if ($options['type'] == 'list' || $options['type'] == 'multi-list') {
$this->assertArrayHasKey('values', $options, $field . ': missing list values');
$this->assertIsArray($options['values'], $field . ': invalid list values');
$this->assertNotEmpty($options['values'], $field . ': empty list values');
Expand All @@ -116,6 +117,7 @@ public function testParameters($path)
if ($options['required'] === true && isset($options['type'])) {
switch ($options['type']) {
case 'list':
case 'multi-list':
case 'checkbox':
$this->assertArrayNotHasKey(
'required',
Expand Down
32 changes: 32 additions & 0 deletions tests/ParameterValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,36 @@ public function test2()
];
$this->assertNotEmpty($sut->validateInput($input, $parameters));
}

public function test3()
{
$sut = new \ParameterValidator();
$input = [ 'categories' => [ 'economy', 'politics', 'entertainment', ], ];
$parameters = [
[
'categories' => [
'name' => 'Categories',
'type' => 'multi-list',
'values' => [ 'economy', 'politics', 'entertainment', ],
],
]
];
$this->assertSame([], $sut->validateInput($input, $parameters));
}

public function test4()
{
$sut = new \ParameterValidator();
$input = [ 'categories' => [ 'economy', 'politics', 'entertainment', ], ];
$parameters = [
[
'category' => [
'name' => 'Categories',
'type' => 'multi-list',
'values' => [ 'economy', 'politics', 'entertainment', ],
],
]
];
$this->assertNotEmpty($sut->validateInput($input, $parameters));
}
}