Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
122 commits
Select commit Hold shift + click to select a range
001cf17
Add: column external_uuid & missing column changed_at
Jan-Schuppik Sep 3, 2025
92778e1
Run with:
nilmerg Aug 11, 2025
8131fca
Introduce: PSR Logger
Jan-Schuppik Aug 22, 2025
f02e3f2
Fix: test-setup for MySQL compatibility (PostgreSQL tests still failing)
Jan-Schuppik Aug 14, 2025
a1b8b2b
Adjust: unit tests
Jan-Schuppik Aug 22, 2025
481f9df
Add: route for new api controller
Jan-Schuppik Aug 22, 2025
fc77a2c
Introduce: new API srtucture (starting with Contact GET)
Jan-Schuppik Aug 22, 2025
a7eb80b
Add: initial openapi docs
Jan-Schuppik Aug 22, 2025
df74d4e
Adjust: give the request object to ApiCore
Jan-Schuppik Aug 22, 2025
c219ea7
Fix: lint-errors & moduleName fetching
Jan-Schuppik Aug 25, 2025
07d3919
Remove: Icinga Web 2 BaseTestCase dependency
Jan-Schuppik Aug 25, 2025
2b7ee4c
Adjust: add small review suggestions
Jan-Schuppik Aug 28, 2025
710cf89
Rework: implements psr-15 RequestHandlerInterface
Jan-Schuppik Aug 29, 2025
5891dac
Adjust: api-version handling
Jan-Schuppik Sep 1, 2025
c772342
Fix: request validation & plural-GET result name & test permissions
Jan-Schuppik Sep 1, 2025
544c673
Adjust: openapi documentation creating & storing
Jan-Schuppik Sep 1, 2025
b846798
Fix: Psr-Logger exception-handling
Jan-Schuppik Sep 3, 2025
5664f94
Introduce: PUT method handling
Jan-Schuppik Sep 3, 2025
5bf799c
Adjust: Openapi Attributes (+ fix strings)
Jan-Schuppik Sep 3, 2025
cdfdfb3
Introduce: POST method handling
Jan-Schuppik Sep 3, 2025
bcb5e57
Fix: phpdocs of endpoint-methods
Jan-Schuppik Sep 3, 2025
cf63e87
Introduce: DELETE method handling
Jan-Schuppik Sep 3, 2025
dff73f0
WIP: introduce psr15 pipeline & validation middleware
Jan-Schuppik Sep 4, 2025
caa5ffd
Fix: contact tests (enable POST with identifier)
Jan-Schuppik Sep 4, 2025
40333b6
Fix: CodeSniffer Errors
Jan-Schuppik Sep 5, 2025
c97f42d
Add: contactgroup endpoint methods
Jan-Schuppik Sep 5, 2025
0e5c409
Fix: contactgroup tests
Jan-Schuppik Sep 5, 2025
eb198e2
Adjust: enriching of get-result-rows
Jan-Schuppik Sep 5, 2025
ca67141
Add: channel GET-methods
Jan-Schuppik Sep 5, 2025
a67cd65
Fix: wrong Uuid-class & filterString fetching
Jan-Schuppik Sep 5, 2025
ad08004
Add: channel tests (+ adjust channel filters)
Jan-Schuppik Sep 5, 2025
c89da33
Fix: add missing escape-signs in tests
Jan-Schuppik Sep 5, 2025
c466160
Fix: CodeSniffer Errors
Jan-Schuppik Sep 5, 2025
f8ebcde
Remove: error abstraction
Jan-Schuppik Sep 8, 2025
b2d7c7a
Adjust: channel-tests add second default channel
Jan-Schuppik Sep 8, 2025
03453a4
Remove: middleware & middleware-pipeline
Jan-Schuppik Sep 8, 2025
86f71d7
Fix: channel-tests
Jan-Schuppik Sep 9, 2025
1095d7c
Fix: contact-tests
Jan-Schuppik Sep 10, 2025
14f9eed
Fix: contacgroup-tests & contact-tests
Jan-Schuppik Sep 10, 2025
bde3390
Adjust: parse all exceptions in error response (temporary workaround)
Jan-Schuppik Sep 10, 2025
c67c059
Fix: CodeSniffer Errors
Jan-Schuppik Sep 10, 2025
f9a03c3
Adjust: create response only if necessary
Jan-Schuppik Sep 10, 2025
1d019f5
Fix: general request-body-validation
Jan-Schuppik Sep 10, 2025
2881291
Adjust: naming of multi-result-response
Jan-Schuppik Sep 11, 2025
b358f90
Fix: request-body-validation & error-throwing
Jan-Schuppik Sep 11, 2025
e051fef
Adjust: simplify response-creating
Jan-Schuppik Sep 11, 2025
f632d1e
Adjust: move openapi-logic from core to openapi-endpoint
Jan-Schuppik Sep 11, 2025
0a8c530
Fix: unit-tests & adjust behavior
Jan-Schuppik Sep 15, 2025
e3f631b
Adjust: simplify dispatching of ApiController
Jan-Schuppik Sep 18, 2025
23d4d7d
Adjust: change scope of allowedMethod & generator funcs
Jan-Schuppik Sep 18, 2025
5cb4fc1
Fix: small mistakes in endpoints openapi & contactgroups
Jan-Schuppik Sep 18, 2025
e97d98d
Add: methode to control the creation of a Response
Jan-Schuppik Sep 18, 2025
4dc3803
Adjust: apply review suggestions to tests
Jan-Schuppik Sep 18, 2025
97423fb
Adjust: 'expected' variables in test
Jan-Schuppik Sep 18, 2025
f3e550a
Adjust: db usage & move general handle-logic to core
Jan-Schuppik Sep 19, 2025
49efec2
Remove: http error code 409
Jan-Schuppik Sep 19, 2025
3009ae1
Adjust: change endpoint const in abstract method
Jan-Schuppik Sep 19, 2025
c2b7fda
Fix: change unclear test names
Jan-Schuppik Sep 19, 2025
32a87ee
Adjust: Http enum creation
Jan-Schuppik Sep 19, 2025
9d24f14
Remove: plural-check from ApiCore and handle it in endpoints
Jan-Schuppik Sep 19, 2025
4018274
Fix: base assertValidRequest shouldn't provide logic
Jan-Schuppik Sep 19, 2025
9b7a3c9
Adjust: beautify filter assembling for plural get
Jan-Schuppik Sep 19, 2025
ad2861d
Adjust: json string creation
Jan-Schuppik Sep 19, 2025
0d3bcc1
Remove: unnecessary stream-request-body method
Jan-Schuppik Sep 19, 2025
ecf502a
Fix: move request asserting to from core to v1
Jan-Schuppik Sep 19, 2025
b91a66f
Adjust: replace PHP-docs with more descriptive test-names
Jan-Schuppik Sep 19, 2025
584ac80
Fix: change remaining test-UUID to constants
Jan-Schuppik Sep 22, 2025
2525735
Adjust: review suggestions
Jan-Schuppik Sep 25, 2025
aa43277
Fix: wrong format of endpoint
Jan-Schuppik Sep 25, 2025
c8191d0
Fix: mysql external_uuid db type
Jan-Schuppik Sep 25, 2025
d515000
Fix: cases of class-call in dispatching
Jan-Schuppik Sep 25, 2025
22c102e
Adjust: response header content-type
Jan-Schuppik Sep 26, 2025
6a5b374
Fix: Code-Sniffer Errors
Jan-Schuppik Sep 26, 2025
96e1ad5
Adjust: cleanup row preparation for output
Jan-Schuppik Sep 26, 2025
b9c5747
Adjust: improve the php-docs
Jan-Schuppik Sep 26, 2025
ab22f12
Adjust: cleanup code & set response-header if not provided
Jan-Schuppik Sep 26, 2025
5393d81
Adjust: return response instead of assoc array in endpoint-methods
Jan-Schuppik Sep 29, 2025
1b10084
Fix: change unsuitable names
Jan-Schuppik Sep 29, 2025
8d4c69e
Fix: correct indentations
Jan-Schuppik Sep 29, 2025
1d71c5c
Adjust: use sprintf() to cleanup strings
Jan-Schuppik Sep 29, 2025
c1d2197
Remove: TODOs
Jan-Schuppik Sep 29, 2025
b22fbe8
Adjust: identifier-check in post-methods
Jan-Schuppik Sep 29, 2025
3d9aa4f
Fix: scope of variables initialized in if-condition
Jan-Schuppik Sep 29, 2025
a9cf9ba
Fix: add spacing to improve readability
Jan-Schuppik Sep 29, 2025
e05c915
Fix: IDE warnings
Jan-Schuppik Sep 29, 2025
82a28be
Adjust: use database via singleton instead of initialize variables
Jan-Schuppik Sep 29, 2025
d75633e
Fix: exceeding line
Jan-Schuppik Sep 29, 2025
25909c2
Fix: unit-tests for new database-trait
Jan-Schuppik Sep 29, 2025
d8af316
Fix: scope of a variable
Jan-Schuppik Sep 30, 2025
154fe88
Adjust: make unit-tests independent
Jan-Schuppik Sep 30, 2025
8813ec1
Fix: use getConnection() (need ipl-sql shared-test-databases updated)
Jan-Schuppik Sep 30, 2025
fba9c10
Fix: unit-test errors caused by databases
Jan-Schuppik Oct 1, 2025
53784c3
Revert "Fix: unit-test errors caused by databases"
Jan-Schuppik Oct 2, 2025
4e94dee
Introduce and use new `ApiTestBackends` data provider
nilmerg Oct 2, 2025
e965792
Fix: unit-tests according to new data provider
Jan-Schuppik Oct 6, 2025
fcd79f2
Fix: Code-Sniffer Errors
Jan-Schuppik Oct 6, 2025
5959e1f
Fix: softdelete-blocks-uuid problem
Jan-Schuppik Oct 7, 2025
7bba0f1
php: Spin up database backends for unit tests
nilmerg Oct 8, 2025
048e39b
tests: Set up Icinga Web Database
nilmerg Oct 8, 2025
32b78fe
ApiTestBackends: Pass through `ICINGAWEB_LIBDIR` to webserver
nilmerg Oct 8, 2025
7cdbe98
Adjust: implement HandlerInterface in endpoints instead of in base class
Jan-Schuppik Oct 10, 2025
8850461
Fix: Error-handling, log no-http-errors
Jan-Schuppik Oct 10, 2025
c864495
Fix: psr logger
Jan-Schuppik Oct 10, 2025
9e5bd80
Fix: stream the response
Jan-Schuppik Oct 10, 2025
481cb98
Fix: ServerRequest creation, avoid Content-type checking
Jan-Schuppik Oct 14, 2025
4a9f31e
Fix: web-forms add column external_uuid
Jan-Schuppik Oct 14, 2025
40cc67a
Adjust: wrapping-condition of result data in tests
Jan-Schuppik Oct 14, 2025
a751d49
Fix: HttpMethod enum
Jan-Schuppik Oct 14, 2025
bb966be
Adjust: check if http method exists for endpoint
Jan-Schuppik Oct 14, 2025
8beb160
Add: license headers
Jan-Schuppik Oct 15, 2025
5494522
Adjust: replace PHP-docs with more descriptive test-names
Jan-Schuppik Sep 19, 2025
697c98c
Introduce: own handling of swagger-php attributes (start with GET)
Jan-Schuppik Sep 24, 2025
70d73eb
Add: DELETE swagger-php attributes
Jan-Schuppik Sep 24, 2025
6289d7e
Add: POST swagger-php attributes
Jan-Schuppik Oct 1, 2025
ff4188f
Add: PUT swagger-php attributes
Jan-Schuppik Oct 1, 2025
c0fe743
WIP: Adjust openapi description according to schemathesis checks
Jan-Schuppik Oct 9, 2025
e06838d
Adjust: cleanup OpenApi Description
Jan-Schuppik Oct 10, 2025
26307e8
Adjust: give the POST request body a new uuid
Jan-Schuppik Oct 15, 2025
3526533
Fix: standardize the designation of Contact Groups
Jan-Schuppik Oct 15, 2025
c774c90
Fix: output format of a single get result
Jan-Schuppik Oct 15, 2025
00d3880
Add: missing license headers (to OAD files)
Jan-Schuppik Oct 15, 2025
06a2750
Fix: Codesniffer errors
Jan-Schuppik Oct 15, 2025
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: 55 additions & 0 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,36 @@ jobs:
php: ['8.2', '8.3', '8.4']
os: ['ubuntu-latest']

