Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
6132474
Add: column external_uuid & missing column changed_at
Jan-Schuppik Sep 3, 2025
9a622d8
Run with:
nilmerg Aug 11, 2025
f67dc8c
Introduce: PSR Logger
Jan-Schuppik Aug 22, 2025
efce1f9
Fix: test-setup for MySQL compatibility (PostgreSQL tests still failing)
Jan-Schuppik Aug 14, 2025
be015b6
Adjust: unit tests
Jan-Schuppik Aug 22, 2025
e6fac6a
Add: route for new api controller
Jan-Schuppik Aug 22, 2025
47dcb8e
Introduce: new API srtucture (starting with Contact GET)
Jan-Schuppik Aug 22, 2025
bed7f05
Add: initial openapi docs
Jan-Schuppik Aug 22, 2025
a1e1d4f
Adjust: give the request object to ApiCore
Jan-Schuppik Aug 22, 2025
ed824d1
Fix: lint-errors & moduleName fetching
Jan-Schuppik Aug 25, 2025
5c15181
Remove: Icinga Web 2 BaseTestCase dependency
Jan-Schuppik Aug 25, 2025
f3999ce
Adjust: add small review suggestions
Jan-Schuppik Aug 28, 2025
6bd5cbc
Rework: implements psr-15 RequestHandlerInterface
Jan-Schuppik Aug 29, 2025
0dc1def
Adjust: api-version handling
Jan-Schuppik Sep 1, 2025
5ee193a
Fix: request validation & plural-GET result name & test permissions
Jan-Schuppik Sep 1, 2025
9116e17
Adjust: openapi documentation creating & storing
Jan-Schuppik Sep 1, 2025
fe816f8
Fix: Psr-Logger exception-handling
Jan-Schuppik Sep 3, 2025
64912d8
Introduce: PUT method handling
Jan-Schuppik Sep 3, 2025
7a86078
Adjust: Openapi Attributes (+ fix strings)
Jan-Schuppik Sep 3, 2025
3510235
Introduce: POST method handling
Jan-Schuppik Sep 3, 2025
282485d
Fix: phpdocs of endpoint-methods
Jan-Schuppik Sep 3, 2025
fece5c1
Introduce: DELETE method handling
Jan-Schuppik Sep 3, 2025
a256cdc
WIP: introduce psr15 pipeline & validation middleware
Jan-Schuppik Sep 4, 2025
115207b
Fix: contact tests (enable POST with identifier)
Jan-Schuppik Sep 4, 2025
e5b8b3d
Fix: CodeSniffer Errors
Jan-Schuppik Sep 5, 2025
4e0a22c
Add: contactgroup endpoint methods
Jan-Schuppik Sep 5, 2025
e68863b
Fix: contactgroup tests
Jan-Schuppik Sep 5, 2025
f00c61b
Adjust: enriching of get-result-rows
Jan-Schuppik Sep 5, 2025
46619f3
Add: channel GET-methods
Jan-Schuppik Sep 5, 2025
3a9fc09
Fix: wrong Uuid-class & filterString fetching
Jan-Schuppik Sep 5, 2025
ae926b4
Add: channel tests (+ adjust channel filters)
Jan-Schuppik Sep 5, 2025
acb091c
Fix: add missing escape-signs in tests
Jan-Schuppik Sep 5, 2025
5d0d954
Fix: CodeSniffer Errors
Jan-Schuppik Sep 5, 2025
4d6dedc
Remove: error abstraction
Jan-Schuppik Sep 8, 2025
4582a73
Adjust: channel-tests add second default channel
Jan-Schuppik Sep 8, 2025
27ad56a
Remove: middleware & middleware-pipeline
Jan-Schuppik Sep 8, 2025
b90041f
Fix: channel-tests
Jan-Schuppik Sep 9, 2025
d1d9368
Fix: contact-tests
Jan-Schuppik Sep 10, 2025
5a49d86
Fix: contacgroup-tests & contact-tests
Jan-Schuppik Sep 10, 2025
16df14d
Adjust: parse all exceptions in error response (temporary workaround)
Jan-Schuppik Sep 10, 2025
0f39baf
Fix: CodeSniffer Errors
Jan-Schuppik Sep 10, 2025
d36ec70
Adjust: create response only if necessary
Jan-Schuppik Sep 10, 2025
e2db760
Fix: general request-body-validation
Jan-Schuppik Sep 10, 2025
e3a53d6
Adjust: naming of multi-result-response
Jan-Schuppik Sep 11, 2025
41f869e
Fix: request-body-validation & error-throwing
Jan-Schuppik Sep 11, 2025
f650381
Adjust: simplify response-creating
Jan-Schuppik Sep 11, 2025
da1bc68
Adjust: move openapi-logic from core to openapi-endpoint
Jan-Schuppik Sep 11, 2025
9723cba
Fix: unit-tests & adjust behavior
Jan-Schuppik Sep 15, 2025
b230a89
Adjust: simplify dispatching of ApiController
Jan-Schuppik Sep 18, 2025
65c8c27
Adjust: change scope of allowedMethod & generator funcs
Jan-Schuppik Sep 18, 2025
2d14d67
Fix: small mistakes in endpoints openapi & contactgroups
Jan-Schuppik Sep 18, 2025
357a563
Add: methode to control the creation of a Response
Jan-Schuppik Sep 18, 2025
838587d
Adjust: apply review suggestions to tests
Jan-Schuppik Sep 18, 2025
466f740
Adjust: 'expected' variables in test
Jan-Schuppik Sep 18, 2025
b5f4b7e
Adjust: db usage & move general handle-logic to core
Jan-Schuppik Sep 19, 2025
c7d30aa
Remove: http error code 409
Jan-Schuppik Sep 19, 2025
d301bbe
Adjust: change endpoint const in abstract method
Jan-Schuppik Sep 19, 2025
4b5d251
Fix: change unclear test names
Jan-Schuppik Sep 19, 2025
6ac90bd
Adjust: Http enum creation
Jan-Schuppik Sep 19, 2025
38cc01a
Remove: plural-check from ApiCore and handle it in endpoints
Jan-Schuppik Sep 19, 2025
a712bc9
Fix: base assertValidRequest shouldn't provide logic
Jan-Schuppik Sep 19, 2025
e1a51f3
Adjust: beautify filter assembling for plural get
Jan-Schuppik Sep 19, 2025
637fa1a
Adjust: json string creation
Jan-Schuppik Sep 19, 2025
29160aa
Remove: unnecessary stream-request-body method
Jan-Schuppik Sep 19, 2025
101eac3
Fix: move request asserting to from core to v1
Jan-Schuppik Sep 19, 2025
8ba9bb6
Adjust: replace PHP-docs with more descriptive test-names
Jan-Schuppik Sep 19, 2025
6d39ea0
Fix: change remaining test-UUID to constants
Jan-Schuppik Sep 22, 2025
cb846ba
Adjust: review suggestions
Jan-Schuppik Sep 25, 2025
2bc6826
Fix: wrong format of endpoint
Jan-Schuppik Sep 25, 2025
3f349d0
Fix: mysql external_uuid db type
Jan-Schuppik Sep 25, 2025
76eb05b
Fix: cases of class-call in dispatching
Jan-Schuppik Sep 25, 2025
da665ca
Adjust: response header content-type
Jan-Schuppik Sep 26, 2025
22cc2c2
Fix: Code-Sniffer Errors
Jan-Schuppik Sep 26, 2025
439c88b
Adjust: cleanup row preparation for output
Jan-Schuppik Sep 26, 2025
dc36e1c
Adjust: improve the php-docs
Jan-Schuppik Sep 26, 2025
1792eb3
Adjust: cleanup code & set response-header if not provided
Jan-Schuppik Sep 26, 2025
5b1dda2
Adjust: return response instead of assoc array in endpoint-methods
Jan-Schuppik Sep 29, 2025
bf626f2
Fix: change unsuitable names
Jan-Schuppik Sep 29, 2025
b602287
Fix: correct indentations
Jan-Schuppik Sep 29, 2025
a4a8b93
Adjust: use sprintf() to cleanup strings
Jan-Schuppik Sep 29, 2025
93b1f96
Remove: TODOs
Jan-Schuppik Sep 29, 2025
8b0757b
Adjust: identifier-check in post-methods
Jan-Schuppik Sep 29, 2025
efaebe2
Fix: scope of variables initialized in if-condition
Jan-Schuppik Sep 29, 2025
6a7294b
Fix: add spacing to improve readability
Jan-Schuppik Sep 29, 2025
f5978f7
Fix: IDE warnings
Jan-Schuppik Sep 29, 2025
bd5722e
Adjust: use database via singleton instead of initialize variables
Jan-Schuppik Sep 29, 2025
7033717
Fix: exceeding line
Jan-Schuppik Sep 29, 2025
3edeb19
Fix: unit-tests for new database-trait
Jan-Schuppik Sep 29, 2025
8b95b09
Fix: scope of a variable
Jan-Schuppik Sep 30, 2025
fa3c98e
Adjust: make unit-tests independent
Jan-Schuppik Sep 30, 2025
bb49065
Fix: use getConnection() (need ipl-sql shared-test-databases updated)
Jan-Schuppik Sep 30, 2025
e4dd073
Fix: unit-test errors caused by databases
Jan-Schuppik Oct 1, 2025
98f37f7
Revert "Fix: unit-test errors caused by databases"
Jan-Schuppik Oct 2, 2025
2de692e
Introduce and use new `ApiTestBackends` data provider
nilmerg Oct 2, 2025
5a419f5
Fix: unit-tests according to new data provider
Jan-Schuppik Oct 6, 2025
c381c9f
Fix: Code-Sniffer Errors
Jan-Schuppik Oct 6, 2025
0548fbf
Fix: softdelete-blocks-uuid problem
Jan-Schuppik Oct 7, 2025
af2ba99
php: Spin up database backends for unit tests
nilmerg Oct 8, 2025
bcee5a0
tests: Set up Icinga Web Database
nilmerg Oct 8, 2025
58fb45d
ApiTestBackends: Pass through `ICINGAWEB_LIBDIR` to webserver
nilmerg Oct 8, 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
136 changes: 136 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,136 @@
<?php

