Skip to content

Commit b8a38fc

Browse files
Handle rule filter serialization
Introduce `RuleSerializer` to create Json from a configured filter. Introduce `Icinga\Module\Notifications\Hook\V2` which no longer requires `serializeRuleFilter()` and `getRuleFilterTargets()` as they are now obsolete. Adjust `EventRuleController` to skip target selection use the new `RuleSerializer`.
1 parent 16ae89d commit b8a38fc

5 files changed

Lines changed: 183 additions & 45 deletions

File tree

application/controllers/EventRuleController.php

Lines changed: 32 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,29 @@
77

88
use Icinga\Application\Hook;
99
use Icinga\Application\Logger;
10+
use Icinga\Exception\ConfigurationError;
1011
use Icinga\Exception\Http\HttpNotFoundException;
1112
use Icinga\Module\Notifications\Common\Auth;
1213
use Icinga\Module\Notifications\Common\Database;
1314
use Icinga\Module\Notifications\Common\Links;
1415
use Icinga\Module\Notifications\Forms\EventRuleConfigElements\NotificationConfigProvider;
1516
use Icinga\Module\Notifications\Forms\EventRuleConfigForm;
1617
use Icinga\Module\Notifications\Forms\EventRuleForm;
17-
use Icinga\Module\Notifications\Hook\V1\SourceHook;
18+
use Icinga\Module\Notifications\Hook\V2\SourceHook;
1819
use Icinga\Module\Notifications\Model\Rule;
1920
use Icinga\Module\Notifications\Model\Source;
21+
use Icinga\Module\Notifications\Util\RuleSerializer;
2022
use Icinga\Module\Notifications\Web\Control\SearchBar\ExtraTagSuggestions;
2123
use Icinga\Web\Notification;
2224
use Icinga\Web\Session;
2325
use ipl\Html\Contract\Form;
2426
use ipl\Html\Html;
2527
use ipl\Stdlib\Filter;
2628
use ipl\Web\Compat\CompatController;
27-
use ipl\Web\Compat\CompatForm;
2829
use ipl\Web\Url;
2930
use ipl\Web\Widget\Icon;
3031
use ipl\Web\Widget\Link;
32+
use JsonException;
3133
use Psr\Http\Message\ServerRequestInterface;
3234
use Throwable;
3335

@@ -217,7 +219,7 @@ public function searchEditorAction(): void
217219
}
218220

219221
$hook = null;
220-
foreach (Hook::all('Notifications/v1/Source') as $h) {
222+
foreach (Hook::all('Notifications/v2/Source') as $h) {
221223
/** @var SourceHook $h */
222224
try {
223225
if ($h->getSourceType() === $source->type) {
@@ -237,50 +239,37 @@ public function searchEditorAction(): void
237239
), $source->type));
238240
}
239241