services:
mysql:
image: mariadb
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: icinga_unittest
MYSQL_USER: icinga_unittest
MYSQL_PASSWORD: icinga_unittest
options: >-
--health-cmd "mariadb -s -uroot -proot -e'SHOW DATABASES;' 2> /dev/null | grep icinga_unittest > test"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 3306/tcp

pgsql:
image: postgres
env:
POSTGRES_USER: icinga_unittest
POSTGRES_PASSWORD: icinga_unittest
POSTGRES_DB: icinga_unittest
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432/tcp

steps:
- name: Checkout code base
uses: actions/checkout@v4
Expand All @@ -75,7 +105,32 @@ jobs:
git clone --depth 1 -b snapshot/nightly https://github.com/Icinga/icinga-php-library.git _libraries/ipl
git clone --depth 1 -b snapshot/nightly https://github.com/Icinga/icinga-php-thirdparty.git _libraries/vendor

- name: Initialize Icinga Web
run: |
mysql --host="127.0.0.1" --port="${{ job.services.mysql.ports['3306'] }}" --user="root" --password="root" \
-e "CREATE DATABASE icingaweb; CREATE USER icingaweb@'%' IDENTIFIED BY 'icingaweb'; GRANT ALL ON icingaweb.* TO icingaweb@'%';"
PGPASSWORD=icinga_unittest psql --host="127.0.0.1" --port="${{ job.services.pgsql.ports['5432'] }}" \
--username "icinga_unittest" -c "CREATE DATABASE icingaweb;"

