Skip to content

Commit 0aee9c5

Browse files
authored
Merge pull request #459 from jjrom/develop
Validate update catalog
2 parents f77457d + 729ac5c commit 0aee9c5

File tree

6 files changed

+148
-28
lines changed

6 files changed

+148
-28
lines changed

app/resto/core/api/STACAPI.php

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -487,20 +487,52 @@ public function updateCatalog($params, $body)
487487

488488
// Get catalogs and childs
489489
$catalogs = $this->catalogsFunctions->getCatalogs(array(
490-
'id' => join('/', $params['segments'])
490+
'id' => join('/', $params['segments']),
491+
'noCount' => true
491492
), $this->context->core['baseUrl'], true);
492493

493494
if ( count($catalogs) === 0 ){
494495
RestoLogUtil::httpError(404);
495496
}
496497

497-
498498
// If user has not the right to update catalog then 403
499499
if ( !$this->user->hasRightsTo(RestoUser::UPDATE_CATALOG, array('catalog' => $catalogs[0])) ) {
500500
return RestoLogUtil::httpError(403);
501501
}
502+
503+
// Update is not forced so we should check that input links array don't remove existing childs
504+
// [IMPORTANT] if no links object is in the body then only other properties are updated and existing links are not destroyed
505+
if ( array_key_exists('links', $body) && !filter_var($params['_force'] ?? false, FILTER_VALIDATE_BOOLEAN) ) {
506+
$levelUp = array();
507+
for ($i = 0, $ii = count($catalogs); $i < $ii; $i++) {
508+
if ($catalogs[$i]['level'] !== $catalogs[0]['level'] + 1) {
509+
continue;
510+
}
511+
$levelUp[$catalogs[$i]['id']] = false;
512+
for ($j = 0, $jj = count($body['links']); $j < $jj; $j++) {
513+
if ( !str_starts_with($body['links'][$j]['href'], $this->context->core['baseUrl'] . RestoRouter::ROUTE_TO_CATALOGS ) ) {
514+
continue;
515+
}
516+
if ($catalogs[$i]['id'] === substr($body['links'][$j]['href'], strlen($this->context->core['baseUrl'] . RestoRouter::ROUTE_TO_CATALOGS) + 1)) {
517+
$levelUp[$catalogs[$i]['id']] = true;
518+
break;
519+
}
520+
}
521+
}
522+
$removed = 0;
523+
foreach (array_keys($levelUp) as $key) {
524+
if ( $levelUp[$key] === false ) {
525+
$removed++;
526+
}
527+
}
528+
529+
if ($removed > 0) {
530+
return RestoLogUtil::httpError(400, 'The catalog update would remove ' . $removed . ' existing child(s). Set **_force** query parameter to true to force update anyway');
531+
}
532+
}
502533

