diff --git a/application/controllers/ApiV1ChannelsController.php b/application/controllers/ApiV1ChannelsController.php new file mode 100644 index 00000000..ef3ee417 --- /dev/null +++ b/application/controllers/ApiV1ChannelsController.php @@ -0,0 +1,141 @@ +assertPermission('notifications/api/v1'); + + $request = $this->getRequest(); + if (! $request->isApiRequest()) { + $this->httpBadRequest('No API request'); + } + + $method = $request->getMethod(); + if ($method !== 'GET') { + $this->httpBadRequest('Only GET method supported'); + } + + $db = Database::get(); + + /** @var ?string $identifier */ + $identifier = $request->getParam('identifier'); + + if ($identifier && ! Uuid::isValid($identifier)) { + $this->httpBadRequest('The given identifier is not a valid UUID'); + } + + try { + $filterRule = QueryString::fromString(Url::fromRequest()->getQueryString()) + ->on( + QueryString::ON_CONDITION, + function (Filter\Condition $condition) { + $column = $condition->getColumn(); + if (! in_array($column, ['id', 'name', 'type'])) { + $this->httpBadRequest(sprintf( + 'Invalid filter column %s given, only id, name and type are allowed', + $column + )); + } + + if ($column === 'id') { + if (! Uuid::isValid($condition->getValue())) { + $this->httpBadRequest('The given filter id is not a valid UUID'); + } + + $condition->setColumn('external_uuid'); + } + } + )->parse(); + + $filter = FilterProcessor::assembleFilter($filterRule); + } catch (Exception $e) { + $this->httpBadRequest($e->getMessage()); + } + + $stmt = (new Select()) + ->distinct() + ->from('channel ch') + ->columns([ + 'channel_id' => 'ch.id', + 'id' => 'ch.external_uuid', + 'name', + 'type', + 'config' + ]); + + if ($identifier !== null) { + $stmt->where(['external_uuid = ?' => $identifier]); + + /** @var stdClass|false $result */ + $result = $db->fetchOne($stmt); + + if ($result === false) { + $this->httpNotFound('Channel not found'); + } + + unset($result->channel_id); + + $this->getResponse() + ->setHttpResponseCode(200) + ->json() + ->setSuccessData((array) $result) + ->sendResponse(); + } else { + if ($filter !== null) { + $stmt->where($filter); + } + + $stmt->limit(500); + $offset = 0; + + ob_end_clean(); + Environment::raiseExecutionTime(); + + $this->getResponse() + ->setHeader('Content-Type', 'application/json') + ->setHeader('Cache-Control', 'no-store') + ->sendResponse(); + + echo '['; + + $res = $db->select($stmt->offset($offset)); + do { + /** @var stdClass $row */ + foreach ($res as $i => $row) { + if ($i > 0 || $offset !== 0) { + echo ",\n"; + } + + unset($row->channel_id); + + echo Json::sanitize($row); + } + + $offset += 500; + $res = $db->select($stmt->offset($offset)); + } while ($res->rowCount()); + + echo ']'; + } + + exit; + } +} diff --git a/application/controllers/ApiV1ContactgroupsController.php b/application/controllers/ApiV1ContactgroupsController.php new file mode 100644 index 00000000..6492be3c --- /dev/null +++ b/application/controllers/ApiV1ContactgroupsController.php @@ -0,0 +1,445 @@ +assertPermission('notifications/api/v1'); + + $request = $this->getRequest(); + if (! $request->isApiRequest()) { + $this->httpBadRequest('No API request'); + } + + $method = $request->getMethod(); + if ( + in_array($method, ['POST', 'PUT']) + && ( + ! preg_match('/([^;]*);?/', $request->getHeader('Content-Type'), $matches) + || $matches[1] !== 'application/json' + ) + ) { + $this->httpBadRequest('No JSON content'); + } + + $results = []; + $responseCode = 200; + $db = Database::get(); + + /** @var ?string $identifier */ + $identifier = $request->getParam('identifier'); + + if ($identifier && ! Uuid::isValid($identifier)) { + $this->httpBadRequest('The given identifier is not a valid UUID'); + } + + $filterStr = Url::fromRequest()->getQueryString(); + if ($method !== 'GET' && $filterStr) { + $this->httpBadRequest('Filter is only allowed for GET requests'); + } + + try { + $filterRule = QueryString::fromString($filterStr) + ->on( + QueryString::ON_CONDITION, + function (Filter\Condition $condition) { + $column = $condition->getColumn(); + if (! in_array($column, ['id', 'name'])) { + $this->httpBadRequest(sprintf( + 'Invalid filter column %s given, only id and name are allowed', + $column + )); + } + + if ($column === 'id') { + if (! Uuid::isValid($condition->getValue())) { + $this->httpBadRequest('The given filter id is not a valid UUID'); + } + + $condition->setColumn('external_uuid'); + } + } + )->parse(); + + $filter = FilterProcessor::assembleFilter($filterRule); + } catch (Exception $e) { + $this->httpBadRequest($e->getMessage()); + } + + switch ($method) { + case 'GET': + $stmt = (new Select()) + ->distinct() + ->from('contactgroup cg') + ->columns([ + 'contactgroup_id' => 'cg.id', + 'id' => 'cg.external_uuid', + 'name' + ]); + + if ($identifier !== null) { + $stmt->where(['external_uuid = ?' => $identifier]); + + /** @var stdClass|false $result */ + $result = $db->fetchOne($stmt); + + if ($result === false) { + $this->httpNotFound('Contactgroup not found'); + } + + $result->users = $this->fetchUserIdentifiers($result->contactgroup_id); + + unset($result->contactgroup_id); + $results[] = $result; + + break; + } + + if ($filter !== null) { + $stmt->where($filter); + } + + $stmt->limit(500); + $offset = 0; + + ob_end_clean(); + Environment::raiseExecutionTime(); + + $this->getResponse() + ->setHeader('Content-Type', 'application/json') + ->setHeader('Cache-Control', 'no-store') + ->sendResponse(); + + echo '['; + + $res = $db->select($stmt->offset($offset)); + do { + /** @var stdClass $row */ + foreach ($res as $i => $row) { + $row->users = $this->fetchUserIdentifiers($row->contactgroup_id); + + if ($i > 0 || $offset !== 0) { + echo ",\n"; + } + + unset($row->contactgroup_id); + + echo Json::sanitize($row); + } + + $offset += 500; + $res = $db->select($stmt->offset($offset)); + } while ($res->rowCount()); + + echo ']'; + + exit; + case 'POST': + if ($filter !== null) { + $this->httpBadRequest('Cannot filter on POST'); + } + + $data = $this->getValidatedData(); + + $db->beginTransaction(); + + if ($identifier === null) { + if ($this->getContactgroupId($data['id']) !== null) { + throw new HttpException(422, 'Contactgroup already exists'); + } + + $this->addContactgroup($data); + } else { + $contactgroupId = $this->getContactgroupId($identifier); + if ($contactgroupId === null) { + $this->httpNotFound('Contactgroup not found'); + } + + if ($identifier === $data['id'] || $this->getContactgroupId($data['id']) !== null) { + throw new HttpException(422, 'Contactgroup already exists'); + } + + $this->removeContactgroup($contactgroupId); + $this->addContactgroup($data); + } + + $db->commitTransaction(); + + $this->getResponse()->setHeader('Location', self::ENDPOINT . '/' . $data['id']); + $responseCode = 201; + + break; + case 'PUT': + if ($identifier === null) { + $this->httpBadRequest('Identifier is required'); + } + + $data = $this->getValidatedData(); + + if ($identifier !== $data['id']) { + $this->httpBadRequest('Identifier mismatch'); + } + + $db->beginTransaction(); + + $contactgroupId = $this->getContactgroupId($identifier); + if ($contactgroupId !== null) { + $db->update('contactgroup', ['name' => $data['name']], ['id = ?' => $contactgroupId]); + $db->update('contactgroup_member', ['deleted' => 'y'], ['contactgroup_id = ?' => $contactgroupId, 'deleted = ?' => 'n']); + + if (! empty($data['users'])) { + $this->addUsers($contactgroupId, $data['users']); + } + + $responseCode = 204; + } else { + $this->addContactgroup($data); + $responseCode = 201; + } + + $db->commitTransaction(); + + break; + case 'DELETE': + if ($identifier === null) { + $this->httpBadRequest('Identifier is required'); + } + + $db->beginTransaction(); + + $contactgroupId = $this->getContactgroupId($identifier); + if ($contactgroupId === null) { + $this->httpNotFound('Contactgroup not found'); + } + + $this->removeContactgroup($contactgroupId); + + $db->commitTransaction(); + + $responseCode = 204; + + break; + default: + $this->httpBadRequest('Invalid method'); + } + + $this->getResponse() + ->setHttpResponseCode($responseCode) + ->json() + ->setSuccessData($results) + ->sendResponse(); + } + + /** + * Fetch the user(contact) identifiers of the contactgroup with the given id + * + * @param int $contactgroupId + * + * @return string[] + */ + private function fetchUserIdentifiers(int $contactgroupId): array + { + return Database::get()->fetchCol( + (new Select()) + ->from('contactgroup_member cgm') + ->columns('co.external_uuid') + ->joinLeft('contact co', 'co.id = cgm.contact_id') + ->where(['cgm.contactgroup_id = ?' => $contactgroupId]) + ->groupBy('co.external_uuid') + ); + } + + /** + * Assert that the given user IDs exist + * + * @param string $identifier + * + * @return int + * + * @throws HttpNotFoundException if the user with the given identifier does not exist + */ + private function getUserId(string $identifier): int + { + /** @var stdClass|false $user */ + $user = Database::get()->fetchOne( + (new Select()) + ->from('contact') + ->columns('id') + ->where(['external_uuid = ?' => $identifier]) + ); + + if ($user === false) { + $this->httpNotFound(sprintf('User with identifier %s not found', $identifier)); + } + + return $user->id; + } + + /** + * Get the contactgroup id with the given identifier + * + * @param string $identifier + * + * @return ?int Returns null, if contact does not exist + */ + private function getContactgroupId(string $identifier): ?int + { + /** @var stdClass|false $contactgroup */ + $contactgroup = Database::get()->fetchOne( + (new Select()) + ->from('contactgroup') + ->columns('id') + ->where(['external_uuid = ?' => $identifier]) + ); + + return $contactgroup->id ?? null; + } + + /** + * Add a new contactgroup with the given data + * + * @param requestBody $data + * + * @return void + */ + private function addContactgroup(array $data): void + { + Database::get()->insert('contactgroup', [ + 'name' => $data['name'], + 'external_uuid' => $data['id'] + ]); + + $id = Database::get()->lastInsertId(); + + if (! empty($data['users'])) { + $this->addUsers($id, $data['users']); + } + } + + /** + * Add the given users as contactgroup_member with the given id + * + * @param int $contactgroupId + * @param string[] $users + * + * @return void + */ + private function addUsers(int $contactgroupId, array $users): void + { + foreach ($users as $identifier) { + $contactId = $this->getUserId($identifier); + + Database::get()->insert('contactgroup_member', [ + 'contactgroup_id' => $contactgroupId, + 'contact_id' => $contactId + ]); + } + } + + /** + * Remove the contactgroup with the given id + * + * @param int $id + * + * @return void + */ + private function removeContactgroup(int $id): void + { + $db = Database::get(); + $markAsDeleted = ['deleted' => 'y']; + + $db->update( + 'rotation_member', + $markAsDeleted + ['position' => null], + ['contactgroup_id = ?' => $id, 'deleted = ?' => 'n'] + ); + + $db->update( + 'rule_escalation_recipient', + $markAsDeleted, + ['contactgroup_id = ?' => $id, 'deleted = ?' => 'n'] + ); + + $db->update('contactgroup_member', $markAsDeleted, ['contactgroup_id = ?' => $id, 'deleted = ?' => 'n']); + $db->update('contactgroup', $markAsDeleted, ['id = ?' => $id]); + + //TODO: properly remove rotations|escalations with no members as in form + } + + /** + * Get the validated POST|PUT request data + * + * @return requestBody + * + * @throws HttpBadRequestException if the request body is invalid + */ + private function getValidatedData(): array + { + $msgPrefix = 'Invalid request body: '; + + try { + $data = $this->getRequest()->getPost(); + } catch (Exception $e) { + $this->httpBadRequest($msgPrefix . 'given content is not a valid JSON'); + } + + if ( + ! isset($data['id'], $data['name']) + || ! is_string($data['id']) + || ! is_string($data['name']) + ) { + $this->httpBadRequest( + $msgPrefix . 'the fields id and name must be present and of type string' + ); + } + + if (! Uuid::isValid($data['id'])) { + $this->httpBadRequest($msgPrefix . 'given id is not a valid UUID'); + } + + if (! empty($data['users'])) { + if (! is_array($data['users'])) { + $this->httpBadRequest($msgPrefix . 'expects users to be an array'); + } + + foreach ($data['users'] as $user) { + if (! is_string($user) || ! Uuid::isValid($user)) { + $this->httpBadRequest($msgPrefix . 'user identifiers must be valid UUIDs'); + } + + //TODO: check if users exist, here? + } + } + + /** @var requestBody $data */ + return $data; + } +} diff --git a/application/controllers/ApiV1ContactsController.php b/application/controllers/ApiV1ContactsController.php new file mode 100644 index 00000000..44a82eb7 --- /dev/null +++ b/application/controllers/ApiV1ContactsController.php @@ -0,0 +1,609 @@ + + * } + */ +class ApiV1ContactsController extends CompatController +{ + private const ENDPOINT = 'notifications/api/v1/contacts'; + + public function indexAction(): void + { + $this->assertPermission('notifications/api/v1'); + + $request = $this->getRequest(); + if (! $request->isApiRequest()) { + $this->httpBadRequest('No API request'); + } + + $method = $request->getMethod(); + if ( + in_array($method, ['POST', 'PUT']) + && ( + ! preg_match('/([^;]*);?/', $request->getHeader('Content-Type'), $matches) + || $matches[1] !== 'application/json' + ) + ) { + $this->httpBadRequest('No JSON content'); + } + + $results = []; + $responseCode = 200; + $db = Database::get(); + + /** @var ?string $identifier */ + $identifier = $request->getParam('identifier'); + + if ($identifier && ! Uuid::isValid($identifier)) { + $this->httpBadRequest('The given identifier is not a valid UUID'); + } + + $filterStr = Url::fromRequest()->getQueryString(); + if ($method !== 'GET' && $filterStr) { + $this->httpBadRequest('Filter is only allowed for GET requests'); + } + + try { + $filterRule = QueryString::fromString($filterStr) + ->on( + QueryString::ON_CONDITION, + function (Filter\Condition $condition) { + $column = $condition->getColumn(); + if (! in_array($column, ['id', 'full_name', 'username'])) { + $this->httpBadRequest(sprintf( + 'Invalid filter column %s given, only id, full_name and username are allowed', + $column + )); + } + + if ($column === 'id') { + if (! Uuid::isValid($condition->getValue())) { + $this->httpBadRequest('The given filter id is not a valid UUID'); + } + + $condition->setColumn('external_uuid'); + } + } + )->parse(); + + $filter = FilterProcessor::assembleFilter($filterRule); + } catch (Exception $e) { + $this->httpBadRequest($e->getMessage()); + } + + switch ($method) { + case 'GET': + $stmt = (new Select()) + ->distinct() + ->from('contact co') + ->columns([ + 'contact_id' => 'co.id', + 'id' => 'co.external_uuid', + 'full_name', + 'username', + 'default_channel' => 'ch.external_uuid', + ]) + ->joinLeft('contact_address ca', 'ca.contact_id = co.id') + ->joinLeft('channel ch', 'ch.id = co.default_channel_id'); + + if ($identifier !== null) { + $stmt->where(['external_uuid = ?' => $identifier]); + + /** @var stdClass|false $result */ + $result = $db->fetchOne($stmt); + + if ($result === false) { + $this->httpNotFound('Contact not found'); + } + + $result->groups = $this->fetchGroupIdentifiers($result->contact_id); + $result->addresses = $this->fetchContactAddresses($result->contact_id); + + unset($result->contact_id); + $results[] = $result; + + break; + } + + if ($filter !== null) { + $stmt->where($filter); + } + + $stmt->limit(500); + $offset = 0; + + ob_end_clean(); + Environment::raiseExecutionTime(); + + $this->getResponse() + ->setHeader('Content-Type', 'application/json') + ->setHeader('Cache-Control', 'no-store') + ->sendResponse(); + + echo '['; + + $res = $db->select($stmt->offset($offset)); + do { + /** @var stdClass $row */ + foreach ($res as $i => $row) { + $row->groups = $this->fetchGroupIdentifiers($row->contact_id); + $row->addresses = $this->fetchContactAddresses($row->contact_id); + + if ($i > 0 || $offset !== 0) { + echo ",\n"; + } + + unset($row->contact_id); + + echo Json::sanitize($row); + } + + $offset += 500; + $res = $db->select($stmt->offset($offset)); + } while ($res->rowCount()); + + echo ']'; + + exit; + case 'POST': + $data = $this->getValidatedData(); + + $db->beginTransaction(); + + if ($identifier === null) { + if ($this->getContactId($data['id']) !== null) { + throw new HttpException(422, 'Contact already exists'); + } + + $this->addContact($data); + } else { + $contactId = $this->getContactId($identifier); + if ($contactId === null) { + $this->httpNotFound('Contact not found'); + } + + if ($identifier === $data['id'] || $this->getContactId($data['id']) !== null) { + throw new HttpException(422, 'Contact already exists'); + } + + $this->removeContact($contactId); + $this->addContact($data); + } + + $db->commitTransaction(); + + $this->getResponse()->setHeader('Location', self::ENDPOINT . '/' . $data['id']); + $responseCode = 201; + + break; + case 'PUT': + if ($identifier === null) { + $this->httpBadRequest('Identifier is required'); + } + + $data = $this->getValidatedData(); + + if ($identifier !== $data['id']) { + $this->httpBadRequest('Identifier mismatch'); + } + + $db->beginTransaction(); + + $contactId = $this->getContactId($identifier); + if ($contactId !== null) { + if (! empty($data['username'])) { + $this->assertUniqueUsername($data['username'], $contactId); + } + + $db->update('contact', [ + 'full_name' => $data['full_name'], + 'username' => $data['username'] ?? null, + 'default_channel_id' => $this->getChannelId($data['default_channel']), + ], ['id = ?' => $contactId]); + + $markAsDeleted = ['deleted' => 'y']; + $db->update('contact_address', $markAsDeleted, ['contact_id = ?' => $contactId, 'deleted = ?' => 'n']); + $db->update('contactgroup_member', $markAsDeleted, ['contact_id = ?' => $contactId, 'deleted = ?' => 'n']); + + if (! empty($data['addresses'])) { + $this->addAddresses($contactId, $data['addresses']); + } + + if (! empty($data['groups'])) { + $this->addGroups($contactId, $data['groups']); + } + + $responseCode = 204; + } else { + $this->addContact($data); + $responseCode = 201; + } + + $db->commitTransaction(); + + break; + case 'DELETE': + if ($identifier === null) { + $this->httpBadRequest('Identifier is required'); + } + + $db->beginTransaction(); + + $contactId = $this->getContactId($identifier); + if ($contactId === null) { + $this->httpNotFound('Contact not found'); + } + + $this->removeContact($contactId); + + $db->commitTransaction(); + + $responseCode = 204; + + break; + default: + $this->httpBadRequest('Invalid method'); + } + + $this->getResponse() + ->setHttpResponseCode($responseCode) + ->json() + ->setSuccessData($results) + ->sendResponse(); + } + + /** + * Get the channel id with the given identifier + * + * @param string $channelIdentifier + * + * @return int + * + * @throws HttpNotFoundException if the channel does not exist + */ + private function getChannelId(string $channelIdentifier): int + { + /** @var stdClass|false $channel */ + $channel = Database::get()->fetchOne( + (new Select()) + ->from('channel') + ->columns('id') + ->where(['external_uuid = ?' => $channelIdentifier]) + ); + + if ($channel === false) { + $this->httpNotFound('Channel not found'); + } + + return $channel->id; + } + + /** + * Fetch the addresses of the contact with the given id + * + * @param int $contactId + * + * @return string + */ + private function fetchContactAddresses(int $contactId): string + { + /** @var array $addresses */ + $addresses = Database::get()->fetchPairs( + (new Select()) + ->from('contact_address') + ->columns(['type', 'address']) + ->where(['contact_id = ?' => $contactId]) + ); + + return Json::sanitize($addresses, JSON_FORCE_OBJECT); + } + + /** + * Fetch the group identifiers of the contact with the given id + * + * @param int $contactId + * + * @return string[] + */ + private function fetchGroupIdentifiers(int $contactId): array + { + return Database::get()->fetchCol( + (new Select()) + ->from('contactgroup_member cgm') + ->columns('cg.external_uuid') + ->joinLeft('contactgroup cg', 'cg.id = cgm.contactgroup_id') + ->where(['cgm.contact_id = ?' => $contactId]) + ->groupBy('cg.external_uuid') + ); + } + + /** + * Get the group id with the given identifier + * + * @param string $identifier + * + * @return int + * + * @throws HttpNotFoundException if the contactgroup with the given identifier does not exist + */ + private function getGroupId(string $identifier): int + { + /** @var stdClass|false $group */ + $group = Database::get()->fetchOne( + (new Select()) + ->from('contactgroup') + ->columns('id') + ->where(['external_uuid = ?' => $identifier]) + ); + + if ($group === false) { + $this->httpNotFound(sprintf('Group with identifier %s not found', $identifier)); + } + + return $group->id; + } + + /** + * Get the contact id with the given identifier + * + * @param string $identifier + * + * @return ?int Returns null, if contact does not exist + */ + protected function getContactId(string $identifier): ?int + { + /** @var stdClass|false $contact */ + $contact = Database::get()->fetchOne( + (new Select()) + ->from('contact') + ->columns('id') + ->where(['external_uuid = ?' => $identifier]) + ); + + return $contact->id ?? null; + } + + /** + * Add a new contact with the given data + * + * @param requestBody $data + * + * @return void + */ + private function addContact(array $data): void + { + if (! empty($data['username'])) { + $this->assertUniqueUsername($data['username']); + } + + Database::get()->insert('contact', [ + 'full_name' => $data['full_name'], + 'username' => $data['username'] ?? null, + 'default_channel_id' => $this->getChannelId($data['default_channel']), + 'external_uuid' => $data['id'] + ]); + + $contactId = Database::get()->lastInsertId(); + + if (! empty($data['addresses'])) { + $this->addAddresses($contactId, $data['addresses']); + } + + if (! empty($data['groups'])) { + $this->addGroups($contactId, $data['groups']); + } + } + + /** + * Assert that the username is unique + * + * @param string $username + * @param int $contactId The id of the contact to exclude + * + * @return void + * + * @throws HttpException if the username already exists + */ + private function assertUniqueUsername(string $username, int $contactId = null): void + { + $stmt = (new Select()) + ->from('contact') + ->columns('1') + ->where(['username = ?' => $username]); + + if ($contactId) { + $stmt->where(['id != ?' => $contactId]); + } + + $user = Database::get()->fetchOne($stmt); + + if ($user !== false) { + throw new HttpException(422, 'Username already exists'); + } + } + + /** + * Add the groups to the given contact + * + * @param int $contactId + * @param string[] $groups + * + * @return void + */ + private function addGroups(int $contactId, array $groups): void + { + foreach ($groups as $groupIdentifier) { + $groupId = $this->getGroupId($groupIdentifier); + + Database::get()->insert('contactgroup_member', [ + 'contact_id' => $contactId, + 'contactgroup_id' => $groupId + ]); + } + } + + /** + * Add the addresses to the given contact + * + * @param int $contactId + * @param array $addresses + * + * @return void + */ + private function addAddresses(int $contactId, array $addresses): void + { + foreach ($addresses as $type => $address) { + Database::get()->insert('contact_address', [ + 'contact_id' => $contactId, + 'type' => $type, + 'address' => $address + ]); + } + } + + /** + * Remove the contact with the given id + * + * @param int $id + * + * @return void + */ + private function removeContact(int $id): void + { + $db = Database::get(); + $markAsDeleted = ['deleted' => 'y']; + + $db->update( + 'rotation_member', + $markAsDeleted + ['position' => null], + ['contact_id = ?' => $id, 'deleted = ?' => 'n'] + ); + + $db->update( + 'rule_escalation_recipient', + $markAsDeleted, + ['contact_id = ?' => $id, 'deleted = ?' => 'n'] + ); + + $db->update('contactgroup_member', $markAsDeleted, ['contact_id = ?' => $id, 'deleted = ?' => 'n']); + $db->update('contact_address', $markAsDeleted, ['contact_id = ?' => $id, 'deleted = ?' => 'n']); + $db->update('contact', $markAsDeleted, ['id = ?' => $id]); + + //TODO: properly remove rotations|escalations with no members as in form + } + + /** + * Get the validated POST|PUT request data + * + * @return requestBody + * + * @throws HttpBadRequestException if the request body is invalid + */ + private function getValidatedData(): array + { + $msgPrefix = 'Invalid request body: '; + + try { + $data = $this->getRequest()->getPost(); + } catch (Exception $e) { + $this->httpBadRequest($msgPrefix . 'given content is not a valid JSON'); + } + + if ( + ! isset($data['id'], $data['full_name'], $data['default_channel']) + || ! is_string($data['id']) + || ! is_string($data['full_name']) + || ! is_string($data['default_channel']) + ) { + $this->httpBadRequest( + $msgPrefix . 'the fields id, full_name and default_channel must be present and of type string' + ); + } + + if (! Uuid::isValid($data['id'])) { + $this->httpBadRequest($msgPrefix . 'given id is not a valid UUID'); + } + + if (! Uuid::isValid($data['default_channel'])) { + $this->httpBadRequest($msgPrefix . 'given default_channel is not a valid UUID'); + } + + if (! empty($data['username']) && ! is_string($data['username'])) { + $this->httpBadRequest($msgPrefix . 'expects username to be a string'); + } + + if (! empty($data['groups'])) { + if (! is_array($data['groups'])) { + $this->httpBadRequest($msgPrefix . 'expects groups to be an array'); + } + + foreach ($data['groups'] as $group) { + if (! is_string($group) || ! Uuid::isValid($group)) { + $this->httpBadRequest($msgPrefix . 'group identifiers must be valid UUIDs'); + } + } + } + + if (! empty($data['addresses'])) { + if (! is_array($data['addresses'])) { + $this->httpBadRequest($msgPrefix . 'expects addresses to be an array'); + } + + $addressTypes = array_keys($data['addresses']); + + $types = Database::get()->fetchCol( + (new Select()) + ->from('available_channel_type') + ->columns('type') + ->where(['type IN (?)' => $addressTypes]) + ); + + if (count($types) !== count($addressTypes)) { + $this->httpBadRequest(sprintf( + $msgPrefix . 'undefined address type %s given', + implode(', ', array_diff($addressTypes, $types)) + )); + } + //TODO: is it a good idea to check valid channel types here?, if yes, + //default_channel and group identifiers must be checked here as well..404 OR 400? + + if ( + ! empty($data['addresses']['email']) + && ! (new EmailAddressValidator())->isValid($data['addresses']['email']) + ) { + $this->httpBadRequest($msgPrefix . 'an invalid email address given'); + } + } + + /** @var requestBody $data */ + return $data; + } +} diff --git a/application/forms/ChannelForm.php b/application/forms/ChannelForm.php index 7b03b7e1..e413aaec 100644 --- a/application/forms/ChannelForm.php +++ b/application/forms/ChannelForm.php @@ -20,6 +20,7 @@ use ipl\Validator\EmailAddressValidator; use ipl\Web\Common\CsrfCounterMeasure; use ipl\Web\Compat\CompatForm; +use Ramsey\Uuid\Uuid; /** * @phpstan-type ChannelOptionConfig array{ @@ -201,6 +202,7 @@ public function addChannel(): void $channel['config'] = json_encode($this->filterConfig($channel['config'])); $channel['changed_at'] = time() * 1000; + $channel['external_uuid'] = Uuid::uuid4()->toString(); $this->db->insert('channel', $channel); } diff --git a/application/forms/ContactGroupForm.php b/application/forms/ContactGroupForm.php index d7d63ed8..7a462c2c 100644 --- a/application/forms/ContactGroupForm.php +++ b/application/forms/ContactGroupForm.php @@ -22,6 +22,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 { @@ -179,7 +180,13 @@ public function addGroup(): int $this->db->beginTransaction(); $changedAt = time() * 1000; - $this->db->insert('contactgroup', ['name' => trim($data['group_name']), 'changed_at' => $changedAt]); + $this->db->insert( + 'contactgroup', + [ + 'name' => $data['group_name'], + 'external_uuid' => Uuid::uuid4()->toString() + ] + ); $groupIdentifier = $this->db->lastInsertId(); diff --git a/configuration.php b/configuration.php index 5d098aeb..c62c8ad9 100644 --- a/configuration.php +++ b/configuration.php @@ -42,6 +42,11 @@ $this->translate('Allow to configure contact groups') ); +$this->providePermission( + 'notifications/api/v1', + $this->translate('Allow to modify configuration via API') +); + $this->provideRestriction( 'notifications/filter/objects', $this->translate('Restrict access to the objects that match the filter') diff --git a/library/Notifications/Model/Channel.php b/library/Notifications/Model/Channel.php index cc4f8103..c7ad1f16 100644 --- a/library/Notifications/Model/Channel.php +++ b/library/Notifications/Model/Channel.php @@ -46,7 +46,8 @@ public function getColumns(): array 'type', 'config', 'changed_at', - 'deleted' + 'deleted', + 'external_uuid' ]; } @@ -55,7 +56,7 @@ public function getColumnDefinitions(): array return [ 'name' => t('Name'), 'type' => t('Type'), - 'changed_at' => t('Changed At') + 'external_uuid' => t('UUID') ]; } diff --git a/library/Notifications/Model/Contact.php b/library/Notifications/Model/Contact.php index 10f17b01..629aad1c 100644 --- a/library/Notifications/Model/Contact.php +++ b/library/Notifications/Model/Contact.php @@ -50,7 +50,8 @@ public function getColumns(): array 'username', 'default_channel_id', 'changed_at', - 'deleted' + 'deleted', + 'external_uuid' ]; } @@ -59,7 +60,7 @@ public function getColumnDefinitions(): array return [ 'full_name' => t('Full Name'), 'username' => t('Username'), - 'changed_at' => t('Changed At') + 'external_uuid' => t('UUID') ]; } diff --git a/library/Notifications/Model/Contactgroup.php b/library/Notifications/Model/Contactgroup.php index 3dc79481..95595cd0 100644 --- a/library/Notifications/Model/Contactgroup.php +++ b/library/Notifications/Model/Contactgroup.php @@ -42,13 +42,17 @@ public function getColumns(): array return [ 'name', 'changed_at', - 'deleted' + 'deleted', + 'external_uuid' ]; } public function getColumnDefinitions(): array { - return ['name' => t('Name')]; + return [ + 'name' => t('Name'), + 'external_uuid' => t('UUID') + ]; } public function getSearchColumns(): array diff --git a/library/Notifications/Web/Control/SearchBar/ObjectSuggestions.php b/library/Notifications/Web/Control/SearchBar/ObjectSuggestions.php index 6ca9b208..956bd378 100644 --- a/library/Notifications/Web/Control/SearchBar/ObjectSuggestions.php +++ b/library/Notifications/Web/Control/SearchBar/ObjectSuggestions.php @@ -231,7 +231,7 @@ protected function queryTags(Model $model, string $searchTerm): Query protected function matchSuggestion($path, $label, $searchTerm) { - if (preg_match('/[_.](id)$/', $path)) { + if (preg_match('/[_.](id|uuid)$/', $path)) { // Only suggest exotic columns if the user knows about them $trimmedSearch = trim($searchTerm, ' *'); return substr($path, -strlen($trimmedSearch)) === $trimmedSearch; diff --git a/library/Notifications/Web/Form/ContactForm.php b/library/Notifications/Web/Form/ContactForm.php index 246f015a..70ebfae2 100644 --- a/library/Notifications/Web/Form/ContactForm.php +++ b/library/Notifications/Web/Form/ContactForm.php @@ -21,6 +21,7 @@ use ipl\Validator\StringLengthValidator; use ipl\Web\Common\CsrfCounterMeasure; use ipl\Web\Compat\CompatForm; +use Ramsey\Uuid\Uuid; class ContactForm extends CompatForm { @@ -185,7 +186,11 @@ public function addContact(): void $contactInfo = $this->getValues(); $changedAt = time() * 1000; $this->db->beginTransaction(); - $this->db->insert('contact', $contactInfo['contact'] + ['changed_at' => $changedAt]); + $this->db->insert( + 'contact', + $contactInfo['contact'] + ['external_uuid' => Uuid::uuid4()->toString(), 'changed_at' => $changedAt] + ); + $this->contactId = $this->db->lastInsertId(); foreach (array_filter($contactInfo['contact_address']) as $type => $address) { diff --git a/phpstan-baseline-7x.neon b/phpstan-baseline-7x.neon index 5be0f381..5c9adab6 100644 --- a/phpstan-baseline-7x.neon +++ b/phpstan-baseline-7x.neon @@ -1,10 +1,5 @@ parameters: ignoreErrors: - - - message: "#^Parameter \\#2 \\$str of function explode expects string, mixed given\\.$#" - count: 2 - path: application/forms/EntryForm.php - - message: "#^Parameter \\#2 \\$str of function explode expects string, mixed given\\.$#" count: 2 diff --git a/phpstan-baseline-8x.neon b/phpstan-baseline-8x.neon index 99ed68cf..a26c7dd5 100644 --- a/phpstan-baseline-8x.neon +++ b/phpstan-baseline-8x.neon @@ -1,10 +1,5 @@ parameters: ignoreErrors: - - - message: "#^Parameter \\#2 \\$string of function explode expects string, mixed given\\.$#" - count: 2 - path: application/forms/EntryForm.php - - message: "#^Parameter \\#2 \\$string of function explode expects string, mixed given\\.$#" count: 2 diff --git a/phpstan-baseline-standard.neon b/phpstan-baseline-standard.neon index 7c04a796..2009dc0b 100644 --- a/phpstan-baseline-standard.neon +++ b/phpstan-baseline-standard.neon @@ -45,11 +45,6 @@ parameters: count: 1 path: application/controllers/ChannelsController.php - - - message: "#^Parameter \\#1 \\$name of method ipl\\\\Web\\\\Widget\\\\Tabs\\:\\:add\\(\\) expects string, mixed given\\.$#" - count: 1 - path: application/controllers/ChannelsController.php - - message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#" count: 1 @@ -60,11 +55,6 @@ parameters: count: 1 path: application/controllers/ConfigController.php - - - message: "#^Parameter \\#1 \\$name of method ipl\\\\Web\\\\Widget\\\\Tabs\\:\\:add\\(\\) expects string, mixed given\\.$#" - count: 1 - path: application/controllers/ConfigController.php - - message: "#^Cannot access property \\$full_name on ipl\\\\Orm\\\\Model\\|null\\.$#" count: 1 @@ -120,11 +110,6 @@ parameters: count: 2 path: application/controllers/EventRuleController.php - - - message: "#^Cannot access property \\$rule_escalation on ipl\\\\Orm\\\\Model\\|null\\.$#" - count: 1 - path: application/controllers/EventRuleController.php - - message: "#^Cannot access property \\$rule_escalation_recipient on mixed\\.$#" count: 1 @@ -275,161 +260,6 @@ parameters: count: 1 path: application/forms/DatabaseConfigForm.php - - - message: "#^Call to an undefined method ipl\\\\Html\\\\Contract\\\\FormElement\\:\\:isChecked\\(\\)\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Call to an undefined method ipl\\\\Html\\\\Contract\\\\FormElement\\:\\:prepareMultipartUpdate\\(\\)\\.$#" - count: 2 - path: application/forms/EntryForm.php - - - - message: "#^Call to an undefined method ipl\\\\Html\\\\Contract\\\\FormElement\\:\\:setDescription\\(\\)\\.$#" - count: 3 - path: application/forms/EntryForm.php - - - - message: "#^Call to an undefined method ipl\\\\Html\\\\Contract\\\\FormElement\\:\\:setLabel\\(\\)\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Call to an undefined method ipl\\\\Scheduler\\\\RRule\\:\\:getUntil\\(\\)\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Call to an undefined method ipl\\\\Sql\\\\Connection\\:\\:lastInsertId\\(\\)\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Cannot access offset 'frequency' on mixed\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Cannot access offset 'rrule' on mixed\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Cannot access offset 'start' on mixed\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Cannot access property \\$contact_id on mixed\\.$#" - count: 6 - path: application/forms/EntryForm.php - - - - message: "#^Cannot access property \\$contactgroup_id on mixed\\.$#" - count: 4 - path: application/forms/EntryForm.php - - - - message: "#^Cannot access property \\$full_name on mixed\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Cannot access property \\$id on mixed\\.$#" - count: 2 - path: application/forms/EntryForm.php - - - - message: "#^Cannot access property \\$name on mixed\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Cannot call method add\\(\\) on DateTime\\|false\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Cannot call method diff\\(\\) on mixed\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Cannot call method format\\(\\) on DateTime\\|false\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Cannot call method format\\(\\) on mixed\\.$#" - count: 2 - path: application/forms/EntryForm.php - - - - message: "#^Cannot call method getTimezone\\(\\) on DateTime\\|false\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Cannot cast mixed to int\\.$#" - count: 2 - path: application/forms/EntryForm.php - - - - message: "#^Cannot clone non\\-object variable \\$start of type DateTime\\|false\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Forms\\\\EntryForm\\:\\:assemble\\(\\) has no return type specified\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Forms\\\\EntryForm\\:\\:formValuesToDb\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Forms\\\\EntryForm\\:\\:getPartUpdates\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Method class@anonymous/application/forms/EntryForm\\.php\\:142\\:\\:assemble\\(\\) has no return type specified\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Method ipl\\\\Html\\\\Contract\\\\FormElement\\:\\:getValue\\(\\) invoked with 1 parameter, 0 required\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Parameter \\#1 \\$json of static method ipl\\\\Scheduler\\\\RRule\\:\\:fromJson\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Parameter \\#1 \\.\\.\\.\\$content of method ipl\\\\Html\\\\HtmlDocument\\:\\:setHtmlContent\\(\\) expects ipl\\\\Html\\\\ValidHtml, ipl\\\\Html\\\\Contract\\\\Wrappable\\|null given\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Parameter \\#2 \\$datetime of static method DateTime\\:\\:createFromFormat\\(\\) expects string, mixed given\\.$#" - count: 2 - path: application/forms/EntryForm.php - - - - message: "#^Parameter \\#2 \\$value of static method ipl\\\\Stdlib\\\\Filter\\:\\:equal\\(\\) expects array\\|bool\\|float\\|int\\|string, mixed given\\.$#" - count: 1 - path: application/forms/EntryForm.php - - - - message: "#^Property Icinga\\\\Module\\\\Notifications\\\\Forms\\\\EntryForm\\:\\:\\$submitLabel \\(string\\) on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: application/forms/EntryForm.php - - message: "#^Call to an undefined method ipl\\\\Html\\\\Contract\\\\FormElement\\:\\:setName\\(\\)\\.$#" count: 4 @@ -580,11 +410,6 @@ parameters: count: 1 path: application/forms/ScheduleForm.php - - - message: "#^Cannot access property \\$id on mixed\\.$#" - count: 3 - path: application/forms/ScheduleForm.php - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Forms\\\\ScheduleForm\\:\\:assemble\\(\\) has no return type specified\\.$#" count: 1 @@ -675,11 +500,6 @@ parameters: count: 1 path: library/Notifications/Model/ContactAddress.php - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Model\\\\Contactgroup\\:\\:createRelations\\(\\) has no return type specified\\.$#" - count: 1 - path: library/Notifications/Model/Contactgroup.php - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Model\\\\Event\\:\\:createBehaviors\\(\\) has no return type specified\\.$#" count: 1 @@ -840,11 +660,6 @@ parameters: count: 1 path: library/Notifications/Model/RuleEscalationRecipient.php - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Model\\\\ScheduleMember\\:\\:createRelations\\(\\) has no return type specified\\.$#" - count: 1 - path: library/Notifications/Model/ScheduleMember.php - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Model\\\\Source\\:\\:createRelations\\(\\) has no return type specified\\.$#" count: 1 @@ -1000,26 +815,11 @@ parameters: count: 1 path: library/Notifications/Web/Form/EventRuleDecorator.php - - - message: "#^Call to an undefined method DateTimeInterface\\:\\:add\\(\\)\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar.php - - - - message: "#^Cannot call method diff\\(\\) on DateTime\\|null\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar.php - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\:\\:assemble\\(\\) has no return type specified\\.$#" count: 1 path: library/Notifications/Widget/Calendar.php - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\:\\:getEntries\\(\\) return type has no value type specified in iterable type Traversable\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar.php - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\:\\:getModeStart\\(\\) should return DateTime but returns DateTime\\|false\\.$#" count: 2 @@ -1030,26 +830,6 @@ parameters: count: 1 path: library/Notifications/Widget/Calendar.php - - - message: "#^Parameter \\#1 \\$from of static method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\Util\\:\\:diffHours\\(\\) expects DateTime, DateTime\\|null given\\.$#" - count: 2 - path: library/Notifications/Widget/Calendar.php - - - - message: "#^Parameter \\#1 \\$start of method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\Entry\\:\\:setStart\\(\\) expects DateTime, DateTimeInterface given\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar.php - - - - message: "#^Parameter \\#1 \\$start of method ipl\\\\Scheduler\\\\RRule\\:\\:startAt\\(\\) expects DateTimeInterface, DateTime\\|null given\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar.php - - - - message: "#^Parameter \\#2 \\$to of static method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\Util\\:\\:diffHours\\(\\) expects DateTime, DateTime\\|null given\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar.php - - message: "#^Property Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\:\\:\\$addEntryUrl \\(ipl\\\\Web\\\\Url\\) does not accept ipl\\\\Web\\\\Url\\|null\\.$#" count: 1 @@ -1060,31 +840,6 @@ parameters: count: 1 path: library/Notifications/Widget/Calendar/Attendee.php - - - message: "#^Cannot call method format\\(\\) on DateTime\\|null\\.$#" - count: 7 - path: library/Notifications/Widget/Calendar/BaseGrid.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\BaseGrid\\:\\:createGridSteps\\(\\) return type has no value type specified in iterable type Traversable\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar/BaseGrid.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\BaseGrid\\:\\:getGridArea\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar/BaseGrid.php - - - - message: "#^Parameter \\#1 \\$content of static method ipl\\\\Html\\\\Text\\:\\:create\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar/BaseGrid.php - - - - message: "#^Property Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\BaseGrid\\:\\:\\$extraEntriesCount type has no value type specified in iterable type array\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar/BaseGrid.php - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\Controls\\:\\:assemble\\(\\) has no return type specified\\.$#" count: 1 @@ -1100,91 +855,16 @@ parameters: count: 1 path: library/Notifications/Widget/Calendar/DayGrid.php - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\DayGrid\\:\\:createGridSteps\\(\\) return type has no value type specified in iterable type Traversable\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar/DayGrid.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\DayGrid\\:\\:getGridArea\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar/DayGrid.php - - - - message: "#^Property Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\Entry\\:\\:\\$description has no type specified\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar/Entry.php - - - - message: "#^Property Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\Entry\\:\\:\\$end has no type specified\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar/Entry.php - - - - message: "#^Property Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\Entry\\:\\:\\$id has no type specified\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar/Entry.php - - - - message: "#^Property Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\Entry\\:\\:\\$isOccurrence has no type specified\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar/Entry.php - - - - message: "#^Property Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\Entry\\:\\:\\$rrule has no type specified\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar/Entry.php - - - - message: "#^Property Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\Entry\\:\\:\\$start has no type specified\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar/Entry.php - - - - message: "#^Property Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\Entry\\:\\:\\$url \\(ipl\\\\Web\\\\Url\\) does not accept ipl\\\\Web\\\\Url\\|null\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar/Entry.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\ExtraEntryCount\\:\\:assemble\\(\\) has no return type specified\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar/ExtraEntryCount.php - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\MonthGrid\\:\\:assemble\\(\\) has no return type specified\\.$#" count: 1 path: library/Notifications/Widget/Calendar/MonthGrid.php - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\MonthGrid\\:\\:createGridSteps\\(\\) return type has no value type specified in iterable type Traversable\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar/MonthGrid.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\MonthGrid\\:\\:getGridArea\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar/MonthGrid.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\Util\\:\\:diffHours\\(\\) has no return type specified\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar/Util.php - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\WeekGrid\\:\\:assemble\\(\\) has no return type specified\\.$#" count: 1 path: library/Notifications/Widget/Calendar/WeekGrid.php - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\WeekGrid\\:\\:createGridSteps\\(\\) return type has no value type specified in iterable type Traversable\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar/WeekGrid.php - - - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Calendar\\\\WeekGrid\\:\\:getGridArea\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: library/Notifications/Widget/Calendar/WeekGrid.php - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\CheckboxIcon\\:\\:assemble\\(\\) has no return type specified\\.$#" count: 1 @@ -1410,11 +1090,6 @@ parameters: count: 2 path: library/Notifications/Widget/RecipientSuggestions.php - - - message: "#^Cannot access property \\$color on mixed\\.$#" - count: 2 - path: library/Notifications/Widget/RecipientSuggestions.php - - message: "#^Cannot access property \\$full_name on mixed\\.$#" count: 2 @@ -1480,66 +1155,11 @@ parameters: count: 1 path: library/Notifications/Widget/RuleEscalationRecipientBadge.php - - - message: "#^Cannot access property \\$contact on mixed\\.$#" - count: 2 - path: library/Notifications/Widget/Schedule.php - - - - message: "#^Cannot access property \\$contact_id on mixed\\.$#" - count: 1 - path: library/Notifications/Widget/Schedule.php - - - - message: "#^Cannot access property \\$contactgroup on mixed\\.$#" - count: 2 - path: library/Notifications/Widget/Schedule.php - - - - message: "#^Cannot access property \\$description on mixed\\.$#" - count: 1 - path: library/Notifications/Widget/Schedule.php - - - - message: "#^Cannot access property \\$end_time on mixed\\.$#" - count: 1 - path: library/Notifications/Widget/Schedule.php - - - - message: "#^Cannot access property \\$id on mixed\\.$#" - count: 2 - path: library/Notifications/Widget/Schedule.php - - - - message: "#^Cannot access property \\$rrule on mixed\\.$#" - count: 1 - path: library/Notifications/Widget/Schedule.php - - - - message: "#^Cannot access property \\$start_time on mixed\\.$#" - count: 1 - path: library/Notifications/Widget/Schedule.php - - - - message: "#^Cannot access property \\$timeperiod_id on mixed\\.$#" - count: 1 - path: library/Notifications/Widget/Schedule.php - - - - message: "#^Cannot access property \\$timezone on mixed\\.$#" - count: 2 - path: library/Notifications/Widget/Schedule.php - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Schedule\\:\\:assemble\\(\\) has no return type specified\\.$#" count: 1 path: library/Notifications/Widget/Schedule.php - - - message: "#^Property Icinga\\\\Module\\\\Notifications\\\\Widget\\\\Schedule\\:\\:\\$schedule \\(Icinga\\\\Module\\\\Notifications\\\\Model\\\\Schedule\\) does not accept Icinga\\\\Module\\\\Notifications\\\\Model\\\\Schedule\\|null\\.$#" - count: 1 - path: library/Notifications/Widget/Schedule.php - - message: "#^Method Icinga\\\\Module\\\\Notifications\\\\Widget\\\\ShowMore\\:\\:assemble\\(\\) has no return type specified\\.$#" count: 1 diff --git a/run.php b/run.php index 9e94cef8..fd185e72 100644 --- a/run.php +++ b/run.php @@ -21,3 +21,42 @@ ] ) ); + +$this->addRoute('notifications/api-v1-contacts', new Zend_Controller_Router_Route_Regex( + 'notifications/api/v1/contacts(?:\/(.+)|\?(.+))?', + [ + 'controller' => 'api-v1-contacts', + 'action' => 'index', + 'module' => 'notifications', + 'identifier' => null + ], + [ + 1 => 'identifier' + ] +)); + +$this->addRoute('notifications/api-v1-contactgroups', new Zend_Controller_Router_Route_Regex( + 'notifications/api/v1/contactgroups(?:\/(.+)|\?(.+))?', + [ + 'controller' => 'api-v1-contactgroups', + 'action' => 'index', + 'module' => 'notifications', + 'identifier' => null + ], + [ + 1 => 'identifier' + ] +)); + +$this->addRoute('notifications/api-v1-channels', new Zend_Controller_Router_Route_Regex( + 'notifications/api/v1/channels(?:\/(.+)|\?(.+))?', + [ + 'controller' => 'api-v1-channels', + 'action' => 'index', + 'module' => 'notifications', + 'identifier' => null + ], + [ + 1 => 'identifier' + ] +));