namespace Icinga\Module\Notifications\Controllers;

use Exception;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\ServerRequest;
use Icinga\Exception\Http\HttpBadRequestException;
use Icinga\Exception\Http\HttpExceptionInterface;
use Icinga\Exception\Json\JsonEncodeException;
use Icinga\Module\Notifications\Api\V1\ApiV1;
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 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 39 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 39 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 39 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 45 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 45 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 45 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 46 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 46 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 46 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, ApiV1::class)) {
Comment on lines +54 to +55
Copy link
Member

Choose a reason for hiding this comment

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

Then I'd suggest to check for Psr\Http\Server\RequestHandlerInterface instead, given that this is the only assumption this actions makes for this class: being able to call handle and pass it a request.

Copy link
Member

Choose a reason for hiding this comment

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

But I see that we're going in circles here. Earlier, an abstract check for reflection was used instead. Now, ApiV1 implements the interface I mentioned above. Let me propose something in addition: Do not let the base class implement the interface, the actual endpoint classes should implement it.

$this->httpNotFound("Endpoint $endpoint does not exist.");
}

$serverRequest = (new ServerRequest(
$request->getMethod(),
$request->getRequestUri(),
['Content-Type' => $request->getHeader('Content-Type')],

Check failure on line 62 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 #3 $headers of class GuzzleHttp\Psr7\ServerRequest constructor expects array<array<string>|string>, array<string, string|false> given.

Check failure on line 62 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 #3 $headers of class GuzzleHttp\Psr7\ServerRequest constructor expects array<array<string>|string>, array<string, string|false> given.

Check failure on line 62 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 #3 $headers of class GuzzleHttp\Psr7\ServerRequest constructor expects array<array<string>|string>, array<string, string|false> given.
Copy link
Member

Choose a reason for hiding this comment

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

phpstan is on point here. It's not guaranteed at this stage that this header is part of the request, is it?

serverParams: $request->getServer(),
))
->withParsedBody($this->getRequestBody($request))
->withAttribute('identifier', $identifier);

