Skip to content

Commit 7187db3

Browse files
Introduce ApiV1(Contacts/Contactgroups)Controller
1 parent 5c4fc70 commit 7187db3

File tree

2 files changed

+938
-0
lines changed

2 files changed

+938
-0
lines changed
Lines changed: 387 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,387 @@
1+
<?php
2+
3+
/* Icinga Notifications Web | (c) 2024 Icinga GmbH | GPLv2 */
4+
5+
namespace Icinga\Module\Notifications\Controllers;
6+
7+
use Icinga\Exception\Http\HttpBadRequestException;
8+
use Icinga\Exception\Http\HttpException;
9+
use Icinga\Exception\Http\HttpNotFoundException;
10+
use Icinga\Module\Notifications\Common\Database;
11+
use Icinga\Util\Environment;
12+
use Icinga\Util\Json;
13+
use ipl\Sql\Compat\FilterProcessor;
14+
use ipl\Sql\Select;
15+
use ipl\Stdlib\Filter;
16+
use ipl\Web\Compat\CompatController;
17+
use ipl\Web\Filter\QueryString;
18+
use ipl\Web\Url;
19+
use Ramsey\Uuid\Uuid;
20+
21+
class ApiV1ContactgroupsController extends CompatController
22+
{
23+
private const ENDPOINT = 'notifications/api/v1/contactgroups';
24+
25+
/**
26+
* @return void
27+
*/
28+
public function indexAction(): void
29+
{
30+
$this->assertPermission('notifications/api/v1');
31+
32+
$request = $this->getRequest();
33+
if (! $request->isApiRequest()) {
34+
$this->httpBadRequest('No API request');
35+
}
36+
37+
$method = $request->getMethod();
38+
if (in_array($method, ['POST', 'PUT'])
39+
&& (! preg_match('/([^;]*);?/', $request->getHeader('Content-Type'), $matches)
40+
|| $matches[1] !== 'application/json'
41+
)
42+
) {
43+
$this->httpBadRequest('No JSON content');
44+
}
45+
46+
$results = [];
47+
$responseCode = 200;
48+
$db = Database::get();
49+
$identifier = $request->getParam('identifier');
50+
51+
if ($identifier && ! Uuid::isValid($identifier)) {
52+
$this->httpBadRequest('The given identifier is not a valid UUID');
53+
}
54+
55+
$filter = FilterProcessor::assembleFilter(
56+
QueryString::fromString(Url::fromRequest()->getQueryString())
57+
->on(
58+
QueryString::ON_CONDITION,
59+
function (Filter\Condition $condition) {
60+
$column = $condition->getColumn();
61+
if (! in_array($column, ['id', 'name'])) {
62+
$this->httpBadRequest(sprintf(
63+
'Invalid filter column %s given, only id and name are allowed',
64+
$column
65+
));
66+
}
67+
68+
if ($column === 'id') {
69+
if (! Uuid::isValid($condition->getValue())) {
70+
$this->httpBadRequest('The given filter id is not a valid UUID');
71+
}
72+
73+
$condition->setColumn('external_uuid');
74+
}
75+
}
76+
)->parse()
77+
);
78+
79+
switch ($method) {
80+
case 'GET':
81+
$stmt = (new Select())
82+
->distinct()
83+
->from('contactgroup cg')
84+
->columns([
85+
'contactgroup_id' => 'cg.id',
86+
'id' => 'cg.external_uuid',
87+
'name'
88+
]);
89+
90+
if ($identifier !== null) {
91+
$stmt->where(['external_uuid = ?' => $identifier]);
92+
$result = $db->fetchOne($stmt);
93+
94+
if ($result === false) {
95+
$this->httpNotFound('Contactgroup not found');
96+
}
97+
98+
$users = $this->fetchUserIdentifiers($result->contactgroup_id);
99+
if ($users) {
100+
$result->users = $users;
101+
}
102+
103+
unset($result->contactgroup_id);
104+
$results[] = $result;
105+
106+
break;
107+
}
108+
109+
if ($filter !== null) {
110+
$stmt->where($filter);
111+
}
112+
113+
$stmt->limit(500);
114+
$offset = 0;
115+
116+
ob_end_clean();
117+
Environment::raiseExecutionTime();
118+
119+
$this->getResponse()
120+
->setHeader('Content-Type', 'application/json')
121+
->setHeader('Cache-Control', 'no-store')
122+
->sendResponse();
123+
124+
echo '[';
125+
126+
$res = $db->select($stmt->offset($offset));
127+
do {
128+
foreach ($res as $i => $row) {
129+
$users = $this->fetchUserIdentifiers($row->contactgroup_id);
130+
if ($users) {
131+
$row->users = $users;
132+
}
133+
134+
if ($i > 0 || $offset !== 0) {
135+
echo ",\n";
136+
}
137+
138+
unset($row->contactgroup_id);
139+
140+
echo Json::sanitize($row);
141+
}
142+
143+
$offset += 500;
144+
$res = $db->select($stmt->offset($offset));
145+
} while ($res->rowCount());
146+
147+
echo ']';
148+
149+
exit;
150+
case 'POST':
151+
if ($filter !== null) {
152+
$this->httpBadRequest('Cannot filter on POST');
153+
}
154+
155+
$data = $request->getPost();
156+
157+
$this->assertValidData($data);
158+
159+
if ($this->getContactgroupId($data['id']) !== null) {
160+
throw new HttpException(422, 'Contactgroup already exists');
161+
}
162+
163+
$db->beginTransaction();
164+
165+
if ($identifier === null) {
166+
$this->addContactgroup($data);
167+
} else {
168+
$contactgroupId = $this->getContactgroupId($identifier);
169+
if ($contactgroupId === null) {
170+
$this->httpNotFound('Contactgroup not found');
171+
}
172+
173+
$this->removeContactgroup($contactgroupId);
174+
$this->addContactgroup($data);
175+
}
176+
177+
$db->commitTransaction();
178+
179+
$this->getResponse()->setHeader('Location', self::ENDPOINT . '/' . $data['id']);
180+
$responseCode = 201;
181+
182+
break;
183+
case 'PUT':
184+
if ($identifier === null) {
185+
$this->httpBadRequest('Identifier is required');
186+
}
187+
188+
$data = $request->getPost();
189+
190+
$this->assertValidData($data);
191+
192+
if ($identifier !== $data['id']) {
193+
$this->httpBadRequest('Identifier mismatch');
194+
}
195+
196+
$db->beginTransaction();
197+
198+
$contactgroupId = $this->getContactgroupId($identifier);
199+
if ($contactgroupId !== null) {
200+
$db->update('contactgroup', ['name' => $data['name']], ['id = ?' => $contactgroupId]);
201+
202+
$db->delete('contactgroup_member', ['contactgroup_id = ?' => $contactgroupId]);
203+
204+
if (! empty($data['users'])) {
205+
$this->addUsers($contactgroupId, $data['users']);
206+
}
207+
208+
$responseCode = 204;
209+
} else {
210+
$this->addContactgroup($data);
211+
$responseCode = 201;
212+
}
213+
214+
$db->commitTransaction();
215+
216+
break;
217+
case 'DELETE':
218+
if ($identifier === null) {
219+
$this->httpBadRequest('Identifier is required');
220+
}
221+
222+
$db->beginTransaction();
223+
224+
$contactgroupId = $this->getContactgroupId($identifier);
225+
if ($contactgroupId === null) {
226+
$this->httpNotFound('Contactgroup not found');
227+
}
228+
229+
$this->removeContactgroup($contactgroupId);
230+
231+
$db->commitTransaction();
232+
233+
$responseCode = 204;
234+
235+
break;
236+
default:
237+
$this->httpBadRequest('Invalid method');
238+
}
239+
240+
$this->getResponse()
241+
->setHttpResponseCode($responseCode)
242+
->json()
243+
->setSuccessData($results)
244+
->sendResponse();
245+
}
246+
247+
/**
248+
* Fetch the user(contact) identifiers of the contactgroup with the given id
249+
*
250+
* @param int $contactgroupId
251+
*
252+
* @return ?string[]
253+
*/
254+
private function fetchUserIdentifiers(int $contactgroupId): ?array
255+
{
256+
$users = Database::get()->fetchCol(
257+
(new Select())
258+
->from('contactgroup_member cgm')
259+
->columns('co.external_uuid')
260+
->joinLeft('contact co', 'co.id = cgm.contact_id')
261+
->where(['cgm.contactgroup_id = ?' => $contactgroupId])
262+
->groupBy('co.external_uuid')
263+
);
264+
265+
return ! empty($users) ? $users : null;
266+
}
267+
268+
/**
269+
* Assert that the given user IDs exist
270+
*
271+
* @param string $identifier
272+
*
273+
* @return int
274+
*
275+
* @throws HttpNotFoundException if the user with the given identifier does not exist
276+
*/
277+
private function getUserId(string $identifier): int
278+
{
279+
$user = Database::get()->fetchOne(
280+
(new Select())
281+
->from('contact')
282+
->columns('id')
283+
->where(['external_uuid = ?' => $identifier])
284+
);
285+
286+
if ($user === false) {
287+
throw new HttpNotFoundException(sprintf('User with identifier %s not found', $identifier));
288+
}
289+
290+
return $user->id;
291+
}
292+
293+
/**
294+
* Get the contactgroup id with the given identifier
295+
*
296+
* @param string $identifier
297+
*
298+
* @return ?int Returns null, if contact does not exist
299+
*/
300+
private function getContactgroupId(string $identifier): ?int
301+
{
302+
$contactgroup = Database::get()->fetchOne(
303+
(new Select())
304+
->from('contactgroup')
305+
->columns('id')
306+
->where(['external_uuid = ?' => $identifier])
307+
);
308+
309+
return $contactgroup->id ?? null;
310+
}
311+
312+
/**
313+
* Add a new contactgroup with the given data
314+
*
315+
* @param array<string, mixed> $data
316+
*/
317+
private function addContactgroup(array $data): void
318+
{
319+
Database::get()->insert('contactgroup', [
320+
'name' => $data['name'],
321+
'external_uuid' => $data['id']
322+
]);
323+
324+
$id = Database::get()->lastInsertId();
325+
326+
if (! empty($data['users'])) {
327+
$this->addUsers($id, $data['users']);
328+
}
329+
}
330+
331+
/**
332+
* Add the given users as contactgroup_member with the given id
333+
*
334+
* @param int $contactgroupId
335+
* @param string[] $users
336+
*
337+
* @return void
338+
*/
339+
private function addUsers(int $contactgroupId, array $users): void
340+
{
341+
foreach ($users as $identifier) {
342+
$contactId = $this->getUserId($identifier);
343+
344+
Database::get()->insert('contactgroup_member', [
345+
'contactgroup_id' => $contactgroupId,
346+
'contact_id' => $contactId
347+
]);
348+
}
349+
}
350+
351+
/**
352+
* Remove the contactgroup with the given id
353+
*
354+
* @param int $id
355+
*/
356+
private function removeContactgroup(int $id): void
357+
{
358+
Database::get()->delete('contactgroup_member', ['contactgroup_id = ?' => $id]);
359+
Database::get()->delete('contactgroup', ['id = ?' => $id]);
360+
}
361+
362+
/**
363+
* Assert that the given data contains the required fields
364+
*
365+
* @param array<string, mixed> $data
366+
*
367+
* @throws HttpBadRequestException
368+
*/
369+
private function assertValidData(array $data): void
370+
{
371+
if (! isset($data['id'], $data['name'])) {
372+
$this->httpBadRequest('The request body must contain the fields id and name');
373+
}
374+
375+
if (! Uuid::isValid($data['id'])) {
376+
$this->httpBadRequest('Given id in request body is not a valid UUID');
377+
}
378+
379+
if (! empty($data['users'])) {
380+
foreach ($data['users'] as $user) {
381+
if (! Uuid::isValid($user)) {
382+
$this->httpBadRequest('User identifiers in request body must be valid UUIDs');
383+
}
384+
}
385+
}
386+
}
387+
}

0 commit comments

Comments
 (0)