240-
if (! $filter) {
241-
$targets = $hook->getRuleFilterTargets($source->id);
242-
if (count($targets) === 1 && ! is_array(reset($targets))) {
243-
$filter = key($targets);
244-
} else {
245-
$target = null;
246-
$form = (new CompatForm())
247-
->applyDefaultElementDecorators()
248-
->setAction(Url::fromRequest()->getAbsoluteUrl())
249-
->addElement('select', 'target', [
250-
'required' => true,
251-
'label' => $this->translate('Filter Target'),
252-
'options' => ['' => ' - ' . $this->translate('Please choose') . ' - '] + $targets,
253-
'disabledOptions' => ['']
254-
])
255-
->addElement('submit', 'btn_submit', [
256-
// translators: shown on a submit button to proceed to the next step of a form wizard
257-
'label' => $this->translate('Next')
258-
])
259-
->on(Form::ON_SUBMIT, function (CompatForm $form) use (&$target) {
260-
$target = $form->getValue('target');
261-
})
262-
->handleRequest($this->getServerRequest());
263-
264-
if ($target !== null) {
265-
$filter = $target;
266-
} else {
267-
$this->addContent($form);
268-
}
242+
$queryStr = '';
243+
if ($filter) {
244+
try {
245+
$data = json_decode($filter, true, flags: JSON_THROW_ON_ERROR);
246+
} catch (JsonException $e) {
247+
Logger::error('Failed to parse rule filter configuration: %s (Error: %s)', $filter, $e);
248+
throw new ConfigurationError($this->translate(
249+
'Failed to parse rule filter configuration. Please contact your system administrator.'
250+
));
269251
}
270-
}
271252

272-
if ($filter) {
273-
$form = $hook->getRuleFilterEditor($filter)
274-
->setAction(Url::fromRequest()->with('object_filter', $filter)->getAbsoluteUrl())
275-
->on(Form::ON_SUBMIT, function (Form $form) use ($ruleId, $hook) {
276-
$this->session->set('object_filter', $hook->serializeRuleFilter($form));
277-
$this->redirectNow(Links::eventRule($ruleId)->setParam('_filterOnly'));
278-
})
279-
->handleRequest($this->getServerRequest());
280-
281-
$this->getDocument()->addHtml($form);
253+
if (! isset($data['filter']) || $data['version'] !== 2) {
254+
Logger::error('Invalid rule filter configuration: %s', $filter);
255+
throw new ConfigurationError($this->translate(
256+
'Invalid rule filter configuration. Please contact your system administrator.'
257+
));
258+
}
259+
260+
$queryStr = $data['filter'];
282261
}
283262

263+
$form = $hook->getRuleFilterEditor($queryStr)
264+
->setAction(Url::fromRequest()->with('object_filter', $filter)->getAbsoluteUrl())
265+
->on(Form::ON_SUBMIT, function (Form $form) use ($ruleId, $hook) {
266+
$this->session->set('object_filter', (new RuleSerializer($form->getFilter()))->getJson());
267+
$this->redirectNow(Links::eventRule($ruleId)->setParam('_filterOnly'));
268+
})
269+
->handleRequest($this->getServerRequest());
270+
271+
$this->getDocument()->addHtml($form);
272+
284273
$this->setTitle($this->translate('Adjust Filter'));
285274
}
286275

application/forms/SourceForm.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
use Icinga\Application\Hook;
1010
use Icinga\Application\Logger;
1111
use Icinga\Exception\Http\HttpNotFoundException;
12-
use Icinga\Module\Notifications\Hook\V1\SourceHook;
12+
use Icinga\Module\Notifications\Hook\V2\SourceHook;
1313
use Icinga\Module\Notifications\Model\Source;
1414
use ipl\Html\Attributes;
1515
use ipl\Html\HtmlDocument;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2026 Icinga GmbH <https://icinga.com>
4+
// SPDX-License-Identifier: GPL-3.0-or-later
5+
6+
namespace Icinga\Module\Notifications\Hook\V2;
7+
8+
use ipl\Html\Contract\Form;
9+
use ipl\Web\Widget\Icon;
10+
11+
interface SourceHook
12+
{
13+
/**
14+
* Get the type of source this integration is responsible for
15+
*
16+
* @return string
17+
*/
18+
public function getSourceType(): string;
19+
20+
/**
21+
* Get the label of the source this integration is responsible for
22+
*
23+
* @return string
24+
*/
25+
public function getSourceLabel(): string;
26+
27+
/**
28+
* Get the icon of the source this integration is responsible for
29+
*
30+
* @return Icon
31+
*/
32+
public function getSourceIcon(): Icon;
33+
34+
/**
35+
* Get an editor for the given filter
36+
*
37+
* The returned form MUST NOT have a request associated with it and MUST NOT navigate away upon submission.
38+
* The action of the form is overridden in any case.
39+
*
40+
* @param string $filter The query filter
41+
*
42+
* @return Form
43+
*/
44+
public function getRuleFilterEditor(string $filter): Form;
45+
}

library/Notifications/Model/Source.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use DateTime;
99
use Icinga\Application\Hook;
1010
use Icinga\Application\Logger;
11-
use Icinga\Module\Notifications\Hook\V1\SourceHook;
11+
use Icinga\Module\Notifications\Hook\V2\SourceHook;
1212
use ipl\Orm\Behavior\BoolCast;
1313
use ipl\Orm\Behavior\MillisecondTimestamp;
1414
use ipl\Orm\Behaviors;
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2026 Icinga GmbH <https://icinga.com>
4+
// SPDX-License-Identifier: GPL-3.0-or-later
5+
6+
namespace Icinga\Module\Notifications\Util;
7+
8+
use Icinga\Util\Json;
9+
use ipl\Stdlib\Filter;
10+
use ipl\Stdlib\Filter\Chain;
11+
use ipl\Web\Filter\Renderer;
12+
13+
class RuleSerializer
14+
{
15+
protected Filter\Rule $filter;
16+
17+
/**
18+
* Create an object that can be used to serialize a rule to JSON
19+
*
20+
* @param Filter\Rule $filter
21+
*/
22+
public function __construct(Filter\Rule $filter)
23+
{
24+
$this->filter = $filter;
25+
}
26+
27+
/**
28+
* Serialize the filter as Json
29+
*
30+
* @return string
31+
*
32+
* @throws \Icinga\Exception\Json\JsonEncodeException
33+
*/
34+
public function getJson(): string
35+
{
36+
$queryString = (new Renderer($this->filter))->render();
37+
$result = [];
38+
$result['version'] = 2;
39+
$result['filter'] = $queryString;
40+
if ($this->filter instanceof Filter\Chain) {
41+
$result['json'] = $this->serializeChain($this->filter);
42+
} else {
43+
/** @var Filter\Condition $this->filter */
44+
$result['json'] = $this->serializeCondition($this->filter);
45+
}
46+
47+
return Json::encode($result);
48+
}
49+
50+
/**
51+
* Create an array with keys `op` and `rules` from a chain
52+
*
53+
* @param Chain $chain
54+
*
55+
* @return array{op: string, rules: array}
56+
*/
57+
protected function serializeChain(Chain $chain): array
58+
{
59+
$result = [
60+
'op' => match (true) {
61+
$chain instanceof Filter\All => '&',
62+
$chain instanceof Filter\None => '!',
63+
$chain instanceof Filter\Any => '|'
64+
}
65+
];
66+
67+
$rules = [];
68+
foreach ($chain as $rule) {
69+
if ($rule instanceof Chain) {
70+
$rules[] = $this->serializeChain($rule);
71+
} else {
72+
$rules[] = $this->serializeCondition($rule);
73+
}
74+
}
75+
76+
$result['rules'] = $rules;
77+
return $result;
78+
}
79+
80+
/**
81+
* Create an array with the keys `op`, `column` and `value` from a condition
82+
*
83+
* @param Filter\Condition $condition
84+
*
85+
* @return array{op: string, column: string, value: mixed}
86+
*/
87+
protected function serializeCondition(Filter\Condition $condition): array
88+
{
89+
return [
90+
'op' => match (true) {
91+
$condition instanceof Filter\Unlike => '!~',
92+
$condition instanceof Filter\Unequal => '!=',
93+
$condition instanceof Filter\Like => '~',
94+
$condition instanceof Filter\Equal => '=',
95+
$condition instanceof Filter\GreaterThan => '>',
96+
$condition instanceof Filter\LessThan => '<',
97+
$condition instanceof Filter\GreaterThanOrEqual => '>=',
98+
$condition instanceof Filter\LessThanOrEqual => '<=',
99+
},
100+
'column' => $condition->getColumn(),
101+
'value' => $condition->getValue(),
102+
];
103+
}
104+
}

0 commit comments

Comments
 (0)