$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 72 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 72 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 72 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) {
$response = new Response(
Copy link
Member

Choose a reason for hiding this comment

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

Log the error:

Suggested change
$response = new Response(
Icinga\Application\Logger::error($e);
Icinga\Application\Logger::debug(IcingaException::getConfidentialTraceAsString($e));
$response = new Response(

500,
['Content-Type' => 'application/json'],
Json::sanitize(['message' => $e->getMessage()])
Copy link
Member

Choose a reason for hiding this comment

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

Do not use the exception message. Instead, An error occurred. Please check the log

);
} finally {
$this->emitResponse($response);

Check failure on line 82 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 82 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 82 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
* @throws Zend_Controller_Request_Exception
*/
private function getRequestBody(Request $request): ?array

Check failure on line 98 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 98 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 98 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.
{
if (
! preg_match('/([^;]*);?/', $request->getHeader('Content-Type'), $matches)

Check failure on line 101 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 $subject of function preg_match expects string, string|false given.

Check failure on line 101 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 $subject of function preg_match expects string, string|false given.

Check failure on line 101 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 $subject of function preg_match expects string, string|false given.
|| $matches[1] !== 'application/json'
Copy link
Member

Choose a reason for hiding this comment

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

  • ApiV1::getValidRequestBody() throws in this case
  • This returns null, effectively preventing any handling of non-JSON

I must ask: Why does this not throw already? Or why does it check for JSON?

) {
return null;
}

try {
$data = $request->getPost();
} 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
{
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');

echo $response->getBody();
Comment on lines +125 to +134
Copy link
Member

Choose a reason for hiding this comment

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

Prior to this, stop output buffering:

do {
    ob_end_clean();
} while (ob_get_level() > 0);

We're dealing with potentially large responses here and I don't want to really buffer everything twice.

Which brings me to the next suggestion: Don't cast the body to string.

You get a StreamInterface, handle it as such. Didn't look at the endpoints yet, but they have to produce streams, at least those that return large result sets (i.e. plural indexes).

}
}
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
123 changes: 123 additions & 0 deletions library/Notifications/Api/ApiCore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

namespace Icinga\Module\Notifications\Api;

use GuzzleHttp\Psr7\Response;
use Icinga\Exception\Http\HttpException;
use Icinga\Module\Notifications\Api\Elements\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 implements RequestHandlerInterface
{
/**
* 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::from(strtolower($request->getMethod()));
Copy link
Member

Choose a reason for hiding this comment

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

🎉

(GitHub, allow to react on changed lines!)

Copy link
Member

Choose a reason for hiding this comment

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

Though, you could go a step further: HttpMethod::fromRequest() 😁

} catch (ValueError) {
throw (new HttpException(405, sprintf('HTTP method %s is not supported', $request->getMethod())))
->setHeader('Allow', $this->getAllowedMethods());
}

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

if (! method_exists($this, $httpMethod->lowercase())) {
Copy link
Member

Choose a reason for hiding this comment

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

May I suggest to use $this->getAllowedMethods() here instead? Of course, it must return an array for this, which isn't a bad idea either, the string join is only necessary in case of the header definition and doing it then on demand seems fine to me.

throw (new HttpException(
405,
sprintf('Method %s is not supported for endpoint %s', $httpMethod->uppercase(), $this->getEndpoint())
))
->setHeader('Allow', $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 string
*/
protected function getAllowedMethods(): string
{
$methods = [];

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

return implode(', ', $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.
Copy link
Member

Choose a reason for hiding this comment

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

Can't this be part of the native type?

* @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);
}
}
30 changes: 30 additions & 0 deletions library/Notifications/Api/Elements/HttpMethod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Icinga\Module\Notifications\Api\Elements;
Copy link
Member

Choose a reason for hiding this comment

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

Elements? Why not inside Notifications\Common?


enum HttpMethod: string
{
case GET = 'get';
case POST = 'post';
case PUT = 'put';
case DELETE = 'delete';


Comment on lines +11 to +12
Copy link
Member

Choose a reason for hiding this comment

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

single empyt lines

/**
* Returns the current enum case as string in uppercase.
*
* @return string
*/
public function uppercase(): string
{
return $this->name;
}

/**
* Returns the current enum case as string in lowercase.
Copy link
Member

Choose a reason for hiding this comment

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

return type missing

*/
public function lowercase(): string
{
return $this->value;
}
}
Loading
Loading