-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/http api new approach #348
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
001cf17
92778e1
8131fca
f02e3f2
a1b8b2b
481f9df
fc77a2c
a7eb80b
df74d4e
c219ea7
07d3919
2b7ee4c
710cf89
5891dac
c772342
544c673
b846798
5664f94
5bf799c
cdfdfb3
bcb5e57
cf63e87
dff73f0
caa5ffd
40333b6
c97f42d
0e5c409
eb198e2
ca67141
a67cd65
ad08004
c89da33
c466160
f8ebcde
b2d7c7a
03453a4
86f71d7
1095d7c
14f9eed
bde3390
c67c059
f9a03c3
1d019f5
2881291
b358f90
e051fef
f632d1e
0a8c530
e3f631b
23d4d7d
5cb4fc1
e97d98d
4dc3803
97423fb
f3e550a
49efec2
3009ae1
c2b7fda
32a87ee
9d24f14
4018274
9b7a3c9
ad2861d
0d3bcc1
ecf502a
b91a66f
584ac80
2525735
aa43277
c8191d0
d515000
22c102e
6a5b374
96e1ad5
b9c5747
ab22f12
5393d81
1b10084
8d4c69e
1d71c5c
c1d2197
b22fbe8
3d9aa4f
a9cf9ba
e05c915
82a28be
d75633e
25909c2
d8af316
154fe88
8813ec1
fba9c10
53784c3
4e94dee
e965792
fcd79f2
5959e1f
7bba0f1
048e39b
32b78fe
7cdbe98
8850461
c864495
9e5bd80
481cb98
4a9f31e
40cc67a
a751d49
bb966be
8beb160
5494522
697c98c
70d73eb
6289d7e
ff4188f
c0fe743
e06838d
26307e8
3526533
c774c90
00d3880
06a2750
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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
|
||
) { | ||
$this->httpBadRequest('No API request'); | ||
} | ||
|
||
$params = $request->getParams(); | ||
$version = ucfirst($params['version']); | ||
Check failure on line 47 in application/controllers/ApiController.php
|
||
$endpoint = ucfirst(Str::camel($params['endpoint'])); | ||
Check failure on line 48 in application/controllers/ApiController.php
|
||
$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
|
||
Json::sanitize(['message' => $e->getMessage()]) | ||
); | ||
} catch (Throwable $e) { | ||
Logger::error($e); | ||
Logger::debug(IcingaException::getConfidentialTraceAsString($e)); | ||
$response = new Response( | ||
nilmerg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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
|
||
} | ||
|
||
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
|
||
{ | ||
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); | ||
} | ||
} | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
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); | ||
nilmerg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 | ||
sukhwinder33445 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
} | ||
sukhwinder33445 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* 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. | ||
nilmerg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* @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); | ||
} | ||
} |
There was a problem hiding this comment.
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.