From 2ef6cdfa4bd09fedb21549fe879f14d695b94ed3 Mon Sep 17 00:00:00 2001 From: Lukas Metzger Date: Thu, 29 Mar 2018 15:16:54 +0200 Subject: [PATCH] Added GET /records --- backend/src/controllers/Records.php | 44 +++++++++ backend/src/operations/Records.php | 130 ++++++++++++++++++++++++ backend/src/public/index.php | 2 + backend/src/services/Database.php | 28 +++++- backend/test/db.sql | 5 +- backend/test/tests/records-get.js | 148 ++++++++++++++++++++++++++++ 6 files changed, 355 insertions(+), 2 deletions(-) create mode 100644 backend/src/controllers/Records.php create mode 100644 backend/src/operations/Records.php create mode 100644 backend/test/tests/records-get.js diff --git a/backend/src/controllers/Records.php b/backend/src/controllers/Records.php new file mode 100644 index 0000000..34e8000 --- /dev/null +++ b/backend/src/controllers/Records.php @@ -0,0 +1,44 @@ +logger = $c->logger; + $this->c = $c; + } + + public function getList(Request $req, Response $res, array $args) + { + $records = new \Operations\Records($this->c); + + $paging = new \Utils\PagingInfo($req->getQueryParam('page'), $req->getQueryParam('pagesize')); + $domain = $req->getQueryParam('domain'); + $queryName = $req->getQueryParam('queryName'); + $type = $req->getQueryParam('type'); + $queryContent = $req->getQueryParam('queryContent'); + $sort = $req->getQueryParam('sort'); + + $userId = $req->getAttribute('userId'); + + $results = $records->getRecords($paging, $userId, $domain, $queryName, $type, $queryContent, $sort); + + return $res->withJson([ + 'paging' => $paging->toArray(), + 'results' => $results + ], 200); + } +} diff --git a/backend/src/operations/Records.php b/backend/src/operations/Records.php new file mode 100644 index 0000000..1dbd2d9 --- /dev/null +++ b/backend/src/operations/Records.php @@ -0,0 +1,130 @@ +logger = $c->logger; + $this->db = $c->db; + $this->c = $c; + } + + /** + * Get a list of domains according to filter criteria + * + * @param $pi PageInfo object, which is also updated with total page number + * @param $userId Id of the user for which the table should be retrieved + * @param $domain Comma separated list of domain ids + * @param $queryName Search query to search in the record name, null for no filter + * @param $type Comma separated list of types + * @param $queryContent Search query to search in the record content, null for no filter + * @param $sort Sort string in format 'field-asc,field2-desc', null for default + * + * @return array Array with matching records + */ + public function getRecords( + \Utils\PagingInfo &$pi, + int $userId, + ? string $domain, + ? string $queryName, + ? string $type, + ? string $queryContent, + ? string $sort + ) : array { + $ac = new \Operations\AccessControl($this->c); + $userIsAdmin = $ac->isAdmin($userId); + + $queryName = $queryName === null ? '%' : '%' . $queryName . '%'; + $queryContent = $queryContent === null ? '%' : '%' . $queryContent . '%'; + + $setDomains = \Services\Database::makeSetString($this->db, $domain); + $setTypes = \Services\Database::makeSetString($this->db, $type); + + //Count elements + if ($pi->pageSize === null) { + $pi->totalPages = 1; + } else { + $query = $this->db->prepare(' + SELECT COUNT(*) AS total FROM records R + LEFT OUTER JOIN domains D ON R.domain_id = D.id + LEFT OUTER JOIN permissions P ON P.domain_id = R.domain_id + WHERE (P.user_id=:userId OR :userIsAdmin) AND + (R.domain_id IN ' . $setDomains . ' OR :noDomainFilter) AND + (R.name LIKE :queryName) AND + (R.type IN ' . $setTypes . ' OR :noTypeFilter) AND + (R.content LIKE :queryContent) + '); + + $query->bindValue(':userId', $userId, \PDO::PARAM_INT); + $query->bindValue(':userIsAdmin', intval($userIsAdmin), \PDO::PARAM_INT); + $query->bindValue(':queryName', $queryName, \PDO::PARAM_STR); + $query->bindValue(':queryContent', $queryContent, \PDO::PARAM_STR); + $query->bindValue(':noDomainFilter', intval($domain === null), \PDO::PARAM_INT); + $query->bindValue(':noTypeFilter', intval($type === null), \PDO::PARAM_INT); + + $query->execute(); + $record = $query->fetch(); + + $pi->totalPages = ceil($record['total'] / $pi->pageSize); + } + + //Query and return result + $ordStr = \Services\Database::makeSortingString($sort, [ + 'id' => 'R.id', + 'name' => 'R.name', + 'type' => 'R.type', + 'content' => 'R.content', + 'priority' => 'R.prio', + 'ttl' => 'R.ttl' + ]); + $pageStr = \Services\Database::makePagingString($pi); + + $query = $this->db->prepare(' + SELECT R.id,R.name,R.type,R.content,R.prio as priority,R.ttl,R.domain_id as domain FROM records R + LEFT OUTER JOIN domains D ON R.domain_id = D.id + LEFT OUTER JOIN permissions P ON P.domain_id = R.domain_id + WHERE (P.user_id=:userId OR :userIsAdmin) AND + (R.domain_id IN ' . $setDomains . ' OR :noDomainFilter) AND + (R.name LIKE :queryName) AND + (R.type IN ' . $setTypes . ' OR :noTypeFilter) AND + (R.content LIKE :queryContent) + GROUP BY R.id' . $ordStr . $pageStr); + + $query->bindValue(':userId', $userId, \PDO::PARAM_INT); + $query->bindValue(':userIsAdmin', intval($userIsAdmin), \PDO::PARAM_INT); + $query->bindValue(':queryName', $queryName, \PDO::PARAM_STR); + $query->bindValue(':queryContent', $queryContent, \PDO::PARAM_STR); + $query->bindValue(':noDomainFilter', intval($domain === null), \PDO::PARAM_INT); + $query->bindValue(':noTypeFilter', intval($type === null), \PDO::PARAM_INT); + + $query->execute(); + + $data = $query->fetchAll(); + + return array_map(function ($item) { + $item['id'] = intval($item['id']); + $item['priority'] = intval($item['priority']); + $item['ttl'] = intval($item['ttl']); + $item['domain'] = intval($item['domain']); + return $item; + }, $data); + } +} diff --git a/backend/src/public/index.php b/backend/src/public/index.php index e2132ac..d276b12 100644 --- a/backend/src/public/index.php +++ b/backend/src/public/index.php @@ -35,6 +35,8 @@ $app->group('/v1', function () { $this->put('/domains/{domainId}/soa', '\Controllers\Domains:putSoa'); $this->get('/domains/{domainId}/soa', '\Controllers\Domains:getSoa'); + + $this->get('/records', '\Controllers\Records:getList'); })->add('\Middlewares\Authentication'); }); diff --git a/backend/src/services/Database.php b/backend/src/services/Database.php index ac4f758..66eb043 100644 --- a/backend/src/services/Database.php +++ b/backend/src/services/Database.php @@ -69,7 +69,7 @@ class Database * * @return string SQL string to use */ - public static function makeSortingString(? string $sort, array $colMap) + public static function makeSortingString(? string $sort, array $colMap) : string { if ($sort === null) { return ''; @@ -95,4 +95,30 @@ class Database return ' ORDER BY ' . implode(', ', $orderStrings); } + + /** + * Makes a string which works to use with an IN SQL clause. + * + * Input is a comma separated list, all items are escaped and joint + * to the form ('a','b') + * + * @param $db PDO object used for escaping + * @param $input Comma separated list of items + * + * @return string SQL string to use + */ + public function makeSetString(\PDO $db, ? string $input) : string + { + if ($input === null || $input === '') { + return '(\'\')'; + } + + $parts = explode(',', $input); + + $partsEscaped = array_map(function ($item) use ($db) { + return $db->quote($item, \PDO::PARAM_STR); + }, $parts); + + return '(' . implode(', ', $partsEscaped) . ')'; + } } diff --git a/backend/test/db.sql b/backend/test/db.sql index cf6bd40..941c3ca 100644 --- a/backend/test/db.sql +++ b/backend/test/db.sql @@ -137,7 +137,10 @@ CREATE TABLE `records` ( -- INSERT INTO `records` (`id`, `domain_id`, `name`, `type`, `content`, `ttl`, `prio`, `change_date`, `disabled`, `ordername`, `auth`) VALUES -(1, 1, 'test.example.com', 'A', '12.34.56.78', 86400, 0, 1521645110, 0, NULL, 1); +(1, 1, 'test.example.com', 'A', '12.34.56.78', 86400, 0, 1521645110, 0, NULL, 1), +(2, 1, 'sdfdf.example.com', 'TXT', 'foo bar baz', 60, 10, 1522321931, 0, NULL, 1), +(3, 1, 'foo.example.com', 'AAAA', '::1', 86400, 0, 1522321902, 0, NULL, 1), +(4, 3, 'foo.de', 'A', '9.8.7.6', 86400, 0, 1522321989, 0, NULL, 1); -- -------------------------------------------------------- diff --git a/backend/test/tests/records-get.js b/backend/test/tests/records-get.js new file mode 100644 index 0000000..8d7bb97 --- /dev/null +++ b/backend/test/tests/records-get.js @@ -0,0 +1,148 @@ +const test = require('../testlib'); +const cartesianProduct = require('cartesian-product'); + +test.run(async function () { + await test('admin', async function (assert, req) { + //Test sorting in all combinations + const sortCombinations = cartesianProduct([ + ['', 'id-asc', 'id-desc'], + ['', 'name-asc', 'name-desc'], + ['', 'type-asc', 'type-desc'], + ['', 'content-asc', 'content-desc'], + ['', 'priority-asc', 'priority-desc'], + ['', 'ttl-asc', 'ttl-desc'], + ]); + + for (list of sortCombinations) { + list = list.filter((str) => str.length > 0); + var sortQuery = list.join(','); + + var res = await req({ + url: '/records?sort=' + sortQuery, + method: 'get' + }); + + assert.equal(res.status, 200); + + var sortedData = res.data.results.slice(); + sortedData.sort(function (a, b) { + for (sort of list) { + var spec = sort.split('-'); + if (a[spec[0]] < b[spec[0]]) { + return spec[1] == 'asc' ? -1 : 1; + } else if (a[spec[0]] > b[spec[0]]) { + return spec[1] == 'asc' ? 1 : -1; + } + } + return 0; + }); + + assert.equal(res.data.results, sortedData, 'Sort failed for ' + res.config.url); + } + + //Test paging + var res = await req({ + url: '/records?pagesize=2', + method: 'get' + }); + + assert.equal(res.status, 200, 'Status should be OK'); + assert.equal(res.data.paging, { + page: 1, + total: 2, + pagesize: 2 + }, 'Paging data fail for ' + res.config.url); + assert.equal(res.data.results.length, 2, "Should be 2 results."); + + var res = await req({ + url: '/records?pagesize=2&page=2', + method: 'get' + }); + + assert.equal(res.status, 200, 'Status should be OK'); + assert.equal(res.data.paging, { + page: 2, + total: 2, + pagesize: 2 + }, 'Paging data fail for ' + res.config.url); + assert.equal(res.data.results.length, 2, "Should be 2 results."); + + //Test query name + var res = await req({ + url: '/records?queryName=foo&sort=id-asc', + method: 'get' + }); + + assert.equal(res.status, 200, 'Status should be OK'); + assert.equal(res.data.results, [{ + id: 3, + name: 'foo.example.com', + type: 'AAAA', + content: '::1', + priority: 0, + ttl: 86400, + domain: 1 + }, + { + id: 4, + name: 'foo.de', + type: 'A', + content: '9.8.7.6', + priority: 0, + ttl: 86400, + domain: 3 + }], 'Result fail for ' + res.config.url); + + //Type filter + var res = await req({ + url: '/records?type=TXT,AAAA', + method: 'get' + }); + + assert.equal(res.status, 200, 'Status should be OK'); + assert.equal(res.data.results, [{ + id: 2, + name: 'sdfdf.example.com', + type: 'TXT', + content: 'foo bar baz', + priority: 10, + ttl: 60, + domain: 1 + }, + { + id: 3, + name: 'foo.example.com', + type: 'AAAA', + content: '::1', + priority: 0, + ttl: 86400, + domain: 1 + }], 'Result fail for ' + res.config.url); + + //Test query content + var res = await req({ + url: '/records?queryContent=6&sort=id-asc', + method: 'get' + }); + + assert.equal(res.status, 200, 'Status should be OK'); + assert.equal(res.data.results, [{ + id: 1, + name: 'test.example.com', + type: 'A', + content: '12.34.56.78', + priority: 0, + ttl: 86400, + domain: 1 + }, + { + id: 4, + name: 'foo.de', + type: 'A', + content: '9.8.7.6', + priority: 0, + ttl: 86400, + domain: 3 + }], 'Result fail for ' + res.config.url); + }); +}); \ No newline at end of file