- name: PHPUnit
env:
ICINGAWEB_LIBDIR: _libraries
ICINGAWEB_PATH: _icingaweb2
ICINGA_NOTIFICATIONS_SCHEMA: test/schema
MYSQL_TESTDB: icinga_unittest
MYSQL_TESTDB_HOST: 127.0.0.1
MYSQL_TESTDB_PORT: ${{ job.services.mysql.ports['3306'] }}
MYSQL_TESTDB_USER: icinga_unittest
MYSQL_TESTDB_PASSWORD: icinga_unittest
MYSQL_ICINGAWEBDB: icingaweb
MYSQL_ICINGAWEBDB_PASSWORD: icingaweb
MYSQL_ICINGAWEBDB_USER: icingaweb
PGSQL_TESTDB: icinga_unittest
PGSQL_TESTDB_HOST: 127.0.0.1
PGSQL_TESTDB_PORT: ${{ job.services.pgsql.ports['5432'] }}
PGSQL_TESTDB_USER: icinga_unittest
PGSQL_TESTDB_PASSWORD: icinga_unittest
PGSQL_ICINGAWEBDB: icingaweb
PGSQL_ICINGAWEBDB_PASSWORD: icinga_unittest
PGSQL_ICINGAWEBDB_USER: icinga_unittest
run: phpunit --bootstrap _icingaweb2/test/php/bootstrap.php
149 changes: 149 additions & 0 deletions application/controllers/ApiController.php
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing license header. It seems, not only this file is missing it, please make sure all PHP files have one.

Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