503-
$updatable = array('title', 'description', 'owner', 'links');
534+
535+
$updatable = array('title', 'description', 'owner', 'links', 'visibility');
504536
for ($i = count($updatable); $i--;) {
505537
if ( isset($body[$updatable[$i]]) ) {
506538
$catalogs[0][$updatable[$i]] = $body[$updatable[$i]];
@@ -576,17 +608,24 @@ public function removeCatalog($params)
576608

577609
// Get catalogs and childs
578610
$catalogs = $this->catalogsFunctions->getCatalogs(array(
579-
'id' => join('/', $params['segments'])
611+
'id' => join('/', $params['segments']),
612+
'noCount' => true
580613
), $this->context->core['baseUrl'], true);
581614

582-
if ( count($catalogs) === 0 ){
615+
$count = count($catalogs);
616+
if ( $count === 0 ){
583617
RestoLogUtil::httpError(404);
584618
}
585619

586620
// If user has not the right to delete catalog then 403
587621
if ( !$this->user->hasRightsTo(RestoUser::DELETE_CATALOG, array('catalog' => $catalogs[0])) ) {
588622
return RestoLogUtil::httpError(403);
589623
}
624+
625+
// If catalogs has child, do not remove it unless _force option is set to true
626+
if ( $count > 1 && !filter_var($params['_force'] ?? false, FILTER_VALIDATE_BOOLEAN) ){
627+
return RestoLogUtil::httpError(400, 'The catalog contains ' . ($count - 1) . ' child(s) and cannot be deleted. Set **_force** query parameter to true to force deletion anyway');
628+
}
590629

591630
return RestoLogUtil::success('Catalog deleted', $this->catalogsFunctions->removeCatalog($catalogs[0]['id']));
592631

app/resto/core/dbfunctions/CatalogsFunctions.php

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ public function updateCatalog($catalog, $userid, $context)
262262
$values = array(
263263
$catalog['id']
264264
);
265-
265+
266266
$canBeUpdated = array(
267267
'title',
268268
'owner',
@@ -273,7 +273,10 @@ public function updateCatalog($catalog, $userid, $context)
273273

274274
$set = array();
275275
$cleanLinks = $this->getCleanLinks($catalog, $userid, $context);
276-
$catalog['links'] = $cleanLinks['links'];
276+
277+
if ( array_key_exists('links', $cleanLinks) ) {
278+
$catalog['links'] = $cleanLinks['links'];
279+
}
277280

278281
foreach (array_keys($catalog) as $key ) {
279282
if (in_array($key, $canBeUpdated)) {
@@ -292,16 +295,38 @@ public function updateCatalog($catalog, $userid, $context)
292295
$this->dbDriver->query('BEGIN');
293296

294297
/*
295-
* Delete all catalog childs BUT NOT HIMSELF
298+
* Delete catalog childs BUT NOT HIMSELF and the one in childIds
296299
*/
297-
$this->dbDriver->fetch($this->dbDriver->pQuery('DELETE FROM ' . $this->dbDriver->targetSchema . '.catalog WHERE lower(id) LIKE lower($1) RETURNING id', array(
298-
$catalog['id'] . '/%'
299-
), 500, 'Cannot delete catalog ' . $catalog['id']));
300-
$this->dbDriver->fetch($this->dbDriver->pQuery('DELETE FROM ' . $this->dbDriver->targetSchema . '.catalog_feature WHERE path ~ $1 AND path <> $2' , array(
301-
RestoUtil::path2ltree($catalog['id']) . '.*',
302-
RestoUtil::path2ltree($catalog['id'])
303-
), 500, 'Cannot delete catalog_feature association for catalog ' . $catalog['id']));
304-
300+
if ( array_key_exists('links', $cleanLinks) ) {
301+
302+
// No childIds => easy !
303+
if ( empty($cleanLinks['childIds']) ) {
304+
$this->dbDriver->fetch($this->dbDriver->pQuery('DELETE FROM ' . $this->dbDriver->targetSchema . '.catalog WHERE lower(id) LIKE lower($1) RETURNING id', array(
305+
$catalog['id'] . '/%'
306+
), 500, 'Cannot update catalog ' . $catalog['id']));
307+
$this->dbDriver->fetch($this->dbDriver->pQuery('DELETE FROM ' . $this->dbDriver->targetSchema . '.catalog_feature WHERE path ~ $1 AND path <> $2' , array(
308+
RestoUtil::path2ltree($catalog['id']) . '.*',
309+
RestoUtil::path2ltree($catalog['id'])
310+
), 500, 'Cannot update catalog_feature association for catalog ' . $catalog['id']));
311+
}
312+
else {
313+
$lowerIds = array();
314+
$paths = array('\'' . RestoUtil::path2ltree($catalog['id']) . '\'');
315+
for ($i = 0, $ii = count($cleanLinks['childIds']); $i < $ii; $i++) {
316+
$lowerIds[] = 'lower(\'' . pg_escape_string($this->dbDriver->getConnection(), $cleanLinks['childIds'][$i]) . '\')';
317+
$paths[] = '\'' . RestoUtil::path2ltree($cleanLinks['childIds'][$i]) . '\'';
318+
}
319+
$this->dbDriver->fetch($this->dbDriver->pQuery('DELETE FROM ' . $this->dbDriver->targetSchema . '.catalog WHERE lower(id) LIKE lower($1) AND lower(id) NOT IN (' . join(',', $lowerIds) . ') RETURNING id', array(
320+
$catalog['id'] . '/%'
321+
), 500, 'Cannot update catalog ' . $catalog['id']));
322+
$this->dbDriver->fetch($this->dbDriver->pQuery('DELETE FROM ' . $this->dbDriver->targetSchema . '.catalog_feature WHERE path ~ $1 AND path NOT IN (' . join(',', $paths) . ')' , array(
323+
RestoUtil::path2ltree($catalog['id']) . '.*'
324+
), 500, 'Cannot update catalog_feature association for catalog ' . $catalog['id']));
325+
326+
}
327+
328+
}
329+
305330
/*
306331
* Then update catalog
307332
*/
@@ -310,9 +335,11 @@ public function updateCatalog($catalog, $userid, $context)
310335
/*
311336
* Add an entry in catalog_feature for each interalItems but first remove all items !
312337
*/
313-
$this->removeCatalogFeatures($catalog['id']);
314-
$this->addInternalItems($cleanLinks['internalItems'], $catalog['id']);
315-
338+
if ( array_key_exists('links', $cleanLinks) ) {
339+
$this->removeCatalogFeatures($catalog['id']);
340+
$this->addInternalItems($cleanLinks['internalItems'], $catalog['id']);
341+
}
342+
316343
$this->dbDriver->query('COMMIT');
317344

318345
} catch (Exception $e) {
@@ -688,13 +715,20 @@ private function getCleanLinks($catalog, $userid, $context) {
688715

689716
$output = array(
690717
'links' => array(),
718+
'childIds' => array(),
691719
'internalItems' => array()
692720
);
693721

694-
if ( !isset($catalog['links']) ) {
695-
return $output;
722+
if ( !array_key_exists('links', $catalog) ) {
723+
return array(
724+
'internalitems' => array()
725+
);
696726
};
697727

728+
if ( empty($catalog['links']) ) {
729+
return $output;
730+
};
731+
698732
for ($i = 0, $ii = count($catalog['links']); $i < $ii; $i++) {
699733
$link = $catalog['links'][$i];
700734
if ( !isset($link['rel']) || in_array($link['rel'], array('root', 'parent', 'self')) ) {
@@ -747,10 +781,15 @@ private function getCleanLinks($catalog, $userid, $context) {
747781
* Avoid cycling (i.e. catalog self referencing one of its parent)
748782
*/
749783
if (str_starts_with($link['href'], $context->core['baseUrl'] . RestoRouter::ROUTE_TO_CATALOGS )) {
750-
$exploded = explode('/', substr($link['href'], strlen($context->core['baseUrl'] . RestoRouter::ROUTE_TO_CATALOGS) + 1));
784+
$childId = substr($link['href'], strlen($context->core['baseUrl'] . RestoRouter::ROUTE_TO_CATALOGS) + 1);
785+
$exploded = explode('/', $childId);
751786
if ( count($exploded) <= count(explode('/', $catalog['id'])) ) {
752787
return RestoLogUtil::httpError(400, 'Child ' . $link['href'] . ' is invalid because it references a parent resource');
753788
}
789+
// Keep track of child ids for delete before update
790+
else {
791+
$output['childIds'][] = $childId;
792+
}
754793
}
755794

756795
/*
@@ -772,9 +811,7 @@ private function getCleanLinks($catalog, $userid, $context) {
772811
if ( $childCatalog === null ) {
773812
return RestoLogUtil::httpError(400, 'Catalog child ' . $link['href'] . ' does not exist');
774813
}
775-
776-
$output['links'][] = $link;
777-
814+
778815
}
779816

780817
}

docs/COLLECTIONS_AND_CATALOGS.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ is to create an empty catalog then add its childs through the POST API
6969
# The catalog dummyCatalogCycling is posted under /catalogs/dummyCatalogChild1 but reference one of this
7070
# parent as a child which is forbiden
7171
curl -X POST -d@examples/catalogs/dummyCatalogChild1.json "http://admin:admin@localhost:5252/catalogs/dummyCatalog"
72+
curl -X POST -d@examples/catalogs/dummyCatalogChildOfChild1.json "http://admin:admin@localhost:5252/catalogs/dummyCatalog/dummyCatalogChild1"
7273
curl -X POST -d@examples/catalogs/dummyCatalogCycling.json "http://admin:admin@localhost:5252/catalogs/dummyCatalog/dummyCatalogChild1"
7374

7475
### Create a catalog with item
@@ -89,7 +90,25 @@ is to create an empty catalog then add its childs through the POST API
8990

9091
curl -X PUT -d@examples/catalogs/dummyCatalogWithItem_update.json "http://admin:admin@localhost:5252/catalogs/dummyCatalog/dummyCatalogChild1"
9192

93+
### Update a catalog that has already childs
94+
95+
# First post 2 catalogs under dummyCatalog
96+
curl -X POST -d@examples/catalogs/dummyCatalogChild1.json "http://admin:admin@localhost:5252/catalogs/dummyCatalog"
97+
curl -X POST -d@examples/catalogs/dummyCatalogChild2.json "http://admin:admin@localhost:5252/catalogs/dummyCatalog"
98+
99+
# Update dummyCatalog removing one catalog in the links will return an HTTP error because it would remove existing child
100+
curl -X PUT -d@examples/catalogs/dummyCatalog_update.json "http://admin:admin@localhost:5252/catalogs/dummyCatalog"
101+
102+
# Use _force flag to force links update
103+
104+
105+
92106
### Delete a catalog
93107

94108
# Delete a catalog - user with the "deleteCatalog" right can delete a catalog he owns
95-
curl -X DELETE "http://admin:admin@localhost:5252/catalogs/dummyCatalog"
109+
# This will return an HTTP error because the catalog contains child and removing it would remove childs
110+
curl -X DELETE "http://admin:admin@localhost:5252/catalogs/dummyCatalog"
111+
112+
# Use _force flag to force deletion
113+
curl -X DELETE "http://admin:admin@localhost:5252/catalogs/dummyCatalog?_force=1"
114+

examples/catalogs/dummyCatalogChild1.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,11 @@
44
"title": "Dummy Catalog child 1",
55
"description":"I'm a child of dummyCatalog",
66
"stac_version":"1.0.0",
7-
"links":[]
7+
"links":[
8+
{
9+
"rel":"item",
10+
"href":"http://127.0.0.1:5252/collections/DummyCollection/items/DummySargasse",
11+
"title":"Catalog can also have item"
12+
}
13+
]
814
}

examples/catalogs/dummyCatalogChild2.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,11 @@
44
"title": "Dummy Catalog child 2",
55
"description":"I'm a child of dummyCatalog",
66
"stac_version":"1.0.0",
7-
"links":[]
7+
"links":[
8+
{
9+
"rel":"item",
10+
"href":"http://127.0.0.1:5252/collections/DummyCollection/items/DummySargasse",
11+
"title":"Catalog can also have item"
12+
}
13+
]
814
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"id": "dummyCatalog",
3+
"title": "Dummy Catalog",
4+
"type": "Catalog",
5+
"description":"This is an update of dummyCatalog with one link removed",
6+
"stac_version":"1.0.0",
7+
"links":[
8+
{
9+
"rel":"child",
10+
"href":"http://127.0.0.1:5252/catalogs/dummyCatalog/dummyCatalogChild1"
11+
}
12+
]
13+
}

0 commit comments

Comments
 (0)