diff --git a/application/controllers/ApiV1ContactgroupsController.php b/application/controllers/ApiV1ContactgroupsController.php new file mode 100644 index 000000000..324961c08 --- /dev/null +++ b/application/controllers/ApiV1ContactgroupsController.php @@ -0,0 +1,350 @@ +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(); + $identifier = $request->getParam('identifier'); + // TODO: Remove rawurldecode(). Only added to test, bcz phpstorm's http client encodes the params + $queryString = rawurldecode(Url::fromRequest()->getQueryString()); + $filter = FilterProcessor::assembleFilter(QueryString::parse($queryString)); + + 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]); + $result = $db->fetchOne($stmt); + + if ($result === false) { + $this->httpNotFound('Contactgroup not found'); + } + + $users = $this->fetchUserIdentifiers($result->contactgroup_id); + if ($users) { + $result->users = $users; + } + + 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 { + foreach ($res as $i => $row) { + $users = $this->fetchUserIdentifiers($row->contactgroup_id); + if ($users) { + $row->users = $users; + } + + 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 = $request->getPost(); + + $this->assertValidData($data); + + $db->beginTransaction(); + + if ($identifier === null) { + $identifier = $data['id']; + + if ($this->getContactgroupId($identifier) !== 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']) { + throw new HttpException('422', 'Contactgroup already exists'); + } + + $identifier = $data['id']; + $this->removeContactgroup($contactgroupId); + $this->addContactgroup($data); + } + + $db->commitTransaction(); + + $this->getResponse()->setHeader('Location', self::ENDPOINT . '/' . $identifier); + $responseCode = 201; + + break; + case 'PUT': + if ($identifier === null) { + $this->httpBadRequest('Identifier is required'); + } + + $data = $request->getPost(); + + $this->assertValidData($data); + + 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->delete('contactgroup_member', ['contactgroup_id = ?' => $identifier]); + + if (! empty($data['users'])) { + $this->assertUsersExist($data['users']); + + foreach ($data['users'] as $userId) { + $db->insert('contactgroup_member', [ + 'contactgroup_id' => $identifier, + 'contact_id' => $userId + ]); + } + } + + $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 + { + $users = 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') + ); + + return ! empty($users) ? $users : null; + } + + /** + * Assert that the given user IDs exist + * + * @param array $userIds + * + * @throws HttpException if a group does not exist + */ + private function assertUsersExist(array $userIds): void + { + $existingUserIds = Database::get()->fetchCol( + (new Select()) + ->from('contact') + ->columns('id') + ->where(['id IN (?)' => $userIds]) + ); + + if (count($existingUserIds) !== count($userIds)) { + throw new HttpException('404', 'Undefined user identifier given'); + } + } + + /** + * 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 + { + $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 array $data + * + * @throws HttpException if a user does not exist + */ + private function addContactgroup(array $data): void + { + $db = Database::get(); + $db->insert('contactgroup', [ + 'name' => $data['name'], + 'external_uuid' => $data['id'] + ]); + + $id = $db->lastInsertId(); + + if (! empty($data['users'])) { + $this->assertUsersExist($data['users']); + foreach ($data['users'] as $contactId) { + $db->insert('contactgroup_member', [ + 'contactgroup_id' => $id, + 'contact_id' => $contactId + ]); + } + } + } + + /** + * Remove the contactgroup with the given id + * + * @param int $id + */ + private function removeContactgroup(int $id): void + { + $db = Database::get(); + + $db->delete('contactgroup_member', ['contactgroup_id = ?' => $id]); + $db->delete('contactgroup', ['id = ?' => $id]); + } + + /** + * Assert that the given data contains the required fields + * + * @param array $data + * + * @throws HttpBadRequestException + */ + private function assertValidData(array $data): void + { + if (! isset($data['id'], $data['name'])) { + $this->httpBadRequest('missing required fields'); + } + } +} diff --git a/application/controllers/ApiV1ContactsController.php b/application/controllers/ApiV1ContactsController.php new file mode 100644 index 000000000..1ae6eed6c --- /dev/null +++ b/application/controllers/ApiV1ContactsController.php @@ -0,0 +1,441 @@ +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(); + $identifier = $request->getParam('identifier'); + // TODO: Remove rawurldecode(). Only added to test, bcz phpstorm's http client encodes the params + $queryString = rawurldecode(Url::fromRequest()->getQueryString()); + $filter = FilterProcessor::assembleFilter(QueryString::parse($queryString)); + + 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.name', + ]) + ->joinLeft('contact_address ca', 'ca.contact_id = co.id') + ->joinLeft('channel ch', 'ch.id = co.default_channel_id'); + + if ($identifier !== null) { + $stmt->where(['co.external_uuid = ?' => $identifier]); + $result = $db->fetchOne($stmt); + + if ($result === false) { + $this->httpNotFound('Contact not found'); + } + + if ($result->username === null) { + unset($result->username); + } + + $groups = $this->fetchGroupIdentifiers($result->contact_id); + if ($groups) { + $result->groups = $groups; + } + + $addresses = $this->fetchContactAddresses($result->contact_id); + if ($addresses) { + $result->addresses = $addresses; + } + + 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 { + foreach ($res as $i => $row) { + if ($row->username === null) { + unset($row->username); + } + + $groups = $this->fetchGroupIdentifiers($row->contact_id); + if ($groups) { + $row->groups = $groups; + } + + $addresses = $this->fetchContactAddresses($row->contact_id); + if ($addresses) { + $row->addresses = $addresses; + } + + 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': + if ($filter !== null) { + $this->httpBadRequest('Cannot filter on POST'); + } + + $data = $request->getPost(); + + $this->assertValidData($data); + + $db->beginTransaction(); + + if ($identifier === null) { + $identifier = $data['id']; + + if ($this->getContactId($identifier) !== 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']) { + throw new HttpException('422', 'Contact already exists'); + } + + $identifier = $data['id']; + $this->removeContact($contactId); + $this->addContact($data); + } + + $db->commitTransaction(); + + $this->getResponse()->setHeader('Location', self::ENDPOINT . '/' . $identifier); + $responseCode = 201; + + break; + case 'PUT': + if ($identifier === null) { + $this->httpBadRequest('Identifier is required'); + } + + $data = $request->getPost(); + + $this->assertValidData($data); + + if ($identifier !== $data['id']) { + $this->httpBadRequest('Identifier mismatch'); + } + + $db->beginTransaction(); + + $contactId = $this->getContactId($identifier); + if ($contactId !== null) { + $db->update('contact', [ + 'full_name' => $data['full_name'], + 'username' => $data['username'] ?? null, + 'default_channel_id' => $this->getChannelId($data['default_channel']) + ], ['id = ?' => $contactId]); + + $db->delete('contact_address', ['contact_id = ?' => $contactId]); + $db->delete('contactgroup_member', ['contact_id = ?' => $contactId]); + + if (! empty($data['addresses'])) { + foreach ($data['addresses'] as $type => $address) { + $db->insert('contact_address', [ + 'contact_id' => $contactId, + 'type' => $type, + 'address' => $address + ]); + } + } + + if (! empty($data['groups'])) { + $this->assertGroupsExist($data['groups']); + + foreach ($data['groups'] as $group) { + $db->insert('contactgroup_member', [ + 'contact_id' => $contactId, + 'contactgroup_id' => $group + ]); + } + } + + $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(); + } + + /** + * @param string $channelName + * + * @return int + * @throws HttpException if the channel does not exist + */ + private function getChannelId(string $channelName): int + { + $channel = Database::get()->fetchOne( + (new Select()) + ->from('channel') + ->columns('id') + ->where(['name = ?' => $channelName]) + ); + + if ($channel === false) { + throw new HttpException('404', '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 + { + $addresses = Database::get()->fetchPairs( + (new Select()) + ->from('contact_address') + ->columns(['type', 'address']) + ->where(['contact_id = ?' => $contactId]) + ); + + return ! empty($addresses) ? json_encode($addresses) : null; + } + + /** + * Fetch the group identifiers of the contact with the given id + * + * @param int $contactId + * + * @return ?string[] + */ + private function fetchGroupIdentifiers(int $contactId): ?array + { + $groups = 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') + ); + + return ! empty($groups) ? $groups : null; + } + + /** + * Assert that the given group IDs exist + * + * @param array $groupIds + * + * @throws HttpException 404 if a group does not exist + */ + private function assertGroupsExist(array $groupIds): void + { + $existingGroupIds = Database::get()->fetchCol( + (new Select()) + ->from('contactgroup') + ->columns('id') + ->where(['id IN (?)' => $groupIds]) + ); + + if (count($existingGroupIds) !== count($groupIds)) { + throw new HttpException('404', 'Undefined group identifier given'); + } + } + + /** + * 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 + { + $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 array $data + * + * @throws HttpException if a group or default_channel does not exist + */ + private function addContact(array $data): void + { + $db = Database::get(); + + $db->insert('contact', [ + 'full_name' => $data['full_name'], + 'username' => $data['username'] ?? null, + 'default_channel_id' => $this->getChannelId($data['default_channel']), + 'external_uuid' => $data['id'] + ]); + + $contactId = $db->lastInsertId(); + + if (! empty($data['addresses'])) { + foreach ($data['addresses'] as $type => $address) { + $db->insert('contact_address', [ + 'contact_id' => $contactId, + 'type' => $type, + 'address' => $address + ]); + } + } + + if (! empty($data['groups'])) { + $this->assertGroupsExist($data['groups']); + + foreach ($data['groups'] as $groupId) { + $db->insert('contactgroup_member', [ + 'contact_id' => $contactId, + 'contactgroup_id' => $groupId + ]); + } + } + } + + /** + * Remove the contact with the given id + * + * @param int $id + */ + private function removeContact(int $id): void + { + $db = Database::get(); + + $db->delete('contactgroup_member', ['contact_id = ?' => $id]); + $db->delete('contact_address', ['contact_id = ?' =>$id]); + $db->delete('contact', ['id = ?' => $id]); + } + + /** + * Assert that the given data contains the required fields + * + * @param array $data + * + * @throws HttpBadRequestException + */ + private function assertValidData(array $data): void + { + if (! isset($data['id'], $data['full_name'], $data['default_channel'])) { + $this->httpBadRequest('missing required fields'); + } + } +}