namespace Icinga\Module\Notifications\Controllers;

use Exception;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\ServerRequest;
use Icinga\Application\Logger;
use Icinga\Exception\Http\HttpBadRequestException;
use Icinga\Exception\Http\HttpExceptionInterface;
use Icinga\Exception\IcingaException;
use Icinga\Exception\Json\JsonEncodeException;
use Icinga\Module\Notifications\Api\V1\OpenApi;
use Icinga\Util\Json;
use Icinga\Web\Request;
use ipl\Stdlib\Str;
use ipl\Web\Compat\CompatController;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
use Zend_Controller_Request_Exception;

class ApiController extends CompatController
{
/**
* Handle API requests and route them to the appropriate endpoint class.
*
* Processes API requests for the Notifications module, serving as the main entry point for all API interactions.
*
* @return never
* @throws JsonEncodeException
*/
public function indexAction(): never
{
try {
$this->assertPermission('notifications/api');

$request = $this->getRequest();
if (
! $request->isApiRequest()
&& strtolower($request->getParam('endpoint')) !== (new OpenApi())->getEndpoint() // for browser query

Check failure on line 41 in application/controllers/ApiController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.2 on ubuntu-latest

Parameter #1 $string of function strtolower expects string, mixed given.

Check failure on line 41 in application/controllers/ApiController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.3 on ubuntu-latest

Parameter #1 $string of function strtolower expects string, mixed given.

Check failure on line 41 in application/controllers/ApiController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.4 on ubuntu-latest

Parameter #1 $string of function strtolower expects string, mixed given.
) {
$this->httpBadRequest('No API request');
}

$params = $request->getParams();
$version = ucfirst($params['version']);

Check failure on line 47 in application/controllers/ApiController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.2 on ubuntu-latest

Parameter #1 $string of function ucfirst expects string, mixed given.

Check failure on line 47 in application/controllers/ApiController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.3 on ubuntu-latest

Parameter #1 $string of function ucfirst expects string, mixed given.

Check failure on line 47 in application/controllers/ApiController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.4 on ubuntu-latest

Parameter #1 $string of function ucfirst expects string, mixed given.
$endpoint = ucfirst(Str::camel($params['endpoint']));

Check failure on line 48 in application/controllers/ApiController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.2 on ubuntu-latest

Parameter #1 $subject of static method ipl\Stdlib\Str::camel() expects string|null, mixed given.

Check failure on line 48 in application/controllers/ApiController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.3 on ubuntu-latest

Parameter #1 $subject of static method ipl\Stdlib\Str::camel() expects string|null, mixed given.

Check failure on line 48 in application/controllers/ApiController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.4 on ubuntu-latest

Parameter #1 $subject of static method ipl\Stdlib\Str::camel() expects string|null, mixed given.
$identifier = $params['identifier'] ?? null;

$module = (($moduleName = $request->getModuleName()) !== null)
? 'Module\\' . ucfirst($moduleName) . '\\'
: '';
$className = sprintf('Icinga\\%sApi\\%s\\%s', $module, $version, $endpoint);

// TODO: works only for V1 right now
if (! class_exists($className) || ! is_subclass_of($className, RequestHandlerInterface::class)) {
$this->httpNotFound("Endpoint $endpoint does not exist.");
}

$httpMethod = $request->getMethod();
$serverRequest = (new ServerRequest(
$httpMethod,
$request->getRequestUri(),
serverParams: $request->getServer(),
))
->withAttribute('identifier', $identifier);

if ($contentType = $request->getHeader('Content-Type')) {
$serverRequest = $serverRequest->withHeader('Content-Type', $contentType);
}

if ($httpMethod === 'POST' || $httpMethod === 'PUT') {
$serverRequest = $serverRequest->withParsedBody($this->getRequestBody($request));
}

$response = (new $className())->handle($serverRequest);
} catch (HttpExceptionInterface $e) {
$response = new Response(
$e->getStatusCode(),
array_merge($e->getHeaders(), ['Content-Type' => 'application/json']),

Check failure on line 81 in application/controllers/ApiController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.2 on ubuntu-latest

Parameter #2 $headers of class GuzzleHttp\Psr7\Response constructor expects array<array<string>|string>, array given.

Check failure on line 81 in application/controllers/ApiController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.3 on ubuntu-latest

Parameter #2 $headers of class GuzzleHttp\Psr7\Response constructor expects array<array<string>|string>, array given.

Check failure on line 81 in application/controllers/ApiController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.4 on ubuntu-latest

Parameter #2 $headers of class GuzzleHttp\Psr7\Response constructor expects array<array<string>|string>, array given.
Json::sanitize(['message' => $e->getMessage()])
);
} catch (Throwable $e) {
Logger::error($e);
Logger::debug(IcingaException::getConfidentialTraceAsString($e));
$response = new Response(
500,
['Content-Type' => 'application/json'],
Json::sanitize(['message' => 'An error occurred, please chack the log.'])
);
} finally {
$this->emitResponse($response);

Check failure on line 93 in application/controllers/ApiController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.2 on ubuntu-latest

Variable $response might not be defined.

Check failure on line 93 in application/controllers/ApiController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.3 on ubuntu-latest

Variable $response might not be defined.

Check failure on line 93 in application/controllers/ApiController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.4 on ubuntu-latest

Variable $response might not be defined.
}

exit;
}

/**
* Validate that the request has an appropriate body.
*
* @param Request $request The request object to validate.
*
* @return ?array The validated JSON content as an associative array.
*
* @throws HttpBadRequestException
*/
private function getRequestBody(Request $request): ?array

Check failure on line 108 in application/controllers/ApiController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.2 on ubuntu-latest

Method Icinga\Module\Notifications\Controllers\ApiController::getRequestBody() return type has no value type specified in iterable type array.

Check failure on line 108 in application/controllers/ApiController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.2 on ubuntu-latest

Method Icinga\Module\Notifications\Controllers\ApiController::getRequestBody() never returns null so it can be removed from the return type.

Check failure on line 108 in application/controllers/ApiController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.3 on ubuntu-latest

Method Icinga\Module\Notifications\Controllers\ApiController::getRequestBody() return type has no value type specified in iterable type array.

Check failure on line 108 in application/controllers/ApiController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.3 on ubuntu-latest

Method Icinga\Module\Notifications\Controllers\ApiController::getRequestBody() never returns null so it can be removed from the return type.

Check failure on line 108 in application/controllers/ApiController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.4 on ubuntu-latest

Method Icinga\Module\Notifications\Controllers\ApiController::getRequestBody() return type has no value type specified in iterable type array.

Check failure on line 108 in application/controllers/ApiController.php

View workflow job for this annotation

GitHub Actions / phpstan / Static analysis with phpstan and php 8.4 on ubuntu-latest

Method Icinga\Module\Notifications\Controllers\ApiController::getRequestBody() never returns null so it can be removed from the return type.
{
try {
$data = $request->getPost();
if ($data !== null && ! is_array($data)) {
$this->httpBadRequest('Invalid request body: given content is not a valid JSON');
}
} catch (Exception) {
$this->httpBadRequest('Invalid request body: given content is not a valid JSON');
}

return $data;
}

/**
* Emit the HTTP response to the client.
*
* @param ResponseInterface $response The response object to emit.
*
* @return void
*/
protected function emitResponse(ResponseInterface $response): void
{
do {
ob_end_clean();
} while (ob_get_level() > 0);

http_response_code($response->getStatusCode());

foreach ($response->getHeaders() as $name => $values) {
foreach ($values as $value) {
header(sprintf('%s: %s', $name, $value), false);
}
}
header('Content-Type: application/json');

$body = $response->getBody();
while (! $body->eof()) {
echo $body->read(8192);
}
}
}
2 changes: 2 additions & 0 deletions application/forms/ChannelForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use ipl\Validator\EmailAddressValidator;
use ipl\Web\Common\CsrfCounterMeasure;
use ipl\Web\Compat\CompatForm;
use Ramsey\Uuid\Uuid;

/**
* @phpstan-type ChannelOptionConfig array{
Expand Down Expand Up @@ -213,6 +214,7 @@ public function addChannel(): void
$channel = $this->getValues();
$channel['config'] = json_encode($this->filterConfig($channel['config']), JSON_FORCE_OBJECT);
$channel['changed_at'] = (int) (new DateTime())->format("Uv");
$channel['external_uuid'] = Uuid::uuid4()->toString();

$this->db->transaction(function (Connection $db) use ($channel): void {
$db->insert('channel', $channel);
Expand Down
11 changes: 10 additions & 1 deletion application/forms/ContactGroupForm.php
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ContactForm and the ChannelForm also require this change.

Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use ipl\Web\Compat\CompatForm;
use ipl\Web\FormElement\TermInput;
use ipl\Web\FormElement\TermInput\Term;
use Ramsey\Uuid\Uuid;

class ContactGroupForm extends CompatForm
{
Expand Down Expand Up @@ -181,7 +182,15 @@ public function addGroup(): int
$this->db->beginTransaction();

$changedAt = (int) (new DateTime())->format("Uv");
$this->db->insert('contactgroup', ['name' => trim($data['group_name']), 'changed_at' => $changedAt]);

$this->db->insert(
'contactgroup',
[
'name' => trim($data['group_name']),
'changed_at' => $changedAt,
'external_uuid' => Uuid::uuid4()->toString()
]
);

$groupIdentifier = $this->db->lastInsertId();

Expand Down
5 changes: 5 additions & 0 deletions configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@
$this->translate('Allow to configure contact groups')
);

$this->providePermission(
'notifications/api',
$this->translate('Allow to modify configuration via API')
);

$this->provideRestriction(
'notifications/filter/objects',
$this->translate('Restrict access to the objects that match the filter')
Expand Down
125 changes: 125 additions & 0 deletions library/Notifications/Api/ApiCore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

/* Icinga Notifications Web | (c) 2025 Icinga GmbH | GPLv2 */

namespace Icinga\Module\Notifications\Api;

use GuzzleHttp\Psr7\Response;
use Icinga\Exception\Http\HttpException;
use Icinga\Module\Notifications\Common\HttpMethod;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Server\RequestHandlerInterface;
use ValueError;

/**
* Abstract base class for API endpoints.
*
* This class provides the base functionality for handling API requests,
*/
abstract class ApiCore
{
/**
* Endpoint based request handling.
*
* @param ServerRequestInterface $request
*
* @return ResponseInterface
*/
abstract protected function handleRequest(ServerRequestInterface $request): ResponseInterface;

/**
* Get the name of the API endpoint.
*
* @return string
*/
abstract public function getEndpoint(): string;

/**
* The main entry point for processing API requests.
*
* @param ServerRequestInterface $request The incoming server request.
*
* @return ResponseInterface The response generated by the invoked method.
*
* @throws HttpException If the requested method does not exist.
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
try {
$httpMethod = HttpMethod::fromRequest($request);
} catch (ValueError) {
throw (new HttpException(405, sprintf('HTTP method %s is not supported', $request->getMethod())))
->setHeader('Allow', implode(', ', $this->getAllowedMethods()));
}

$request = $request->withAttribute('httpMethod', $httpMethod);

if (! in_array($httpMethod->uppercase(), $this->getAllowedMethods())) {
throw (new HttpException(
405,
sprintf('Method %s is not supported for endpoint %s', $httpMethod->uppercase(), $this->getEndpoint())
))
->setHeader('Allow', implode(', ', $this->getAllowedMethods()));
}

$this->assertValidRequest($request);

return $this->handleRequest($request);
}

/**
* Validate the incoming request.
*
* Override to implement specific request validation logic.
*
* @param ServerRequestInterface $request The incoming server request to validate.
*
* @return void
*/
protected function assertValidRequest(ServerRequestInterface $request): void
{
}

/**
* Get allowed HTTP methods for the API.
*
* @return array
*/
protected function getAllowedMethods(): array
{
$methods = [];

foreach (HttpMethod::cases() as $method) {
if (method_exists($this, $method->lowercase())) {
$methods[] = $method->uppercase();
}
}

return $methods;
}

/**
* Create a Response object.
*
* @param int $status The HTTP status code.
* @param array $headers An associative array of HTTP headers.
* @param ?(StreamInterface|resource|string) $body The response body.
* @param string $version The HTTP version.
* @param ?string $reason The reason phrase (optional).
*
* @return ResponseInterface
*/
protected function createResponse(
int $status = 200,
array $headers = [],
$body = null,
string $version = '1.1',
?string $reason = null
): ResponseInterface {
$headers['Content-Type'] = 'application/json';

return new Response($status, $headers, $body, $version, $reason);
}
}
Loading
Loading