From e61212e9316aa6b60929e7508a000faa4b1af526 Mon Sep 17 00:00:00 2001 From: jjrom Date: Fri, 29 Nov 2024 14:26:36 +0100 Subject: [PATCH] [MAJOR] Remove osdescription - needs database update --- README.md | 2 +- app/resto/core/Resto.php | 19 +- app/resto/core/RestoCollection.php | 180 ++------- app/resto/core/RestoCollections.php | 27 +- app/resto/core/RestoContext.php | 22 -- app/resto/core/RestoDatabaseDriver.php | 4 +- app/resto/core/RestoFeatureCollection.php | 14 +- app/resto/core/RestoModel.php | 10 +- app/resto/core/RestoRouter.php | 6 +- app/resto/core/api/AuthAPI.php | 16 +- app/resto/core/api/CollectionsAPI.php | 42 +- app/resto/core/api/FeaturesAPI.php | 12 +- app/resto/core/api/RightsAPI.php | 8 +- app/resto/core/api/STACAPI.php | 44 +-- app/resto/core/api/ServicesAPI.php | 88 +---- app/resto/core/api/UsersAPI.php | 8 +- .../core/dbfunctions/CatalogsFunctions.php | 21 +- .../core/dbfunctions/CollectionsFunctions.php | 361 ++++++------------ .../core/dbfunctions/FeaturesFunctions.php | 12 +- .../core/dbfunctions/GroupsFunctions.php | 2 +- app/resto/core/dbfunctions/UsersFunctions.php | 4 +- app/resto/core/utils/Curly.php | 2 +- app/resto/core/utils/RestoGeometryUtil.php | 2 +- app/resto/core/utils/SecurityUtil.php | 2 +- app/resto/core/xml/ATOMFeed.php | 17 +- app/resto/core/xml/OSDD.php | 345 ----------------- build/resto/config.php.template | 12 - config-dev.env | 19 +- config.env | 17 +- docs/COLLECTIONS_AND_CATALOGS.md | 21 +- docs/MIGRATION_TO_9.1.0.md | 20 + examples/collections/DummyCollection.json | 12 - examples/collections/DummyCollection2.json | 18 +- .../collections/DummyCollection_update.json | 21 + examples/collections/L8.json | 29 +- examples/collections/L8_nosummaries.json | 29 +- examples/collections/S2.json | 33 +- .../03_resto_target_model.sql | 49 +-- 38 files changed, 343 insertions(+), 1207 deletions(-) delete mode 100755 app/resto/core/xml/OSDD.php create mode 100644 docs/MIGRATION_TO_9.1.0.md create mode 100644 examples/collections/DummyCollection_update.json diff --git a/README.md b/README.md index a40ab5de..2a37b34a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ resto is a metadata catalog and a search engine dedicated to geospatialized data. Originally, it’s main purpose it to handle Earth Observation satellite imagery but it can be used to store any kind of metadata localized in time and space. -resto search API conforms to the [SpatioTemporal Asset Catalog (STAC) specification v1.0.0](https://github.com/radiantearth/stac-spec) and to the [CEOS OpenSearch Best Practice Document](http://ceos.org/ourwork/workinggroups/wgiss/access/opensearch/). +resto search API conforms to the [SpatioTemporal Asset Catalog (STAC) specification v1.0.0](https://github.com/radiantearth/stac-spec) It is mentioned in ESA's "Exploitation Platform Common Core Components" as the closest implementation of a catalogue component according to the requirements specified in ESA's ["Exploitation Platform Open Architecture"](https://tep.eo.esa.int/news/-/blogs/exploitation-platforms-open-architecture-released) diff --git a/app/resto/core/Resto.php b/app/resto/core/Resto.php index ec4a1fdc..c2d3bc99 100755 --- a/app/resto/core/Resto.php +++ b/app/resto/core/Resto.php @@ -65,15 +65,12 @@ * * @OA\OpenApi( * @OA\Info( - * title=API_INFO_TITLE, - * description=API_INFO_DESCRIPTION, - * version=RESTO_VERSION, - * @OA\Contact( - * email=API_INFO_CONTACT_EMAIL - * ) + * title=STAC_ROOT_TITLE, + * description=STAC_ROOT_DESCRIPTION, + * version=RESTO_VERSION * ), * @OA\Server( - * description=API_HOST_DESCRIPTION, + * description=STAC_ROOT_DESCRIPTION, * url=PUBLIC_ENDPOINT * ) * ) @@ -190,7 +187,7 @@ private function getResponse() return $this->setCORSHeaders(); default: - return RestoLogUtil::httpError(404); + RestoLogUtil::httpError(404); } $response = $this->router->process($method, $this->context->path, $this->context->query); @@ -261,7 +258,7 @@ private function format($object) * Case 0 - Object is null */ if (!isset($object)) { - return RestoLogUtil::httpError(400, 'Empty object'); + RestoLogUtil::httpError(400, 'Empty object'); } $pretty = isset($this->context->query['_pretty']) ? filter_var($this->context->query['_pretty'], FILTER_VALIDATE_BOOLEAN) : false; @@ -283,7 +280,7 @@ private function format($object) if (method_exists(get_class($object), $methodName)) { return $outputFormat === 'json' ? $object->$methodName($pretty) : $object->$methodName(); } - return RestoLogUtil::httpError(404); + RestoLogUtil::httpError(404); } return $object; @@ -291,7 +288,7 @@ private function format($object) /* * Unknown stuff */ - return RestoLogUtil::httpError(400, 'Invalid object'); + RestoLogUtil::httpError(400, 'Invalid object'); } /** diff --git a/app/resto/core/RestoCollection.php b/app/resto/core/RestoCollection.php index aa5bb232..ea566ccf 100755 --- a/app/resto/core/RestoCollection.php +++ b/app/resto/core/RestoCollection.php @@ -85,21 +85,6 @@ * ) * ), * @OA\Property( - * property="osDescription", - * type="object", - * required={"en"}, - * @OA\Property( - * property="en", - * description="OpenSearch description in English", - * ref="#/components/schemas/OpenSearchDescription" - * ), - * @OA\Property( - * property="fr", - * description="OpenSearch description in French", - * ref="#/components/schemas/OpenSearchDescription" - * ) - * ), - * @OA\Property( * property="links", * type="array", * @OA\Items(ref="#/components/schemas/Link") @@ -135,6 +120,9 @@ * ), * example={ * "id": "S2", + * "type": "Collection", + * "title": "Level 1C Sentinel-2 images", + * "description": "The SENTINEL-2 mission is a land monitoring constellation of two satellites each equipped with a MSI (Multispectral Imager) instrument covering 13 spectral bands providing high resolution optical imagery (i.e., 10m, 20m, 60 m) every 10 days with one satellite and 5 days with two satellites", * "version": "1.0", * "model": "OpticalModel", * "rights": { @@ -143,28 +131,6 @@ * }, * "visibility": 1, * "license": "proprietary", - * "osDescription": { - * "en": { - * "ShortName": "Sentinel-2", - * "LongName": "Level 1C Sentinel-2 images", - * "Description": "The SENTINEL-2 mission is a land monitoring constellation of two satellites each equipped with a MSI (Multispectral Imager) instrument covering 13 spectral bands providing high resolution optical imagery (i.e., 10m, 20m, 60 m) every 10 days with one satellite and 5 days with two satellites", - * "Tags": "copernicus esa eu msi radiance sentinel sentinel2", - * "Developer": "Jérôme Gasperi", - * "Contact": "jrom@snapplanet.io", - * "Query": "Toulouse", - * "Attribution": "European Union/ESA/Copernicus" - * }, - * "fr": { - * "ShortName": "Sentinel-2", - * "LongName": "Images Sentinel-2 Niveau 1C", - * "Description": "La mission SENTINEL-2 est constituée de deux satellites d'imagerie optique équipés d’un imageur multispectral (MSI) en 13 bandes spectrales avec des résolutions de 10, 20 et 60 mètres et d'une fauchée unique de 290 km de large. La capacité d'observation des deux satellites permet de surveiller l'intégralité des terres émergées du globe tous les 5 jours", - * "Tags": "copernicus esa eu msi radiance sentinel sentinel2", - * "Developer": "Jérôme Gasperi", - * "Contact": "jrom@snapplanet.io", - * "Query": "Toulouse", - * "Attribution": "European Union/ESA/Copernicus" - * } - * }, * "providers": { * { * "name": "European Union/ESA/Copernicus", @@ -263,7 +229,7 @@ * * @OA\Schema( * schema="OutputCollection", - * required={"id", "type", "title", "description", "license", "extent", "links"}, + * required={"id", "type", "description", "license", "extent", "links"}, * @OA\Property( * property="id", * type="string", @@ -336,11 +302,6 @@ * ) * ), * @OA\Property( - * property="osDescription", - * type="object", - * ref="#/components/schemas/OpenSearchDescription" - * ), - * @OA\Property( * property="owner", * type="string", * description="Collection owner (i.e. resto user identifier as bigint)" @@ -450,16 +411,6 @@ * "SatelliteModel", * "OpticalModel" * }, - * "osDescription": { - * "ShortName": "Sentinel-2", - * "LongName": "Level 1C Sentinel-2 images", - * "Description": "The SENTINEL-2 mission is a land monitoring constellation of two satellites each equipped with a MSI (Multispectral Imager) instrument covering 13 spectral bands providing high resolution optical imagery (i.e., 10m, 20m, 60 m) every 10 days with one satellite and 5 days with two satellites", - * "Tags": "copernicus esa eu msi radiance sentinel sentinel2", - * "Developer": "J\u00e9r\u00f4me Gasperi", - * "Contact": "jrom@snapplanet.io", - * "Query": "Toulouse", - * "Attribution": "European Union/ESA/Copernicus" - * }, * "owner": "203883411255198721" * }, * "providers": { @@ -578,6 +529,16 @@ class RestoCollection */ public $id = null; + /* + * Collection title + */ + public $title = null; + + /* + * Collection description + */ + public $description = null; + /* * Data model for this collection */ @@ -605,7 +566,7 @@ class RestoCollection public $aliases = array(); public $visibility = RestoConstants::GROUP_DEFAULT_ID; public $version = '1.0.0'; - public $license = 'proprietary'; + public $license = 'other'; public $links = array(); public $providers = array(); public $rights = array(); @@ -629,68 +590,6 @@ class RestoCollection */ public $owner; - /** - * - * Array of OpenSearch Description parameters per lang - * - * @OA\Schema( - * schema="OpenSearchDescription", - * description="OpenSearch description of the search engine attached to the collection", - * required={"ShortName", "Description"}, - * @OA\Property( - * property="ShortName", - * type="string", - * description="Contains a brief human-readable title that identifies the search engine" - * ), - * @OA\Property( - * property="LongName", - * type="string", - * description="Contains an extended human-readable title that identifies this search engine" - * ), - * @OA\Property( - * property="Description", - * type="string", - * description="Contains a human-readable text description of the collection search engine" - * ), - * @OA\Property( - * property="Tags", - * type="string", - * description="Contains a set of words that are used as keywords to identify and categorize this search content. Tags must be a single word and are delimited by the space character" - * ), - * @OA\Property( - * property="Developer", - * type="string", - * description="Contains the human-readable name or identifier of the creator or maintainer of the description document" - * ), - * @OA\Property( - * property="Contact", - * type="string", - * description="Contains an email address at which the maintainer of the description document can be reached" - * ), - * @OA\Property( - * property="Query", - * type="string", - * description="Defines a search query that can be performed by search clients. Please see the OpenSearch Query element specification for more information" - * ), - * @OA\Property( - * property="Attribution", - * type="string", - * description="Contains a list of all sources or entities that should be credited for the content contained in the search feed" - * ), - * example={ - * "ShortName": "S2", - * "LongName": "Sentinel-2", - * "Description": "Sentinel-2 tiles", - * "Tags": "s2 sentinel2", - * "Developer": "Jérôme Gasperi", - * "Contact": "jrom@snapplanet.io", - * "Query": "Toulouse", - * "Attribution": "SnapPlanet - Copyright 2016, All Rights Reserved" - * } - * ) - */ - public $osDescription = null; - /** * Summaries */ @@ -751,7 +650,6 @@ class RestoCollection 'license', 'links', 'model', - 'osDescription', 'providers', 'rights', 'stac_extension', @@ -798,9 +696,9 @@ public function load($object = null, $modelName = null) if ( !$this->isLoaded ) { $this->isLoaded = true; - $collectionObject = (new CollectionsFunctions($this->context->dbDriver))->getCollectionDescription($this->id); + $collectionObject = (new CollectionsFunctions($this->context->dbDriver))->getCollection($this->id); if (! isset($collectionObject)) { - return RestoLogUtil::httpError(404); + RestoLogUtil::httpError(404); } foreach ($collectionObject as $key => $value) { @@ -829,13 +727,13 @@ public function store() * Update collection - collection must be load first ! * * @param array $object - * @return This object + * @return RestoCollection */ public function update($object) { // It means that collection is not loaded - so cannot be updated if (! isset($this->model)) { - return RestoLogUtil::httpError(400, 'Model does not exist'); + RestoLogUtil::httpError(400, 'Model does not exist'); } $this->loadFromJSON($object); @@ -847,7 +745,7 @@ public function update($object) * Search features within collection * * @param array $query - * @return array (FeatureCollection) + * @return RestoFeatureCollection */ public function search($query) { @@ -918,16 +816,14 @@ public function setSummaries($summaries) public function toArray() { - $osDescription = $this->osDescription[$this->context->lang] ?? $this->osDescription['en']; - $collectionArray = array( 'stac_version' => STACAPI::STAC_VERSION, 'stac_extensions' => $this->model->stacExtensions, 'id' => $this->id, 'type' => 'Collection', - 'title' => $osDescription['LongName'] ?? $osDescription['ShortName'], + 'title' => $this->title, + 'description' => $this->description, 'version' => $this->version ?? null, - 'description' => $osDescription['Description'], 'aliases' => $this->aliases ?? array(), 'license' => $this->license, 'extent' => $this->extent, @@ -960,7 +856,6 @@ public function toArray() 'resto:info' => array( 'model' => $this->model->getName(), 'lineage' => $this->model->getLineage(), - 'osDescription' => $this->osDescription[$this->context->lang] ?? $this->osDescription['en'], 'owner' => $this->owner, 'visibility' => $this->visibility ) @@ -1006,14 +901,6 @@ public function toJSON($pretty = false) return json_encode($this->toArray(), $pretty ? JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES : JSON_UNESCAPED_SLASHES); } - /** - * Output collection description as an XML OpenSearch document - */ - public function getOSDD() - { - return new OSDD($this->context, $this->model, $this->getSummaries(), $this); - } - /** * On which planet this collection applied * Based on ssys:target @@ -1046,18 +933,6 @@ private function loadFromJSON($object, $modelName = null) * Check mandatory properties are required */ $this->checkCreationMandatoryProperties($object, $modelName); - - /* - * If OpenSearch Description object is not set, create a minimal one from $object['description'] - */ - if (!isset($object['osDescription']) || !is_array($object['osDescription']) || !isset($object['osDescription']['en']) || !is_array($object['osDescription']['en'])) { - $object['osDescription'] = array( - 'en' => array( - 'ShortName' => $object['title'] ?? $object['id'], - 'Description' => $object['description'] ?? '' - ) - ); - } /* * Default collection visibility is the value of RestoConstants::GROUP_DEFAULT_ID @@ -1068,7 +943,7 @@ private function loadFromJSON($object, $modelName = null) /* * Set values */ - foreach (array_values(array('aliases', 'version', 'license', 'links', 'osDescription', 'providers', 'rights', 'assets', 'keywords', 'extent')) as $key) { + foreach (array_values(array('title', 'description', 'aliases', 'version', 'license', 'links', 'providers', 'rights', 'assets', 'keywords', 'extent')) as $key) { if (isset($object[$key])) { $this->$key = $key === 'links' ? $this->cleanInputLinks($object['links']) : $object[$key]; } @@ -1112,6 +987,13 @@ private function checkCreationMandatoryProperties($object, $modelName) if ( !isset($object['type']) || $object['type'] !== 'Collection') { RestoLogUtil::httpError(400, 'Property "type" is mandatory and must be set to *Collection*'); } + + /* + * description is mandatory + */ + if ( !isset($object['description']) ) { + RestoLogUtil::httpError(400, 'Property "description" is mandatory'); + } /* * Set DefaultModel if not set - preseance to input $modelName @@ -1126,7 +1008,7 @@ private function checkCreationMandatoryProperties($object, $modelName) if (!class_exists($object['model']) || !is_subclass_of($object['model'], 'RestoModel')) { RestoLogUtil::httpError(400, 'Model "' . $object['model'] . '" is not a valid model name'); } - + /* * Set collection model */ diff --git a/app/resto/core/RestoCollections.php b/app/resto/core/RestoCollections.php index 065e3d87..0bfed3e5 100755 --- a/app/resto/core/RestoCollections.php +++ b/app/resto/core/RestoCollections.php @@ -144,8 +144,6 @@ public function __construct($context, $user) $this->context = $context; $this->user = $user; - - return $this; } /** @@ -186,14 +184,14 @@ public function create($object, $modelName) * * @param RestoModel $model * @param array $query - * @return array (FeatureCollection) + * @return RestoFeatureCollection */ public function search($model, $query) { /* * Set a global model with all searchFilters and all tables from other collection */ - $model = $model ?? $this->getFullModel(); + $model ??= $this->getFullModel(); return (new RestoFeatureCollection($this->context, $this->user, $this->collections, $model, $query))->load(null); } @@ -209,7 +207,7 @@ public function load($params = array()) if ( !$this->isLoaded ) { $params['group'] = $this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID) ? null : $this->user->getGroupIds(); - $collectionsDesc = (new CollectionsFunctions($this->context->dbDriver))->getCollectionsDescriptions($params); + $collectionsDesc = (new CollectionsFunctions($this->context->dbDriver))->getCollections($params); foreach (array_keys($collectionsDesc) as $collectionId) { $collection = $this->context->keeper->getRestoCollection($collectionId, $this->user); @@ -240,11 +238,10 @@ public function toArray() { $collections = array( 'stac_version' => STACAPI::STAC_VERSION, - 'id' => $this->context->osDescription['ShortName'], + 'id' => 'collections', 'type' => 'Catalog', 'title' => 'Collections', - 'description' => $this->context->osDescription['Description'], - 'keywords' => explode(' ', $this->context->osDescription['Tags']), + 'description' => 'All collections', 'links' => array( array( 'rel' => 'self', @@ -264,9 +261,6 @@ public function toArray() ) ), 'extent' => $this->extent, - 'resto:info' => array( - 'osDescription' => $this->context->osDescription - ), 'collections' => array() ); @@ -334,15 +328,6 @@ public function toJSON($pretty = false) return json_encode($this->toArray(), $pretty ? JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES : JSON_UNESCAPED_SLASHES); } - /** - * Output collections description as an XML OpenSearch document - */ - public function getOSDD($model) - { - $model = $model ?? new DefaultModel(); - return new OSDD($this->context, $model, $this->filterSummaries($this->getSummaries(), $model->getAutoFacetFields()), null); - } - /** * Return STAC summaries */ @@ -386,7 +371,7 @@ private function updateExtent($collection) /** * Return an array of all available searchFilters on all collections * - * @return array + * @return RestoModel */ private function getFullModel() { diff --git a/app/resto/core/RestoContext.php b/app/resto/core/RestoContext.php index 186a7d4f..2b98f9f3 100755 --- a/app/resto/core/RestoContext.php +++ b/app/resto/core/RestoContext.php @@ -129,11 +129,6 @@ class RestoContext */ public $method = 'GET'; - /** - * Default osDescription - */ - public $osDescription = array(); - /** * Default servicesInfos */ @@ -197,11 +192,6 @@ public function __construct($config) */ $this->setLang(filter_input(INPUT_GET, 'lang', FILTER_UNSAFE_RAW)); - /* - * Set osDescription - */ - $this->setOsDescription($config); - /* * Initialize addons */ @@ -472,18 +462,6 @@ private function setLang($lang) $this->lang = $lang; } - /** - * Set osDescription - * - * @param array $config - */ - private function setOsDescription($config) - { - if (isset($config['osDescriptions'])) { - $this->osDescription = $config['osDescriptions'][$this->lang] ?? $config['osDescriptions']['en']; - } - } - /** * Set public config from configuration input * diff --git a/app/resto/core/RestoDatabaseDriver.php b/app/resto/core/RestoDatabaseDriver.php index c0c81035..9c8caf92 100755 --- a/app/resto/core/RestoDatabaseDriver.php +++ b/app/resto/core/RestoDatabaseDriver.php @@ -154,7 +154,7 @@ public function pQuery($query, $params, $errorCode = 500, $errorMessage = null) * Convert database query result into array * * @param PgSql\Result $results - * @return Array + * @return array */ public function fetch($results) { @@ -176,7 +176,7 @@ public function getConnection() $this->dbh = $this->getConnectionFromConfig($this->config); if (!$this->dbh) { - return RestoLogUtil::httpError(500, 'Cannot connect to database ' . ($this->config['dbname'] ?? '???') . '@' . ($this->config['host'] ?? '???') . ':' . ($this->config['port'] ?? '???')); + RestoLogUtil::httpError(500, 'Cannot connect to database ' . ($this->config['dbname'] ?? '???') . '@' . ($this->config['host'] ?? '???') . ':' . ($this->config['port'] ?? '???')); } return $this->dbh; diff --git a/app/resto/core/RestoFeatureCollection.php b/app/resto/core/RestoFeatureCollection.php index 764619df..5fc62d8c 100755 --- a/app/resto/core/RestoFeatureCollection.php +++ b/app/resto/core/RestoFeatureCollection.php @@ -186,11 +186,6 @@ * "href": "http://127.0.0.1:5252/stac/search.json?" * }, * { - * "rel": "search", - * "type": "application/opensearchdescription+xml", - * "href": "http://127.0.0.1:5252/services/osdd" - * }, - * { * "rel": "next", * "type": "application/json", * "href": "http://127.0.0.1:5252/stac/search.json?next=204449069316703379" @@ -392,7 +387,7 @@ class RestoFeatureCollection /** * Constructor * - * @param RestoResto $context : Resto Context + * @param RestoContext $context : Resto Context * @param RestoUser $user : Resto user * @param array $collections * @param RestoModel $model : Base model @@ -823,11 +818,6 @@ private function getBaseLinks($collectionId) 'type' => RestoUtil::$contentTypes['geojson'], 'href' => RestoUtil::updateUrl($this->context->getUrl(false), $this->writeRequestParams($this->query)) ), - array( - 'rel' => 'search', - 'type' => 'application/opensearchdescription+xml', - 'href' => $this->context->core['baseUrl'] . '/services/osdd' . (isset($collectionId) ? '/' . $collectionId : '') - ), array( 'rel' => 'root', 'type' => RestoUtil::$contentTypes['json'], @@ -1005,7 +995,7 @@ private function getSorting($filters) * Finally check validity */ if (! in_array($sortKey, $this->context->dbDriver->sortKeys)) { - return RestoLogUtil::httpError(400, "Invalid sorting key"); + RestoLogUtil::httpError(400, "Invalid sorting key"); } /* diff --git a/app/resto/core/RestoModel.php b/app/resto/core/RestoModel.php index f585396d..8169f38e 100755 --- a/app/resto/core/RestoModel.php +++ b/app/resto/core/RestoModel.php @@ -425,7 +425,7 @@ public function storeFeatures($collection, $body, $params) $data = $this->inputToResto($body, $collection, $params); if (!isset($data) || !in_array($data['type'], array('Feature', 'FeatureCollection'))) { - return RestoLogUtil::httpError(400, 'Invalid input type - only "Feature" and "FeatureCollection" are allowed'); + RestoLogUtil::httpError(400, 'Invalid input type - only "Feature" and "FeatureCollection" are allowed'); } // Extent @@ -868,7 +868,7 @@ private function storeFeature($collection, $data, $params) * Input feature cannot have both an id and a productIdentifier */ if (isset($data['id']) && isset($data['properties']['productIdentifier']) && $data['id'] !== $data['properties']['productIdentifier']) { - return RestoLogUtil::httpError(400, 'Invalid input feature - found both "id" and "properties.productIdentifier"'); + RestoLogUtil::httpError(400, 'Invalid input feature - found both "id" and "properties.productIdentifier"'); } $productIdentifier = $data['id'] ?? $data['properties']['productIdentifier'] ?? null; @@ -887,7 +887,7 @@ private function storeFeature($collection, $data, $params) * (do this before getKeywords to avoid iTag process) */ if (isset($productIdentifier) && (new FeaturesFunctions($collection->context->dbDriver))->featureExists($featureId, $collection->context->dbDriver->targetSchema . '.feature')) { - return RestoLogUtil::httpError(409, 'Feature ' . $featureId . ' (with productIdentifier=' . $productIdentifier . ') already in database'); + RestoLogUtil::httpError(409, 'Feature ' . $featureId . ' (with productIdentifier=' . $productIdentifier . ') already in database'); } /* @@ -1043,11 +1043,11 @@ private function validateFilterString($filterKey, $value) $elements = array_map('trim', explode(',', $value)); for ($i = count($elements); $i--;) { if (preg_match('\'' . $this->searchFilters[$filterKey]['pattern'] . '\'', $elements[$i]) !== 1) { - return RestoLogUtil::httpError(400, 'Comma separated list of "' . $this->searchFilters[$filterKey]['osKey'] . '" must follow the pattern ' . $this->searchFilters[$filterKey]['pattern']); + RestoLogUtil::httpError(400, 'Comma separated list of "' . $this->searchFilters[$filterKey]['osKey'] . '" must follow the pattern ' . $this->searchFilters[$filterKey]['pattern']); } } } elseif (preg_match('\'' . $this->searchFilters[$filterKey]['pattern'] . '\'', $value) !== 1) { - return RestoLogUtil::httpError(400, 'Value for "' . $this->searchFilters[$filterKey]['osKey'] . '" must follow the pattern ' . $this->searchFilters[$filterKey]['pattern']); + RestoLogUtil::httpError(400, 'Value for "' . $this->searchFilters[$filterKey]['osKey'] . '" must follow the pattern ' . $this->searchFilters[$filterKey]['pattern']); } return true; diff --git a/app/resto/core/RestoRouter.php b/app/resto/core/RestoRouter.php index 9a397c09..76ddea0f 100755 --- a/app/resto/core/RestoRouter.php +++ b/app/resto/core/RestoRouter.php @@ -35,7 +35,7 @@ class RestoRouter const ROUTE_TO_FEATURE = RestoRouter::ROUTE_TO_FEATURES . '/{featureId}'; const ROUTE_TO_FORGOT_PASSWORD = '/services/password/forgot'; const ROUTE_TO_GROUPS = '/groups'; - const ROUTE_TO_OSDD = '/services/osdd'; + const ROUTE_TO_LIVENESS = '/_isLive'; const ROUTE_TO_RESET_PASSWORD = '/services/password/reset'; const ROUTE_TO_SEND_ACTIVATION_LINK = '/services/activation/send'; @@ -105,8 +105,6 @@ class RestoRouter array('PUT', RestoRouter::ROUTE_TO_AUTH . '/activate/{token}', false, 'AuthAPI::activateUser'), // Activate owner of the token // API for services - array('GET', RestoRouter::ROUTE_TO_OSDD, false, 'ServicesAPI::getOSDD'), // Opensearch service description at collections level - array('GET', RestoRouter::ROUTE_TO_OSDD . '/{collectionId}', false, 'ServicesAPI::getOSDDForCollection'), // Opensearch service description for products on {collection} array('POST', RestoRouter::ROUTE_TO_SEND_ACTIVATION_LINK, false, 'ServicesAPI::sendActivationLink'), // Send activation link array('POST', RestoRouter::ROUTE_TO_FORGOT_PASSWORD, false, 'ServicesAPI::forgotPassword'), // Send reset password link array('POST', RestoRouter::ROUTE_TO_RESET_PASSWORD, false, 'ServicesAPI::resetPassword'), // Reset password @@ -331,7 +329,7 @@ private function instantiateRoute($validRoute, $method, $params) * Authentication is required */ if (isset($validRoute[0]['auth']) && $validRoute[0]['auth'] && !isset($this->user->profile['id'])) { - return RestoLogUtil::httpError(401); + RestoLogUtil::httpError(401); } /* diff --git a/app/resto/core/api/AuthAPI.php b/app/resto/core/api/AuthAPI.php index 94d59942..02ca5097 100755 --- a/app/resto/core/api/AuthAPI.php +++ b/app/resto/core/api/AuthAPI.php @@ -94,7 +94,7 @@ public function getToken() // User not activated if ($this->user->profile['activated'] !== 1) { - return RestoLogUtil::httpError(412, 'User not activated'); + RestoLogUtil::httpError(412, 'User not activated'); } return array( @@ -183,11 +183,11 @@ public function createToken($params) // A token can be only be created by admin if ( !$this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID) ) { - return RestoLogUtil::httpError(403); + RestoLogUtil::httpError(403); } if ( isset($params['duration']) && !ctype_digit($params['duration']) ) { - return RestoLogUtil::httpError(400, 'Parameters duration must be a valid integer in days'); + RestoLogUtil::httpError(400, 'Parameters duration must be a valid integer in days'); } if ( !isset($params['duration']) ) { @@ -195,7 +195,7 @@ public function createToken($params) } if ( !isset($params['userId']) || !ctype_digit($params['userId']) ) { - return RestoLogUtil::httpError(400, 'Mandatory userId is not set or not valid'); + RestoLogUtil::httpError(400, 'Mandatory userId is not set or not valid'); } $days = isset($params['duration']) ? (integer) $params['duration'] : round($this->context->core['tokenDuration'] / 86400); @@ -272,13 +272,13 @@ public function revokeToken($params) { $payload = $this->context->decodeJWT($params['token']); if (!isset($payload)) { - return RestoLogUtil::httpError(400, 'Invalid or expired token'); + RestoLogUtil::httpError(400, 'Invalid or expired token'); } // A token can be only be revoked by admin or by its owner if ($this->user->profile['id'] !== $payload['sub']) { if (!$this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID)) { - return RestoLogUtil::httpError(403); + RestoLogUtil::httpError(403); } } @@ -415,13 +415,13 @@ public function activateUser($params) $payload = $this->context->decodeJWT($params['token']); if (!isset($payload) || !isset($payload['sub'])) { - return RestoLogUtil::httpError(400, 'Invalid or expired token'); + RestoLogUtil::httpError(400, 'Invalid or expired token'); } $user = new RestoUser(array('id' => $payload['sub']), $this->context, true); if (!isset($user->profile['id']) || !$user->activate()) { - return RestoLogUtil::httpError(500, 'User not activated'); + RestoLogUtil::httpError(500, 'User not activated'); } return array( diff --git a/app/resto/core/api/CollectionsAPI.php b/app/resto/core/api/CollectionsAPI.php index f37042c5..ddbe06aa 100755 --- a/app/resto/core/api/CollectionsAPI.php +++ b/app/resto/core/api/CollectionsAPI.php @@ -52,7 +52,7 @@ public function __construct($context, $user) * ), * @OA\Response( * response="200", - * description="List of all collection descriptions", + * description="List of all collections", * @OA\JsonContent( * @OA\Property( * property="extent", @@ -60,18 +60,6 @@ public function __construct($context, $user) * ref="#/components/schemas/Extent" * ), * @OA\Property( - * property="resto:info", - * type="object", - * description="resto additional information", - * @OA\JsonContent( - * @OA\Property( - * property="osDescription", - * type="object", - * ref="#/components/schemas/OpenSearchDescription" - * ) - * ) - * ), - * @OA\Property( * property="collections", * description="List of available collections", * type="array", @@ -102,18 +90,6 @@ public function __construct($context, $user) * "trs": "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian" * } * }, - * "resto:info": { - * "osDescription": { - * "ShortName": "resto", - * "LongName": "resto search service", - * "Description": "Search on all collections", - * "Tags": "resto", - * "Developer": "J\u00e9r\u00f4me Gasperi", - * "Contact": "jerome.gasperi@gmail.com", - * "Query": "europe 2015", - * "Attribution": "Copyright 2018, All Rights Reserved" - * } - * }, * "collections": { * { * "id": "L8", @@ -167,16 +143,6 @@ public function __construct($context, $user) * "SatelliteModel", * "OpticalModel" * }, - * "osDescription": { - * "ShortName": "Landsat-8", - * "LongName": "Images Landsat-8 niveau 1C", - * "Description": "Landsat represents the world's longest continuously acquired collection of space-based moderate-resolution land remote sensing data. Four decades of imagery provides a unique resource for those who work in agriculture, geology, forestry, regional planning, education, mapping, and global change research. Landsat images are also invaluable for emergency response and disaster relief", - * "Tags": "landsat level1C USGS", - * "Developer": "J\u00e9r\u00f4me Gasperi", - * "Contact": "jrom@snapplanet.io", - * "Query": "USA 2019", - * "Attribution": "USGS/NASA Landsat" - * }, * "owner": "203883411255198721" * }, * "summaries": { @@ -225,7 +191,7 @@ public function getCollections($params) * @OA\Get( * path="/collections/{collectionId}", * summary="Get collection", - * description="Returns collection description including statistics (i.e. number of products, etc.)", + * description="Returns collection including statistics (i.e. number of products, etc.)", * tags={"Collection"}, * @OA\Parameter( * name="collectionId", @@ -238,7 +204,7 @@ public function getCollections($params) * ), * @OA\Response( * response="200", - * description="Collection description", + * description="Collection", * @OA\JsonContent(ref="#/components/schemas/OutputCollection") * ), * @OA\Response( @@ -651,7 +617,7 @@ public function insertFeatures($params, $body) * This should not happen */ if ($result === false) { - return RestoLogUtil::httpError(500, 'Cannot insert feature in database'); + RestoLogUtil::httpError(500, 'Cannot insert feature in database'); } return RestoLogUtil::success('Inserted features', $result); diff --git a/app/resto/core/api/FeaturesAPI.php b/app/resto/core/api/FeaturesAPI.php index 6fe7b52d..dbc6bacd 100755 --- a/app/resto/core/api/FeaturesAPI.php +++ b/app/resto/core/api/FeaturesAPI.php @@ -575,20 +575,20 @@ public function getFeaturesInCollection($params) // This should return HTTP 400 but we discard it instead otherwise it brokes pystac requests if (isset($params['collections'])) { unset($params['collections']); - //return RestoLogUtil::httpError(400, 'You cannot specify a list of collections on a single collection search'); + //RestoLogUtil::httpError(400, 'You cannot specify a list of collections on a single collection search'); } if (isset($params['ck'])) { - return RestoLogUtil::httpError(400, 'You cannot filter on collections keywords on a single collection search'); + RestoLogUtil::httpError(400, 'You cannot filter on collections keywords on a single collection search'); } if (isset($params['model'])) { - return RestoLogUtil::httpError(400, 'You cannot specify a collection and a model at the same time'); + RestoLogUtil::httpError(400, 'You cannot specify a collection and a model at the same time'); } // [STAC] Only one of either intersects or bbox should be specified. If both are specified, a 400 Bad Request response should be returned. if (isset($params['intersects']) && isset($params['bbox'])) { - return RestoLogUtil::httpError(400, 'Only one of either intersects or bbox should be specified'); + RestoLogUtil::httpError(400, 'Only one of either intersects or bbox should be specified'); } // Set Content-Type to GeoJSON @@ -814,12 +814,12 @@ public function updateFeatureProperty($params, $body) // A value key is mandatory if (! array_key_exists('value', $body)) { - return RestoLogUtil::httpError(400, 'Missing mandatory "value" property'); + RestoLogUtil::httpError(400, 'Missing mandatory "value" property'); } // Only these properties can be updated if (! in_array($params['property'], array('title', 'description', 'visibility', 'owner', 'status'))) { - return RestoLogUtil::httpError(400, 'Invalid property "' . $params['property'] . '"'); + RestoLogUtil::httpError(400, 'Invalid property "' . $params['property'] . '"'); } // Only admin can change owner property diff --git a/app/resto/core/api/RightsAPI.php b/app/resto/core/api/RightsAPI.php index bb36b446..d1a6959f 100755 --- a/app/resto/core/api/RightsAPI.php +++ b/app/resto/core/api/RightsAPI.php @@ -244,11 +244,11 @@ public function setUserRights($params, $body) * [SECURITY] Only admin can set user rights */ if ( !$this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID) ) { - return RestoLogUtil::httpError(403); + RestoLogUtil::httpError(403); } if ( empty($body) ) { - return RestoLogUtil::httpError(400, 'No rights to set'); + RestoLogUtil::httpError(400, 'No rights to set'); } // Get user just to be sure that it exists ! @@ -342,11 +342,11 @@ public function setGroupRights($params, $body) * [SECURITY] Only admin can set group rights */ if ( !$this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID) ) { - return RestoLogUtil::httpError(403); + RestoLogUtil::httpError(403); } if ( empty($body) ) { - return RestoLogUtil::httpError(400, 'No rights to set'); + RestoLogUtil::httpError(400, 'No rights to set'); } // Get group just to be sure that it exists ! diff --git a/app/resto/core/api/STACAPI.php b/app/resto/core/api/STACAPI.php index 855cc937..495b73c9 100644 --- a/app/resto/core/api/STACAPI.php +++ b/app/resto/core/api/STACAPI.php @@ -369,20 +369,20 @@ public function addCatalog($params, $body) { if (!$this->user->hasRightsTo(RestoUser::CREATE_CATALOG)) { - return RestoLogUtil::httpError(403); + RestoLogUtil::httpError(403); } /* * Check mandatory properties */ if ( !isset($body['id']) ) { - return RestoLogUtil::httpError(400, 'Missing mandatory catalog id'); + RestoLogUtil::httpError(400, 'Missing mandatory catalog id'); } if ( !isset($body['type']) || !in_array($body['type'], array('Catalog', 'Collection')) ) { - return RestoLogUtil::httpError(400, 'Missing mandatory type - must be set to *Catalog* or *Collection*'); + RestoLogUtil::httpError(400, 'Missing mandatory type - must be set to *Catalog* or *Collection*'); } if ( !isset($body['description']) && $body['type'] === 'Catalog' ) { - return RestoLogUtil::httpError(400, 'Missing mandatory description'); + RestoLogUtil::httpError(400, 'Missing mandatory description'); } if ( !isset($body['links']) ) { $body['links'] = array(); @@ -396,7 +396,7 @@ public function addCatalog($params, $body) for ($i = 0, $ii = count($params['segments']); $i < $ii; $i++) { $parentId = isset($parentId) ? $parentId . '/' . $params['segments'][$i] : $params['segments'][$i]; if ($this->catalogsFunctions->getCatalog($parentId) === null) { - return RestoLogUtil::httpError(400, 'Parent catalog ' . $parentId . ' does not exist.'); + RestoLogUtil::httpError(400, 'Parent catalog ' . $parentId . ' does not exist.'); } } } @@ -511,12 +511,12 @@ public function updateCatalog($params, $body) // If catalog is a collection it cannot be updated this way if ( isset($catalogs[0]['rtype']) && $catalogs[0]['rtype'] === 'collection' ) { - return RestoLogUtil::httpError(400, 'This catalog is a collection. Collection should be updated using /collections endoint'); + RestoLogUtil::httpError(400, 'This catalog is a collection. Collection should be updated using /collections endoint'); } // If user has not the right to update catalog then 403 if ( !$this->user->hasRightsTo(RestoUser::UPDATE_CATALOG, array('catalog' => $catalogs[0])) ) { - return RestoLogUtil::httpError(403); + RestoLogUtil::httpError(403); } // Update is not forced so we should check that input links array don't remove existing childs @@ -546,7 +546,7 @@ public function updateCatalog($params, $body) } if ($removed > 0 && !filter_var($params['_force'] ?? false, FILTER_VALIDATE_BOOLEAN)) { - return RestoLogUtil::httpError(400, 'The catalog update would remove ' . $removed . ' existing child(s). Set **_force** query parameter to true to force update anyway'); + RestoLogUtil::httpError(400, 'The catalog update would remove ' . $removed . ' existing child(s). Set **_force** query parameter to true to force update anyway'); } } @@ -634,12 +634,12 @@ public function removeCatalog($params) // If user has not the right to delete catalog then 403 if ( !$this->user->hasRightsTo(RestoUser::DELETE_CATALOG, array('catalog' => $catalogs[0])) ) { - return RestoLogUtil::httpError(403); + RestoLogUtil::httpError(403); } // If catalogs has child, do not remove it unless _force option is set to true if ( $count > 1 && !filter_var($params['_force'] ?? false, FILTER_VALIDATE_BOOLEAN) ){ - 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'); + RestoLogUtil::httpError(400, 'The catalog contains ' . ($count - 1) . ' child(s) and cannot be deleted. Set **_force** query parameter to true to force deletion anyway'); } return RestoLogUtil::success('Catalog deleted', $this->catalogsFunctions->removeCatalog($catalogs[0]['id'])); @@ -1321,14 +1321,14 @@ public function search($params, $body) $model = null; if (isset($params['model'])) { if (! class_exists($params['model'])) { - return RestoLogUtil::httpError(400, 'Unknown model ' . $params['model']); + RestoLogUtil::httpError(400, 'Unknown model ' . $params['model']); } $model = new $params['model'](); } // [STAC] Only one of either intersects or bbox should be specified. If both are specified, a 400 Bad Request response should be returned. if (isset($params['intersects']) && isset($params['bbox'])) { - return RestoLogUtil::httpError(400, 'Only one of either intersects or bbox should be specified'); + RestoLogUtil::httpError(400, 'Only one of either intersects or bbox should be specified'); } // Set Content-Type to GeoJSON @@ -1377,7 +1377,7 @@ private function processPath($segments, $params = array()) // This is not possible if (count($segments) < 2) { - return RestoLogUtil::httpError(404); + RestoLogUtil::httpError(404); } array_pop($segments); @@ -1388,7 +1388,7 @@ private function processPath($segments, $params = array()) ), false); if ( empty($catalogs) ) { - return RestoLogUtil::httpError(404); + RestoLogUtil::httpError(404); } $searchParams = array( @@ -1409,7 +1409,7 @@ private function processPath($segments, $params = array()) $parentAndChilds = $this->getParentAndChilds(join('/', $segments), $params); if ( !isset($parentAndChilds) ) { - return RestoLogUtil::httpError(404); + RestoLogUtil::httpError(404); } $catalog = array( @@ -1464,7 +1464,7 @@ private function processAddons($segments, $params) ))); } else { - return RestoLogUtil::httpError(404); + RestoLogUtil::httpError(404); } } @@ -1483,7 +1483,7 @@ private function processAddons($segments, $params) ))); } else { - return RestoLogUtil::httpError(404); + RestoLogUtil::httpError(404); } } @@ -1507,7 +1507,7 @@ private function jsonQueryToKVP($jsonQuery) */ if ( isset($jsonQuery['collections']) ) { if ( !is_array($jsonQuery['collections']) ) { - return RestoLogUtil::httpError(400, 'Invalid collections parameter. Should be an array of strings'); + RestoLogUtil::httpError(400, 'Invalid collections parameter. Should be an array of strings'); } $params['collections'] = join(',', $jsonQuery['collections']); } @@ -1517,7 +1517,7 @@ private function jsonQueryToKVP($jsonQuery) */ if ( isset($jsonQuery['bbox']) ) { if ( is_array($jsonQuery['bbox']) || count($jsonQuery['bbox']) !== 4 ) { - return RestoLogUtil::httpError(400, 'Invalid bbox parameter. Should be an array of 4 coordinates'); + RestoLogUtil::httpError(400, 'Invalid bbox parameter. Should be an array of 4 coordinates'); } $params['bbox'] = join(',', $jsonQuery['bbox']); } @@ -1527,7 +1527,7 @@ private function jsonQueryToKVP($jsonQuery) */ if ( isset($jsonQuery['intersects']) ) { if ( !isset($jsonQuery['intersects']['type']) || !isset($jsonQuery['intersects']['coordinates']) ) { - return RestoLogUtil::httpError(400, 'Invalid intersects. Should be a GeoJSON geometry object'); + RestoLogUtil::httpError(400, 'Invalid intersects. Should be a GeoJSON geometry object'); } $params['intersects'] = RestoGeometryUtil::geoJSONGeometryToWKT($jsonQuery['intersects']); } @@ -1740,7 +1740,7 @@ private function getIdPath($catalog, $parentId) $theoricalUrl = $this->context->core['baseUrl'] . RestoRouter::ROUTE_TO_CATALOGS; $exploded = explode($theoricalUrl, $catalog['links'][$i]['href']); if (count($exploded) !== 2) { - return RestoLogUtil::httpError(400, 'Parent link is set but it\'s url is invalid - should starts with ' . $theoricalUrl); + RestoLogUtil::httpError(400, 'Parent link is set but it\'s url is invalid - should starts with ' . $theoricalUrl); } $parentIdInBody = str_starts_with($exploded[1], '/') ? substr($exploded[1], 1) : $exploded[1]; break; @@ -1749,7 +1749,7 @@ private function getIdPath($catalog, $parentId) if ( $hasParentInBody ) { if ( isset($parentId) && $parentId !== $parentIdInBody ) { - return RestoLogUtil::httpError(400, 'The rel=parent catalog differs from the path ' . $parentId); + RestoLogUtil::httpError(400, 'The rel=parent catalog differs from the path ' . $parentId); } return $parentIdInBody . '/' . $catalog['id']; } diff --git a/app/resto/core/api/ServicesAPI.php b/app/resto/core/api/ServicesAPI.php index f318e175..100d8e74 100755 --- a/app/resto/core/api/ServicesAPI.php +++ b/app/resto/core/api/ServicesAPI.php @@ -33,8 +33,8 @@ public function __construct($context, $user) { $this->context = $context; $this->user = $user; - $this->title = getenv('API_INFO_TITLE') ?? $this->title; - $this->description = getenv('API_INFO_DESCRIPTION') ?? $this->description; + $this->title = getenv('STAC_ROOT_TITLE') ?? $this->title; + $this->description = getenv('STAC_ROOT_DESCRIPTION') ?? $this->description; } /** @@ -75,7 +75,7 @@ public function api() } if ($content === false) { - return RestoLogUtil::httpError(404); + RestoLogUtil::httpError(404); } if ($this->context->outputFormat === 'json') { @@ -249,7 +249,7 @@ public function hello() array( 'rel' => 'root', 'type' => RestoUtil::$contentTypes['json'], - 'title' => getenv('API_INFO_TITLE'), + 'title' => $this->title, 'href' => $this->context->core['baseUrl'] ), array( @@ -273,86 +273,6 @@ public function hello() return $this->context->core['useJSONLD'] ? JSONLDUtil::addDataCatalogMetadata($hello) : $hello; } - /** - * Return OpenSearchDescription document - * - * @OA\Get( - * path="/services/osdd/{collectionId}", - * summary="Get OpenSearch Description Document for a collection", - * description="Returns the OpenSearch Document Description (OSDD) for the search service of collection {collectionId}", - * tags={"Collection"}, - * @OA\Parameter( - * name="collectionId", - * in="path", - * description="Collection identifier", - * required=true, - * @OA\Items( - * type="string" - * ) - * ), - * @OA\Response( - * response="200", - * description="OpenSearch Document Description (OSDD)" - * ), - * @OA\Response( - * response="404", - * description="Collection not found" - * ) - * ) - * - * @param array params - */ - public function getOSDDForCollection($params) - { - $this->context->outputFormat = 'xml'; - return $this->context->keeper->getRestoCollection($params['collectionId'],$this->user)->load()->getOSDD(); - } - - /** - * Return OpenSearchDescription document - * - * @OA\Get( - * path="/services/osdd", - * summary="Get OpenSearch Description Document for all collections", - * description="Returns the OpenSearch Document Description (OSDD) for the search service on all collections", - * tags={"Collection"}, - * @OA\Parameter( - * name="model", - * in="query", - * style="form", - * description="Limit description to collections belonging to *model* - e.g. *model=SatelliteModel* will search in all satellite collections", - * required=false, - * @OA\Items( - * type="string" - * ) - * ), - * @OA\Response( - * response="200", - * description="OpenSearch Document Description (OSDD)" - * ), - * @OA\Response( - * response="404", - * description="Collection not found" - * ) - * ) - * - * @param array params - */ - public function getOSDD($params) - { - $this->context->outputFormat = 'xml'; - $model = null; - if (isset($params['model'])) { - if (! class_exists($params['model'])) { - return RestoLogUtil::httpError(400, 'Unknown model ' . $params['model']); - } - $model = new $params['model'](array( - 'addons' => $this->context->addons - )); - } - return $this->context->keeper->getRestoCollections($this->user)->getOSDD($model); - } - /** * Send a reset password link to user * diff --git a/app/resto/core/api/UsersAPI.php b/app/resto/core/api/UsersAPI.php index c739dcba..7016417f 100755 --- a/app/resto/core/api/UsersAPI.php +++ b/app/resto/core/api/UsersAPI.php @@ -148,18 +148,18 @@ public function __construct($context, $user) public function getUsersProfiles($params) { if (isset($params['lt']) && !ctype_digit($params['lt'])) { - return RestoLogUtil::httpError(400, 'Invalid lt - should be numeric'); + RestoLogUtil::httpError(400, 'Invalid lt - should be numeric'); } if (isset($params['groupid']) && !ctype_digit($params['groupid'])) { - return RestoLogUtil::httpError(400, 'Invalid groupid'); + RestoLogUtil::httpError(400, 'Invalid groupid'); } if (isset($params['in'])) { $exploded = explode(',', $params['in']); for ($i = count($exploded);$i--;) { if (!ctype_digit(trim($exploded[$i]))) { - return RestoLogUtil::httpError(400, 'Invalid in'); + RestoLogUtil::httpError(400, 'Invalid in'); } } } @@ -665,7 +665,7 @@ public function getUserLogs($params) } if (isset($params['lt']) && !ctype_digit($params['lt'])) { - return RestoLogUtil::httpError(400, 'Invalid lt - should be numeric'); + RestoLogUtil::httpError(400, 'Invalid lt - should be numeric'); } return (new LogsFunctions($this->context->dbDriver))->getLogs(array( diff --git a/app/resto/core/dbfunctions/CatalogsFunctions.php b/app/resto/core/dbfunctions/CatalogsFunctions.php index e05e25d4..8aea50ac 100755 --- a/app/resto/core/dbfunctions/CatalogsFunctions.php +++ b/app/resto/core/dbfunctions/CatalogsFunctions.php @@ -533,6 +533,11 @@ private function storeCatalog($catalog, $userid, $context, $collectionId, $featu return; } + // Set rtype for Collection + if ( isset($catalog['type']) && $catalog['type'] === 'Collection' ) { + $catalog['rtype'] = 'collection'; + } + // [IMPORTANT] Catalog identifier should never have a trailing / if ( substr($catalog['id'], -1) === '/' ) { $catalog['id'] = rtrim($catalog['id'], '/'); @@ -542,7 +547,7 @@ private function storeCatalog($catalog, $userid, $context, $collectionId, $featu // For collection, do not store properties since it's a duplication of properties within collection table $properties = null; - if ( isset($catalog['type']) && $catalog['type'] === 'Collection' ) { + if ( isset($catalog['rtype']) && $catalog['rtype'] === 'collection' ) { $catalog = array_merge($catalog, [ 'title' => null, 'description' => null, @@ -850,7 +855,7 @@ private function getCleanLinks($catalog, $context) { if ( in_array($link['rel'], array('child', 'item', 'items')) ) { if ( !isset($link['href']) ) { - return RestoLogUtil::httpError(400, 'One link has an empty href'); + RestoLogUtil::httpError(400, 'One link has an empty href'); } /* @@ -878,7 +883,7 @@ private function getCleanLinks($catalog, $context) { // Check for link existence ! if ( !(new FeaturesFunctions($this->dbDriver))->featureExists($internalItem['id'], $this->dbDriver->targetSchema . '.feature', $internalItem['collection']) ) { - return RestoLogUtil::httpError(400, 'Feature ' . $internalItem['href'] . ' does not exist. Ingest it first !'); + RestoLogUtil::httpError(400, 'Feature ' . $internalItem['href'] . ' does not exist. Ingest it first !'); } continue; @@ -896,7 +901,7 @@ private function getCleanLinks($catalog, $context) { $childId = substr($link['href'], strlen($context->core['baseUrl'] . RestoRouter::ROUTE_TO_CATALOGS) + 1); $exploded = explode('/', $childId); if ( count($exploded) <= count(explode('/', $catalog['id'])) ) { - return RestoLogUtil::httpError(400, 'Child ' . $link['href'] . ' is invalid because it references a parent resource'); + RestoLogUtil::httpError(400, 'Child ' . $link['href'] . ' is invalid because it references a parent resource'); } // Keep track of child ids for delete before update else { @@ -915,12 +920,12 @@ private function getCleanLinks($catalog, $context) { $exploded = explode($context->core['baseUrl'] . RestoRouter::ROUTE_TO_CATALOGS . '/', $link['href']); if ( count($exploded) !== 2) { - return RestoLogUtil::httpError(400, 'One link child has an external href i.e. not starting with ' . $context->core['baseUrl'] . RestoRouter::ROUTE_TO_CATALOGS); + RestoLogUtil::httpError(400, 'One link child has an external href i.e. not starting with ' . $context->core['baseUrl'] . RestoRouter::ROUTE_TO_CATALOGS); } $childCatalog = $this->getCatalog($exploded[1]); if ( $childCatalog === null ) { - return RestoLogUtil::httpError(400, 'Catalog child ' . $link['href'] . ' does not exist'); + RestoLogUtil::httpError(400, 'Catalog child ' . $link['href'] . ' does not exist'); } } @@ -950,7 +955,9 @@ private function onTheFlyUpdateCountersWithCollection($catalogs) // First get collections counts if ( !empty($collectionsList) ) { try { - $results = $this->dbDriver->query('SELECT id, counters, title, description FROM ' . $this->dbDriver->targetSchema . '.catalog WHERE id IN (' . join(',', $collectionsList) . ')'); + $query = 'WITH tmp AS (SELECT id, substring(id, 13) as collectionid, counters, title, description FROM ' . $this->dbDriver->targetSchema . '.catalog WHERE id IN (' . join(',', $collectionsList) . '))'; + $query = $query . ' SELECT tmp.id, col.title, col.description, tmp.counters FROM ' . $this->dbDriver->targetSchema . '.collection as col, tmp WHERE col.id = tmp.collectionid'; + $results = $this->dbDriver->query($query); while ($result = pg_fetch_assoc($results)) { for ($i = 0, $ii = count($catalogs); $i < $ii; $i++) { if ($catalogs[$i]['rtype'] === 'collection' && $result['id'] === 'collections/' . substr($catalogs[$i]['id'], strrpos($catalogs[$i]['id'], '/') + 1)) { diff --git a/app/resto/core/dbfunctions/CollectionsFunctions.php b/app/resto/core/dbfunctions/CollectionsFunctions.php index 26d310f8..46bda4ef 100755 --- a/app/resto/core/dbfunctions/CollectionsFunctions.php +++ b/app/resto/core/dbfunctions/CollectionsFunctions.php @@ -34,13 +34,13 @@ public function __construct($dbDriver) } /** - * Get description for collection + * Get collection * * @param string $id * @return array * @throws Exception */ - public function getCollectionDescription($id) + public function getCollection($id) { // Eventually convert input alias to the real collection id @@ -51,7 +51,7 @@ public function getCollectionDescription($id) // Query with aliases $query = join(' ', array( - 'SELECT id, version, visibility, owner, model, licenseid, to_iso8601(startdate) as startdate, to_iso8601(completiondate) as completiondate, Box2D(bbox) as box2d, providers, properties, links, assets, array_to_json(keywords) as keywords, STRING_AGG(ca.alias, \', \' ORDER BY ca.alias) AS aliases', + 'SELECT id, title, description, version, visibility, owner, model, licenseid, to_iso8601(startdate) as startdate, to_iso8601(completiondate) as completiondate, Box2D(bbox) as box2d, providers, properties, links, assets, array_to_json(keywords) as keywords, STRING_AGG(ca.alias, \', \' ORDER BY ca.alias) AS aliases', 'FROM ' . $this->dbDriver->targetSchema . '.collection', 'LEFT JOIN ' . $this->dbDriver->targetSchema . '.collection_alias ca ON id = ca.collection', 'WHERE id=$1', @@ -59,12 +59,9 @@ public function getCollectionDescription($id) )); $results = $this->dbDriver->pQuery($query, array($id)); - $collection = null; - while ($rowDescription = pg_fetch_assoc($results)) { - // Get Opensearch description - $osDescriptions = $this->getOSDescriptions($id); - $collection = $this->format($rowDescription, $osDescriptions[$id] ?? null); + while ($rawCollection = pg_fetch_assoc($results)) { + $collection = $this->format($rawCollection); } return $collection; @@ -72,19 +69,16 @@ public function getCollectionDescription($id) } /** - * Get description of all collections + * Get all collections * * @param array $params * @return array * @throws Exception */ - public function getCollectionsDescriptions($params = array()) + public function getCollections($params = array()) { $collections = array(); - // Get all Opensearch descriptions - $osDescriptions = $this->getOSDescriptions(); - // Where clause $where = array(); if (isset($params['group']) && count($params['group']) > 0) { @@ -98,7 +92,7 @@ public function getCollectionsDescriptions($params = array()) // Query with aliases $query = join(' ', array( - 'SELECT id, version, visibility, owner, model, licenseid, to_iso8601(startdate) as startdate, to_iso8601(completiondate) as completiondate, Box2D(bbox) as box2d, providers, properties, links, assets, array_to_json(keywords) as keywords, STRING_AGG(ca.alias, \', \' ORDER BY ca.alias) AS aliases', + 'SELECT id, title, description, version, visibility, owner, model, licenseid, to_iso8601(startdate) as startdate, to_iso8601(completiondate) as completiondate, Box2D(bbox) as box2d, providers, properties, links, assets, array_to_json(keywords) as keywords, STRING_AGG(ca.alias, \', \' ORDER BY ca.alias) AS aliases', 'FROM ' . $this->dbDriver->targetSchema . '.collection', 'LEFT JOIN ' . $this->dbDriver->targetSchema . '.collection_alias ca ON id = ca.collection', (count($where) > 0 ? 'WHERE ' . join(' AND ', $where) : ''), @@ -107,8 +101,8 @@ public function getCollectionsDescriptions($params = array()) )); $results = $this->dbDriver->query($query); - while ($rowDescription = pg_fetch_assoc($results)) { - $collections[$rowDescription['id']] = $this->format($rowDescription, $osDescriptions[$rowDescription['id']] ?? null); + while ($rawCollection = pg_fetch_assoc($results)) { + $collections[$rawCollection['id']] = $this->format($rawCollection); } return $collections; } @@ -177,23 +171,97 @@ public function removeCollection($collection, $baseUrl) * Save collection to database * * @param RestoCollection $collection - * @param Array $rights + * @param array $rights * * @throws Exception */ public function storeCollection($collection, $rights) { + + /* + * First generate a right keywords array + */ + $keywords = null; + if (!empty($collection->keywords)) { + $keywords = array(); + for ($i = 0, $ii = count($collection->keywords); $i < $ii; $i++) { + $keywords[] = '"' . $collection->keywords[$i] . '"'; + } + $keywords = '{' . join(',', $keywords) . '}'; + } + + /* + * Extract spatial and temporals extents + */ + $startDate = $collection->extent['temporal']['interval'][0][0] ?? null; + $completionDate = $collection->extent['temporal']['interval'][0][1] ?? null; + try { + /* * Start transaction */ $this->dbDriver->query('BEGIN'); /* - * Create new entry in collections osdescriptions tables + * Create collection */ - $this->storeCollectionDescription($collection); - + if (! $this->collectionExists($collection->id)) { + $toBeSet = array( + 'id' => $collection->id, + 'title' => $collection->title, + 'description' => $collection->description, + 'created' => 'now()', + 'model' => $collection->model->getName(), + 'lineage' => '{' . join(',', $collection->model->getLineage()) . '}', + // Be carefull license column is named licenseid in table + 'licenseid' => $collection->license, + 'visibility' => $collection->visibility, + 'owner' => $collection->owner, + 'providers' => json_encode($collection->providers, JSON_UNESCAPED_SLASHES), + 'properties' => json_encode($collection->properties, JSON_UNESCAPED_SLASHES), + 'links' => json_encode($collection->links, JSON_UNESCAPED_SLASHES), + 'assets' => json_encode($collection->assets, JSON_UNESCAPED_SLASHES), + 'keywords' => $keywords, + 'version' => $collection->version, + 'startdate' => $startDate, + 'completiondate' => $completionDate + ); + + // bbox is set + if (isset($collection->extent['spatial']['bbox'][0])) { + if (count($collection->extent['spatial']['bbox'][0]) !== 4) { + RestoLogUtil::httpError(400, 'Invalid input bbox'); + } + $this->dbDriver->pQuery('INSERT INTO ' . $this->dbDriver->targetSchema . '.collection (' . join(',', array_keys($toBeSet)) . ', bbox) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, ST_SetSRID(ST_MakeBox2D(ST_Point($18, $19), ST_Point($20, $21)), 4326) )', array_merge(array_values($toBeSet), $collection->extent['spatial']['bbox'][0])); + } else { + $this->dbDriver->pQuery('INSERT INTO ' . $this->dbDriver->targetSchema . '.collection (' . join(',', array_keys($toBeSet)) . ') VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)', array_values($toBeSet)); + } + } + /* + * Otherwise update collection fields + */ + else { + $this->dbDriver->pQuery('UPDATE ' . $this->dbDriver->targetSchema . '.collection SET model=$2, lineage=$3, licenseid=$4, visibility=$5, providers=$6, properties=$7, links=$8, assets=$9, keywords=$10, version=$11, title=$12, description=$13 WHERE id=$1', array( + $collection->id, + $collection->model->getName(), + '{' . join(',', $collection->model->getLineage()) . '}', + $collection->license, + $collection->visibility, + json_encode($collection->providers, JSON_UNESCAPED_SLASHES), + json_encode($collection->properties, JSON_UNESCAPED_SLASHES), + json_encode($collection->links, JSON_UNESCAPED_SLASHES), + json_encode($collection->assets, JSON_UNESCAPED_SLASHES), + $keywords, + $collection->version, + $collection->title, + $collection->description + )); + } + + // Store aliases + $this->updateAliases($collection->id, $collection->aliases ?? array()); + /* * Close transaction */ @@ -217,7 +285,7 @@ public function storeCollection($collection, $rights) * * @param RestoCollection $collection * @param array $extentArrays - array of "dates" and "bboxes" arrays - * @return array + * @return boolean * @throws Exception */ public function updateExtent($collection, $extentArrays) @@ -262,7 +330,7 @@ public function updateExtent($collection, $extentArrays) /** * Store collection aliases * - * @param String $collectionName + * @param string $collectionName * @param array $aliases * */ @@ -339,7 +407,7 @@ private function getTimeExtent($dates) * Get bounding box of bounding boxes * * @param array $bboxes - array of bbox - * @return array + * @return array|null * @throws Exception */ private function getSpatialExtent($bboxes) @@ -366,205 +434,6 @@ private function getSpatialExtent($bboxes) return $bbox; } - /** - * Get OpenSearch description array for input collection - * - * @param string $collectionId - * @return array - * @throws Exception - */ - private function getOSDescriptions($collectionId = null) - { - $osDescriptions = array(); - - if (isset($collectionId)) { - $results = $this->dbDriver->pQuery('SELECT * FROM ' . $this->dbDriver->targetSchema . '.osdescription WHERE collection=$1', array($collectionId)); - } else { - $results = $this->dbDriver->query('SELECT * FROM ' . $this->dbDriver->targetSchema . '.osdescription'); - } - - while ($description = pg_fetch_assoc($results)) { - if (!isset($osDescriptions[$description['collection']])) { - $osDescriptions[$description['collection']]['collection'] = array(); - } - $osDescriptions[$description['collection']][$description['lang']] = array( - 'ShortName' => $description['shortname'], - 'LongName' => $description['longname'], - 'Description' => $description['description'], - 'Tags' => $description['tags'], - 'Developer' => $description['developer'], - 'Contact' => $description['contact'], - 'Query' => $description['query'], - 'Attribution' => $description['attribution'] - ); - } - - return $osDescriptions; - } - - /** - * Store Collection description - * - * @param RestoCollection $collection - * - */ - private function storeCollectionDescription($collection) - { - /* - * First generate a right keywords array - */ - $keywords = null; - if (!empty($collection->keywords)) { - $keywords = array(); - for ($i = 0, $ii = count($collection->keywords); $i < $ii; $i++) { - $keywords[] = '"' . $collection->keywords[$i] . '"'; - } - $keywords = '{' . join(',', $keywords) . '}'; - } - - /* - * Extract spatial and temporals extents - */ - $startDate = $collection->extent['temporal']['interval'][0][0] ?? null; - $completionDate = $collection->extent['temporal']['interval'][0][1] ?? null; - - /* - * Create collection - */ - if (! $this->collectionExists($collection->id)) { - $toBeSet = array( - 'id' => $collection->id, - 'created' => 'now()', - 'model' => $collection->model->getName(), - 'lineage' => '{' . join(',', $collection->model->getLineage()) . '}', - // Be carefull license column is named licenseid in table - 'licenseid' => $collection->license, - 'visibility' => $collection->visibility, - 'owner' => $collection->owner, - 'providers' => json_encode($collection->providers, JSON_UNESCAPED_SLASHES), - 'properties' => json_encode($collection->properties, JSON_UNESCAPED_SLASHES), - 'links' => json_encode($collection->links, JSON_UNESCAPED_SLASHES), - 'assets' => json_encode($collection->assets, JSON_UNESCAPED_SLASHES), - 'keywords' => $keywords, - 'version' => $collection->version, - 'startdate' => $startDate, - 'completiondate' => $completionDate - ); - - // bbox is set - if (isset($collection->extent['spatial']['bbox'][0])) { - if (count($collection->extent['spatial']['bbox'][0]) !== 4) { - return RestoLogUtil::httpError(400, 'Invalid input bbox'); - } - $this->dbDriver->pQuery('INSERT INTO ' . $this->dbDriver->targetSchema . '.collection (' . join(',', array_keys($toBeSet)) . ', bbox) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, ST_SetSRID(ST_MakeBox2D(ST_Point($16, $17), ST_Point($18, $19)), 4326) )', array_merge(array_values($toBeSet), $collection->extent['spatial']['bbox'][0])); - } else { - $this->dbDriver->pQuery('INSERT INTO ' . $this->dbDriver->targetSchema . '.collection (' . join(',', array_keys($toBeSet)) . ') VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)', array_values($toBeSet)); - } - } - /* - * Otherwise update collection fields (version, visibility, licenseid, providers and properties) - */ - else { - $this->dbDriver->pQuery('UPDATE ' . $this->dbDriver->targetSchema . '.collection SET model=$2, lineage=$3, licenseid=$4, visibility=$5, providers=$6, properties=$7, links=$8, assets=$9, keywords=$10, version=$11 WHERE id=$1', array( - $collection->id, - $collection->model->getName(), - '{' . join(',', $collection->model->getLineage()) . '}', - $collection->license, - $collection->visibility, - json_encode($collection->providers, JSON_UNESCAPED_SLASHES), - json_encode($collection->properties, JSON_UNESCAPED_SLASHES), - json_encode($collection->links, JSON_UNESCAPED_SLASHES), - json_encode($collection->assets, JSON_UNESCAPED_SLASHES), - $keywords, - $collection->version - )); - } - - // Store aliases - $this->updateAliases($collection->id, $collection->aliases ?? array()); - - // OpenSearch description is stored in another table - $this->storeOSDescription($collection); - - return true; - } - - /** - * Store OpenSearch collection description - * (This function is called within storeCollectionDescription) - * - * @param RestoCollection $collection - * - */ - private function storeOSDescription($collection) - { - /* - * Insert OpenSearch descriptions within osdescriptions table - * (one description per lang) - * - * CREATE TABLE [$this->dbDriver->targetSchema].osdescription ( - * collection TEXT, - * lang TEXT, - * shortname TEXT, - * longname TEXT, - * description TEXT, - * tags TEXT, - * developer TEXT, - * contact TEXT, - * query TEXT, - * attribution TEXT - * ); - */ - $this->dbDriver->pQuery('DELETE FROM ' . $this->dbDriver->targetSchema . '.osdescription WHERE collection=$1', array( - $collection->id - )); - - foreach ($collection->osDescription as $lang => $description) { - $osFields = array( - 'collection', - 'lang' - ); - $osValues = array( - '\'' . pg_escape_string($this->dbDriver->getConnection(), $collection->id) . '\'', - '\'' . pg_escape_string($this->dbDriver->getConnection(), $lang) . '\'' - ); - - /* - * OpenSearch 1.1 draft 5 constraints - * (http://www.opensearch.org/Specifications/OpenSearch/1.1) - * - * [STAC] Remove constraints on ShortName, LongName and Description - */ - $validProperties = array( - //'ShortName' => 16, - 'ShortName' => -1, - //'LongName' => 48, - 'LongName' => -1, - //'Description' => 1024, - 'Description' => -1, - 'Tags' => 256, - 'Developer' => 64, - 'Contact' => -1, - 'Query' => -1, - //'Attribution' => 256 - 'Attribution' => -1 - ); - foreach (array_keys($description) as $key) { - /* - * Throw exception if property is invalid - */ - if (isset($validProperties[$key])) { - if ($validProperties[$key] !== -1 && strlen($description[$key]) > $validProperties[$key]) { - RestoLogUtil::httpError(400, 'OpenSearch property ' . $key . ' length is greater than ' . $validProperties[$key] . ' characters'); - } - $osFields[] = strtolower($key); - $osValues[] = '\'' . pg_escape_string($this->dbDriver->getConnection(), $description[$key]) . '\''; - } - } - $this->dbDriver->query('INSERT INTO ' . $this->dbDriver->targetSchema . '.osdescription (' . join(',', $osFields) . ') VALUES(' . join(',', $osValues) . ')'); - } - } - /** * Return true if collection is empty, false otherwise * @@ -583,57 +452,45 @@ private function collectionIsEmpty($collection) /** * Return a formated collection description * - * @param array $rawDescription - * @param array $osDescription + * @param array $rawCollection */ - private function format($rawDescription, $osDescription) + private function format($rawCollection) { $collection = array( - 'id' => $rawDescription['id'], - 'aliases' => isset($rawDescription['aliases']) ? json_decode($rawDescription['aliases'], true) : array(), - 'version' => $rawDescription['version'] ?? null, - 'model' => $rawDescription['model'], - 'visibility' => (integer) $rawDescription['visibility'], - 'owner' => $rawDescription['owner'], - 'providers' => json_decode($rawDescription['providers'], true), - 'assets' => json_decode($rawDescription['assets'], true), - 'keywords' => isset($rawDescription['keywords']) ? json_decode($rawDescription['keywords'], true) : array(), - 'links' => json_decode($rawDescription['links'], true), + 'id' => $rawCollection['id'], + 'title' => $rawCollection['title'], + 'description' => $rawCollection['description'], + 'aliases' => isset($rawCollection['aliases']) ? json_decode($rawCollection['aliases'], true) : array(), + 'version' => $rawCollection['version'] ?? null, + 'model' => $rawCollection['model'], + 'visibility' => (integer) $rawCollection['visibility'], + 'owner' => $rawCollection['owner'], + 'providers' => json_decode($rawCollection['providers'], true), + 'assets' => json_decode($rawCollection['assets'], true), + 'keywords' => isset($rawCollection['keywords']) ? json_decode($rawCollection['keywords'], true) : array(), + 'links' => json_decode($rawCollection['links'], true), 'extent' => array( 'spatial' => array( 'bbox' => array( - RestoGeometryUtil::box2dTobbox($rawDescription['box2d']) + RestoGeometryUtil::box2dTobbox($rawCollection['box2d']) ), 'crs' => 'http://www.opengis.net/def/crs/OGC/1.3/CRS84' ), 'temporal' => array( 'interval' => array( array( - $rawDescription['startdate'] ?? null, $rawDescription['completiondate'] ?? null + $rawCollection['startdate'] ?? null, $rawCollection['completiondate'] ?? null ) ), 'trs' => 'http://www.opengis.net/def/uom/ISO-8601/0/Gregorian' ) ), // Be carefull license column is named licenseid in table - 'license' => $rawDescription['licenseid'], + 'license' => $rawCollection['licenseid'], // Special _properties will be discarded in toArray() - 'properties' => json_decode($rawDescription['properties'], true), - 'osDescription' => $osDescription + 'properties' => json_decode($rawCollection['properties'], true) ); - /* - * If OpenSearch Description object is not set, create a minimal one from $object['description'] - */ - if (!isset($osDescription) || !is_array($osDescription) || !isset($osDescription['en']) || !is_array($osDescription['en'])) { - $collection['osDescription'] = array( - 'en' => array( - 'ShortName' => $collection['properties']['title'] ?? $collection['id'], - 'Description' => $collection['properties']['description'] ?? '' - ) - ); - } - return $collection; } diff --git a/app/resto/core/dbfunctions/FeaturesFunctions.php b/app/resto/core/dbfunctions/FeaturesFunctions.php index 035e224b..c5f17cee 100755 --- a/app/resto/core/dbfunctions/FeaturesFunctions.php +++ b/app/resto/core/dbfunctions/FeaturesFunctions.php @@ -171,7 +171,7 @@ public function search($context, $user, $model, $collections, $paramsWithOperati try { $results = $this->dbDriver->query($query); } catch (Exception $e) { - return RestoLogUtil::httpError(400, $e->getMessage()); + RestoLogUtil::httpError(400, $e->getMessage()); } $features = (new RestoFeatureUtil($context, $user, $collections))->toFeatureArrayList($this->dbDriver->fetch($results)); @@ -328,7 +328,7 @@ public function storeFeature($id, $collection, $featureArray) foreach (array_values($keysAndValues['catalogs']) as $catalog) { if (isset($catalog['isExternal']) && $catalog['isExternal']) { if ( !$collection->user->hasRightsTo(RestoUser::CREATE_CATALOG) ) { - return RestoLogUtil::httpError(403, 'Feature ingestion leads to creation of catalog ' . $catalog['id'] . ' but you don\'t have right to create catalogs'); + RestoLogUtil::httpError(403, 'Feature ingestion leads to creation of catalog ' . $catalog['id'] . ' but you don\'t have right to create catalogs'); } break; } @@ -408,7 +408,7 @@ public function removeFeature($feature) /* * Update statistics counter for featureId - i.e. remove 1 per catalogs containing this feature */ - $catalogsUpdated = (new CatalogsFunctions($this->dbDriver))->updateFeatureCatalogsCounters($feature->id, $feature->collectio->id, -1); + $catalogsUpdated = (new CatalogsFunctions($this->dbDriver))->updateFeatureCatalogsCounters($feature->id, $feature->collection->id, -1); /* * Next remove @@ -426,7 +426,7 @@ public function removeFeature($feature) } if (empty($result)) { - return RestoLogUtil::httpError(404); + RestoLogUtil::httpError(404); } return array( @@ -477,7 +477,7 @@ public function updateFeature($feature, $collection, $newFeatureArray) foreach (array_values($keysAndValues['catalogs']) as $catalog) { if (isset($catalog['isExternal']) && $catalog['isExternal']) { if ( !$collection->user->hasRightsTo(RestoUser::CREATE_CATALOG) ) { - return RestoLogUtil::httpError(403, 'Feature update leads to creation of catalog ' . $catalog['id'] . ' but you don\'t have right to create catalogs'); + RestoLogUtil::httpError(403, 'Feature update leads to creation of catalog ' . $catalog['id'] . ' but you don\'t have right to create catalogs'); } break; } @@ -580,7 +580,7 @@ public function updateFeatureProperty($feature, $property, $value) public function updateFeatureDescription($feature, $description) { - return RestoLogUtil::httpError(400, 'TODO - update feature description not yet implemented'); + RestoLogUtil::httpError(400, 'TODO - update feature description not yet implemented'); } diff --git a/app/resto/core/dbfunctions/GroupsFunctions.php b/app/resto/core/dbfunctions/GroupsFunctions.php index 03e2b8b1..c0034314 100755 --- a/app/resto/core/dbfunctions/GroupsFunctions.php +++ b/app/resto/core/dbfunctions/GroupsFunctions.php @@ -218,7 +218,7 @@ private function formatGroups($results, $groupId) // 404 if no empty results when id is specified if (! isset($results) || (isset($groupId) && count($results) === 0)) { - return RestoLogUtil::httpError(404); + RestoLogUtil::httpError(404); } $length = isset($groupId) ? 1 : count($results); diff --git a/app/resto/core/dbfunctions/UsersFunctions.php b/app/resto/core/dbfunctions/UsersFunctions.php index 06f29c62..2b3e283a 100755 --- a/app/resto/core/dbfunctions/UsersFunctions.php +++ b/app/resto/core/dbfunctions/UsersFunctions.php @@ -249,7 +249,7 @@ public function getUsersProfiles($params, $userid) // Search on firstname if length > 3 if (isset($params['q'])) { if (strlen($params['q']) < 3 || strpos($params['q'], '%') !== false) { - return RestoLogUtil::httpError(400); + RestoLogUtil::httpError(400); } $where[] = 'name ILIKE \'%' . pg_escape_string($this->dbDriver->getConnection(), $params['q']). '%\''; } @@ -581,6 +581,6 @@ private function getPicture($profile, $storageInfo = null) } } - return RestoLogUtil::httpError(400, 'Invalid picture'); + RestoLogUtil::httpError(400, 'Invalid picture'); } } diff --git a/app/resto/core/utils/Curly.php b/app/resto/core/utils/Curly.php index 08eced89..68fd3ed4 100755 --- a/app/resto/core/utils/Curly.php +++ b/app/resto/core/utils/Curly.php @@ -28,7 +28,7 @@ class Curly public function __construct() { if (! extension_loaded('curl')) { - return RestoLogUtil::httpError(500, 'Curl extension not loaded'); + RestoLogUtil::httpError(500, 'Curl extension not loaded'); } $this->handler = curl_init(); diff --git a/app/resto/core/utils/RestoGeometryUtil.php b/app/resto/core/utils/RestoGeometryUtil.php index 202b84da..94aad6fa 100755 --- a/app/resto/core/utils/RestoGeometryUtil.php +++ b/app/resto/core/utils/RestoGeometryUtil.php @@ -342,7 +342,7 @@ public static function forceWKT($geostring) } if (!isset($geostring) || !RestoGeometryUtil::isValidWKT($geostring)) { - return RestoLogUtil::httpError(400, 'Invalid input geometry for intersects - should be a valid GeoJSON or Well Known Text standard (WKT)'); + RestoLogUtil::httpError(400, 'Invalid input geometry for intersects - should be a valid GeoJSON or Well Known Text standard (WKT)'); } return $geostring; diff --git a/app/resto/core/utils/SecurityUtil.php b/app/resto/core/utils/SecurityUtil.php index b440e8a5..e60922c7 100755 --- a/app/resto/core/utils/SecurityUtil.php +++ b/app/resto/core/utils/SecurityUtil.php @@ -93,7 +93,7 @@ public function authenticate($context) * Authentication headers were present but authentication leads to unauthentified user => security error */ if ($authRequested && !isset($user->profile['id'])) { - return RestoLogUtil::httpError(401); + RestoLogUtil::httpError(401); } return $user; diff --git a/app/resto/core/xml/ATOMFeed.php b/app/resto/core/xml/ATOMFeed.php index cff9939d..6889e929 100755 --- a/app/resto/core/xml/ATOMFeed.php +++ b/app/resto/core/xml/ATOMFeed.php @@ -111,7 +111,7 @@ public function toString() public function setCollectionElements($links, $searchContext, $model) { /* - * Update outputFormat links except for OSDD 'search' + * Update outputFormat links */ $this->setCollectionLinks($links); @@ -499,19 +499,10 @@ private function setCollectionLinks($links) for ($i = 0, $l = count($links); $i < $l; $i++) { $this->startElement('link'); $this->writeAttributes(array( - 'rel' => $links[$i]['rel'] + 'rel' => $links[$i]['rel'], + 'type' => RestoUtil::$contentTypes['atom'], + 'href' => RestoUtil::updateUrlFormat($links[$i]['href'], 'atom') )); - if ($links[$i]['type'] === 'application/opensearchdescription+xml') { - $this->writeAttributes(array( - 'type' => $links[$i]['type'], - 'href' => $links[$i]['href'] - )); - } else { - $this->writeAttributes(array( - 'type' => RestoUtil::$contentTypes['atom'], - 'href' => RestoUtil::updateUrlFormat($links[$i]['href'], 'atom') - )); - } $this->endElement(); // link } } diff --git a/app/resto/core/xml/OSDD.php b/app/resto/core/xml/OSDD.php deleted file mode 100755 index 04d3cb14..00000000 --- a/app/resto/core/xml/OSDD.php +++ /dev/null @@ -1,345 +0,0 @@ - - * OpenSearch search - * My OpenSearch search interface - * - * admin@myserver.org - * opensearch - * My OpenSearch search interface - * - * Jérôme Gasperi - * mapshup.com - * fr - * - * - */ -class OSDD extends RestoXML -{ - /* - * Reference to context - */ - private $context; - - /* - * Reference to model - */ - private $model; - - /* - * Reference to collection object (null if no collection) - */ - private $collection; - - /* - * Client Id (CEOS Opensearch Best Practice document) - */ - private $clientId; - - /* - * OpenSearch description - */ - private $osDescription; - - /* - * Collection statistics - */ - private $statistics = array(); - - /* - * Output contentTypes - */ - private $contentTypes = array('html', 'atom', 'json'); - - /* - * Template extension parameters - */ - private $extensionParams = array( - 'minimum', - 'maximum', - 'minExclusive', - 'maxExclusive', - 'minInclusive', - 'maxInclusive', - 'pattern', - 'title' - ); - - /** - * Constructor - * - * @param RestoContext $context - * @param RestoModel $model - * @param Array $statistics - * @param RestoCollection $collection - */ - public function __construct($context, $model, $statistics, $collection) - { - parent::__construct(); - $this->context = $context; - $this->model = $model; - $this->statistics = $statistics; - $this->collection = $collection; - $this->clientId = isset($this->context->query['clientId']) ? 'clientId=' . rawurlencode($this->context->query['clientId']) . '&' : ''; - $this->osDescription = isset($this->collection) ? $this->collection->osDescription[$this->context->lang] ?? $this->collection->osDescription['en'] : $this->context->osDescription; - $this->setOSDD(); - } - - /** - * Set OpenSearch Description Document - */ - private function setOSDD() - { - /* - * Start OpsenSearchDescription - */ - $this->startOSDD(); - - /* - * Start elements - */ - $this->setStartingElements(); - - /* - * Generate elements - */ - $this->setUrls(); - - /* - * Generate informations elements - */ - $this->setEndingElements(); - - /* - * OpsenSearchDescription - end element - */ - $this->endElement(); - } - - /** - * Start XML OpenSearchDescription element - */ - private function startOSDD() - { - $this->startElement('OpenSearchDescription'); - $this->writeAttributes(array( - 'xml:lang' => $this->context->lang, - 'xmlns' => 'http://a9.com/-/spec/opensearch/1.1/', - 'xmlns:atom' => 'http://www.w3.org/2005/Atom', - 'xmlns:time' => 'http://a9.com/-/opensearch/extensions/time/1.0/', - 'xmlns:geo' => 'http://a9.com/-/opensearch/extensions/geo/1.0/', - 'xmlns:eo' => 'http://a9.com/-/opensearch/extensions/eo/1.0/', - 'xmlns:parameters' => 'http://a9.com/-/spec/opensearch/extensions/parameters/1.0/', - 'xmlns:dc' => 'http://purl.org/dc/elements/1.1/', - 'xmlns:rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - 'xmlns:resto' => 'http://mapshup.info/-/resto/2.0/' - )); - } - - /** - * Set OSDD starting elements - */ - private function setStartingElements() - { - $this->writeElements(array( - 'ShortName' => $this->osDescription['ShortName'], - 'Description' => $this->osDescription['Description'] - )); - } - - /** - * Set OSDD ending elements - */ - private function setEndingElements() - { - $this->writeElements(array( - 'Contact' => $this->osDescription['Contact'], - 'Tags' => implode(' ', array('CEOS-OS-BP-V1.0', $this->osDescription['Tags'])), - 'LongName' => $this->osDescription['LongName'] - )); - $this->startElement('Query'); - $this->writeAttributes(array( - 'role' => 'example', - 'searchTerms' => $this->osDescription['Query'] - )); - $this->endElement('Query'); - $this->writeElements(array( - 'Developer' => $this->osDescription['Developer'], - 'Attribution' => $this->osDescription['Attribution'], - 'SyndicationRight' => 'open', - 'AdultContent' => 'false' - )); - for ($i = 0, $l = count($this->context->core['languages']); $i < $l; $i++) { - $this->writeElement('Language', $this->context->core['languages'][$i]); - } - $this->writeElements(array( - 'InputEncoding' => 'UTF-8', - 'OutputEncoding' => 'UTF-8' - )); - } - - /** - * Generate OSDD elements - */ - private function setUrls() - { - foreach (array_values($this->contentTypes) as $format) { - /* - * Special case for HTML output - */ - if ($format === 'html' && empty($this->context->core['htmlSearchEndpoint'])) { - continue; - } - - /* - * element - */ - $this->startElement('Url'); - $this->writeAttributes(array( - 'type' => RestoUtil::$contentTypes[$format], - 'rel' => 'results', - 'template' => $this->getUrlTemplate($format) - )); - - /* - * Extension parameters - */ - $this->setParameters($format); - - /* - * End element - */ - $this->endElement(); - } - } - - /** - * Return template url for format - * - * @param string $format - * @return string - */ - private function getUrlTemplate($format) - { - /* - * HTML output is based on htmlSearchEndpoint - */ - if ($format === 'html') { - return $this->context->core['htmlSearchEndpoint'] . '?' . $this->model->searchFilters['searchTerms']['osKey'] .'={searchTerms?}'; - } - - $url = RestoUtil::restoUrl( - $this->context->core['baseUrl'], - ( - isset($this->collection) - ? RestoUtil::replaceInTemplate( - RestoRouter::ROUTE_TO_FEATURES, - array( - 'collectionId' => $this->collection->id - ) - ) - : '/search' - ), - $format - ) . '?' . $this->clientId; - - $count = 0; - foreach ($this->model->searchFilters as $filterName => $filter) { - if (isset($filter) && !isset($filter['hidden'])) { - $optional = isset($filter['minimum']) && $filter['minimum'] === 1 ? '' : '?'; - $url .= ($count > 0 ? '&' : '') . $filter['osKey'] . '={' . $filterName . $optional . '}'; - $count++; - } - } - return $url; - } - - /** - * Set elements - * - * @param string $format - */ - private function setParameters($format) - { - foreach ($this->model->searchFilters as $filterName => $filter) { - if (isset($filter) && $filterName != 'searchTerms' && !isset($filter['hidden'])) { - $this->startElement('parameters:Parameter'); - $this->writeAttributes(array( - 'name' => $filter['osKey'], - 'value' => '{' . $filterName . '}' - )); - for ($i = count($this->extensionParams); $i--;) { - if (isset($filter[$this->extensionParams[$i]])) { - $this->writeAttribute($this->extensionParams[$i], $filter[$this->extensionParams[$i]]); - } - } - - /* - * Options - two cases - * 1. predefined value/label - * 2. retrieve from database - */ - if (isset($filter['options'])) { - if (is_array($filter['options'])) { - for ($i = count($filter['options']); $i--;) { - $this->startElement('parameters:Option'); - $this->writeAttribute('value', $filter['options'][$i]['value']); - if (isset($filter['options'][$i]['label'])) { - $this->writeAttribute('label', $filter['options'][$i]['label']); - } - $this->endElement(); - } - } elseif ($filter['options'] === 'auto') { - if (isset($filter['osKey']) && isset($this->statistics[$filter['osKey']])) { - /* - * [STAC] Summaries format is "oneOf" if several options - */ - if (isset($this->statistics[$filter['osKey']]['const'])) { - $this->startElement('parameters:Option'); - $this->writeAttribute('value', $this->statistics[$filter['osKey']]['const']); - $this->endElement(); - } else { - for ($i = count($this->statistics[$filter['osKey']]['oneOf']); $i--;) { - $this->startElement('parameters:Option'); - $this->writeAttribute('value', $this->statistics[$filter['osKey']]['oneOf'][$i]['const']); - $this->endElement(); - } - } - } - } - } - $this->endElement(); // parameters:Parameter - } - } - - /* - * Parameter extension for clientId - */ - if ($this->clientId !== '') { - $this->startElement('parameters:Parameter'); - $this->writeAttributes(array( - 'name' => 'clientId', - 'minimum', '1' - )); - $this->endElement(); // parameters:Parameter - } - } -} diff --git a/build/resto/config.php.template b/build/resto/config.php.template index ad3053ac..07e13a41 100755 --- a/build/resto/config.php.template +++ b/build/resto/config.php.template @@ -79,18 +79,6 @@ return array( ) ) ), - 'osDescriptions' => array( - 'en' => array( - 'ShortName' => '${SEARCH_OPENSEARCH_SHORTNAME}', - 'LongName' => '${SEARCH_OPENSEARCH_LONGNAME}', - 'Description' => '${SEARCH_OPENSEARCH_DESCRIPTION}', - 'Tags' => '${SEARCH_OPENSEARCH_TAGS}', - 'Developer' => '${SEARCH_OPENSEARCH_DEVELOPER}', - 'Contact' => '${SEARCH_OPENSEARCH_CONTACT}', - 'Query' => '${SEARCH_OPENSEARCH_QUERY}', - 'Attribution' => '${SEARCH_OPENSEARCH_ATTRIBUTION}' - ) - ), 'addons' => array( 'Cataloger' => array( 'options' => array( diff --git a/config-dev.env b/config-dev.env index 26defbdb..555f5b2e 100644 --- a/config-dev.env +++ b/config-dev.env @@ -37,10 +37,8 @@ COMPOSE_FILE=docker-compose.yml:docker-compose.dev.yml ### ===================================================================== ### Documentation configuration ### ===================================================================== -API_INFO_TITLE="Welcome to resto" -API_INFO_DESCRIPTION="A metadata catalog and search engine for geospatialized data" -API_INFO_CONTACT_EMAIL=jerome.gasperi@gmail.com -API_HOST_DESCRIPTION="resto localhost server" +STAC_ROOT_TITLE="Welcome to resto" +STAC_ROOT_DESCRIPTION="A metadata catalog and search engine for geospatialized data" ### ===================================================================== ### Database configuration @@ -113,18 +111,7 @@ JWT_PASSPHRASE="Super secret passphrase" ### Comma separated (No space !) list of fields that are sortable - DON'T ADD FIELDS UNLESS YOU KNOW WHAT YOU ARE DOING ### First field is the default sorting field -#SEARCH_SORTABLE_FIELDS=startDate,created - -### Generic OpenSearch service description (i.e. when searching on all collections) -## [IMPORTANT] SEARCH_OPENSEARCH_SHORTNAME is equivalent to STAC collection id - so a best practice is to not uses spaces or special characters -SEARCH_OPENSEARCH_SHORTNAME=resto -SEARCH_OPENSEARCH_LONGNAME="resto search service" -SEARCH_OPENSEARCH_DESCRIPTION="Search on all collections" -SEARCH_OPENSEARCH_TAGS=resto -SEARCH_OPENSEARCH_DEVELOPER="Jérôme Gasperi" -SEARCH_OPENSEARCH_CONTACT=jerome.gasperi@gmail.com -SEARCH_OPENSEARCH_QUERY="europe 2015" -SEARCH_OPENSEARCH_ATTRIBUTION="Copyright 2018, All Rights Reserved" +#SEARCH_SORTABLE_FIELDS=startDate,created- ### ===================================================================== ### Sendmail configuration - use for user activation, reset password etc. diff --git a/config.env b/config.env index 124117db..10aa8588 100644 --- a/config.env +++ b/config.env @@ -40,10 +40,8 @@ SUPPORTED_LANGUAGES=en,fr ### ===================================================================== ### Documentation configuration ### ===================================================================== -API_INFO_TITLE="Welcome to resto" -API_INFO_DESCRIPTION="A metadata catalog and search engine for geospatialized data" -API_INFO_CONTACT_EMAIL=jerome.gasperi@gmail.com -API_HOST_DESCRIPTION="resto localhost server" +STAC_ROOT_TITLE="Welcome to resto" +STAC_ROOT_DESCRIPTION="A metadata catalog and search engine for geospatialized data" ### URL to the documentation url. If not set will use the built in resto html documentation #DOCUMENTATION_URL= @@ -133,17 +131,6 @@ JWT_PASSPHRASE="Super secret passphrase" ### First field is the default sorting field #SEARCH_SORTABLE_FIELDS=startDate,created -### Generic OpenSearch service description (i.e. when searching on all collections) -## [IMPORTANT] SEARCH_OPENSEARCH_SHORTNAME is equivalent to STAC collection id - so a best practice is to not uses spaces or special characters -SEARCH_OPENSEARCH_SHORTNAME=resto -SEARCH_OPENSEARCH_LONGNAME="resto search service" -SEARCH_OPENSEARCH_DESCRIPTION="Search on all collections" -SEARCH_OPENSEARCH_TAGS=resto -SEARCH_OPENSEARCH_DEVELOPER="Jérôme Gasperi" -SEARCH_OPENSEARCH_CONTACT=jerome.gasperi@gmail.com -SEARCH_OPENSEARCH_QUERY="europe 2015" -SEARCH_OPENSEARCH_ATTRIBUTION="Copyright 2018, All Rights Reserved" - ### ===================================================================== ### Sendmail configuration - use for user activation, reset password etc. ### ===================================================================== diff --git a/docs/COLLECTIONS_AND_CATALOGS.md b/docs/COLLECTIONS_AND_CATALOGS.md index d10a4f95..aa54444f 100644 --- a/docs/COLLECTIONS_AND_CATALOGS.md +++ b/docs/COLLECTIONS_AND_CATALOGS.md @@ -13,11 +13,11 @@ Eventually, it can contain an array of *aliases* (see [./examples/collections/L8 Note that id and aliases must be unique in the database. As a consequence, you cannot create a new collection or set an alias to an existing collection that as the same value of one of the aliases of an existing collection. -### Ingest a collection -To ingest a collection using the default **ADMIN_USER_NAME** and **ADMIN_USER_PASSWORD** (see [config.env](config.env)) : +### Add a collection +To add a collection using the default **ADMIN_USER_NAME** and **ADMIN_USER_PASSWORD** (see [config.env](config.env)) : - # POST a S2 dummy collection - curl -X POST -d@examples/collections/S2.json "http://admin:admin@localhost:5252/collections" + # POST a dummy collection + curl -X POST -d@examples/collections/DummyCollection.json "http://admin:admin@localhost:5252/collections" Then get the collections list : @@ -25,6 +25,14 @@ Then get the collections list : *Note: Any user with the "createCollection" right can create a collection ([see rights](./USERS.md))* +### Update a collection +To update a collection + + # UPDATE dummy collection + curl -X PUT -d@examples/collections/DummyCollection_update.json "http://admin:admin@localhost:5252/collections/DummyCollection" + +Then get the collections list : + ### Ingest an item (aka feature) To ingest a feature using the default **ADMIN_USER_NAME** and **ADMIN_USER_PASSWORD** (see [config.env](config.env)) : @@ -42,11 +50,6 @@ Then get the feature : ## Catalogs -### Add a dummy collection - - # Create a dummy collection - curl -X POST -d@examples/collections/DummyCollection.json "http://admin:admin@localhost:5252/collections" - ### Add a catalog # Create a catalog - user with the "createCatalog" right can create a catalog diff --git a/docs/MIGRATION_TO_9.1.0.md b/docs/MIGRATION_TO_9.1.0.md new file mode 100644 index 00000000..1e4dfdf1 --- /dev/null +++ b/docs/MIGRATION_TO_9.1.0.md @@ -0,0 +1,20 @@ +# Migration to resto 9.1.0 +After upgrading to resto v9.1.0, you should perform the following SQL requests on the database + + -- + -- TO BE RUN FOR resto v9.1.0 + -- + ALTER TABLE resto.collection ADD COLUMN title TEXT; + ALTER TABLE resto.collection ADD COLUMN description TEXT; + + WITH tmp as ( + SELECT collection, longname, description + FROM dummy.osdescription + WHERE lang='en' + ) + UPDATE dummy.collection + SET title=tmp.longname, description=tmp.description + FROM tmp + WHERE tmp.collection=id; + + DROP TABLE resto.osdescription; diff --git a/examples/collections/DummyCollection.json b/examples/collections/DummyCollection.json index c43da6e6..6087ad00 100644 --- a/examples/collections/DummyCollection.json +++ b/examples/collections/DummyCollection.json @@ -8,18 +8,6 @@ ], "version": "1.0", "license": "PDDL-1.0", - "osDescription": { - "en": { - "ShortName": "DummyCollection", - "LongName": "DummyCollection with alias", - "Description": "This is a Dummy Collection with an alias", - "Tags": "dummy", - "Developer": "Jérôme Gasperi", - "Contact": "jrom@snapplanet.io", - "Query": "dummy", - "Attribution": "USGS/NASA Landsat" - } - }, "providers": [ { "name": "jeobrowser", diff --git a/examples/collections/DummyCollection2.json b/examples/collections/DummyCollection2.json index adc69d05..b7c5f646 100644 --- a/examples/collections/DummyCollection2.json +++ b/examples/collections/DummyCollection2.json @@ -1,20 +1,14 @@ { "id": "DummyCollection2", "type": "Collection", + "title": "DummyCollection2", + "description": "This is a Dummy Collection2", "version": "1.0", "license": "PDDL-1.0", - "osDescription": { - "en": { - "ShortName": "DummyCollection2", - "LongName": "DummyCollection2", - "Description": "This is a Dummy Collection2", - "Tags": "dummy", - "Developer": "Jérôme Gasperi", - "Contact": "jrom@snapplanet.io", - "Query": "dummy", - "Attribution": "USGS/NASA Landsat" - } - }, + "keywords": [ + "dummy", + "keywords" + ], "providers": [ { "name": "jeobrowser", diff --git a/examples/collections/DummyCollection_update.json b/examples/collections/DummyCollection_update.json new file mode 100644 index 00000000..3566f4e1 --- /dev/null +++ b/examples/collections/DummyCollection_update.json @@ -0,0 +1,21 @@ +{ + "id": "DummyCollection", + "type": "Collection", + "title": "DummyCollection with an updated title", + "description":"This is a Dummy Collection with an updated title", + "aliases":[ + "DummyCollectionAlias" + ], + "version": "1.0", + "license": "PDDL-1.0", + "providers": [ + { + "name": "jeobrowser", + "roles": [ + "processor" + ], + "url": "https://github.com/jjrom/resto" + } + ], + "anyProperty":"You can add other free properties" +} \ No newline at end of file diff --git a/examples/collections/L8.json b/examples/collections/L8.json index 5a779203..87d3674e 100644 --- a/examples/collections/L8.json +++ b/examples/collections/L8.json @@ -1,34 +1,19 @@ { "id": "L8", "type": "Collection", + "title": "Images Landsat-8 niveau 1C", + "description": "Landsat represents the world's longest continuously acquired collection of space-based moderate-resolution land remote sensing data. Four decades of imagery provides a unique resource for those who work in agriculture, geology, forestry, regional planning, education, mapping, and global change research. Landsat images are also invaluable for emergency response and disaster relief", + "keywords":[ + "landsat", + "level1C", + "USGS" + ], "aliases":[ "Landsat8" ], "version": "1.0", "model": "OpticalModel", "license": "PDDL-1.0", - "osDescription": { - "en": { - "ShortName": "Landsat-8", - "LongName": "Images Landsat-8 niveau 1C", - "Description": "Landsat represents the world's longest continuously acquired collection of space-based moderate-resolution land remote sensing data. Four decades of imagery provides a unique resource for those who work in agriculture, geology, forestry, regional planning, education, mapping, and global change research. Landsat images are also invaluable for emergency response and disaster relief", - "Tags": "landsat level1C USGS", - "Developer": "Jérôme Gasperi", - "Contact": "jrom@snapplanet.io", - "Query": "USA 2019", - "Attribution": "USGS/NASA Landsat" - }, - "fr": { - "ShortName": "Landsat-8", - "LongName": "Landsat-8 Level 1C product", - "Description": "Le programme Landsat est le premier programme spatial d'observation de la Terre d\u00e9di\u00e9 \u00e0 des fins civiles. Quarante ann\u00e9es d'imageries spatiales fournissent une ressource unique dans les secteurs de l'agriculture, de la g\u00e9ologie, de la gestion des for\u00eats, de l'\u00e9ducation, et de la recherche sur le changement climatique", - "Tags": "landsat level1C USGS", - "Developer": "Jérôme Gasperi", - "Contact": "jrom@snapplanet.io", - "Query": "US 2019", - "Attribution": "USGS/NASA Landsat" - } - }, "providers": [ { "name": "USGS", diff --git a/examples/collections/L8_nosummaries.json b/examples/collections/L8_nosummaries.json index ab5b1648..ffa5cf65 100644 --- a/examples/collections/L8_nosummaries.json +++ b/examples/collections/L8_nosummaries.json @@ -1,34 +1,19 @@ { "id": "L8", "type": "Collection", + "title":"Landsat-8 Level 1C product", + "description": "Landsat represents the world's longest continuously acquired collection of space-based moderate-resolution land remote sensing data. Four decades of imagery provides a unique resource for those who work in agriculture, geology, forestry, regional planning, education, mapping, and global change research. Landsat images are also invaluable for emergency response and disaster relief", + "keywords":[ + "landsat", + "USGS", + "level1C" + ], "aliases":[ "Landsat8" ], "version": "1.0", "model": "SatelliteModel", "license": "PDDL-1.0", - "osDescription": { - "en": { - "ShortName": "Landsat-8", - "LongName": "Images Landsat-8 niveau 1C", - "Description": "Landsat represents the world's longest continuously acquired collection of space-based moderate-resolution land remote sensing data. Four decades of imagery provides a unique resource for those who work in agriculture, geology, forestry, regional planning, education, mapping, and global change research. Landsat images are also invaluable for emergency response and disaster relief", - "Tags": "landsat level1C USGS", - "Developer": "Jérôme Gasperi", - "Contact": "jrom@snapplanet.io", - "Query": "USA 2019", - "Attribution": "USGS/NASA Landsat" - }, - "fr": { - "ShortName": "Landsat-8", - "LongName": "Landsat-8 Level 1C product", - "Description": "Le programme Landsat est le premier programme spatial d'observation de la Terre d\u00e9di\u00e9 \u00e0 des fins civiles. Quarante ann\u00e9es d'imageries spatiales fournissent une ressource unique dans les secteurs de l'agriculture, de la g\u00e9ologie, de la gestion des for\u00eats, de l'\u00e9ducation, et de la recherche sur le changement climatique", - "Tags": "landsat level1C USGS", - "Developer": "Jérôme Gasperi", - "Contact": "jrom@snapplanet.io", - "Query": "US 2019", - "Attribution": "USGS/NASA Landsat" - } - }, "providers": [ { "name": "USGS", diff --git a/examples/collections/S2.json b/examples/collections/S2.json index e2d7b84d..43e221da 100644 --- a/examples/collections/S2.json +++ b/examples/collections/S2.json @@ -1,31 +1,20 @@ { "id": "S2", "type": "Collection", + "title": "Level 1C Sentinel-2 images", + "description": "The SENTINEL-2 mission is a land monitoring constellation of two satellites each equipped with a MSI (Multispectral Imager) instrument covering 13 spectral bands providing high resolution optical imagery (i.e., 10m, 20m, 60 m) every 10 days with one satellite and 5 days with two satellites", + "kewyords": [ + "copernicus", + "esa", + "eu", + "msi", + "radiance", + "sentinel", + "sentinel2" + ], "version": "1.0", "model": "OpticalModel", "license": "proprietary", - "osDescription": { - "en": { - "ShortName": "Sentinel-2", - "LongName": "Level 1C Sentinel-2 images", - "Description": "The SENTINEL-2 mission is a land monitoring constellation of two satellites each equipped with a MSI (Multispectral Imager) instrument covering 13 spectral bands providing high resolution optical imagery (i.e., 10m, 20m, 60 m) every 10 days with one satellite and 5 days with two satellites", - "Tags": "copernicus esa eu msi radiance sentinel sentinel2", - "Developer": "Jérôme Gasperi", - "Contact": "jrom@snapplanet.io", - "Query": "Toulouse", - "Attribution": "European Union/ESA/Copernicus" - }, - "fr": { - "ShortName": "Sentinel-2", - "LongName": "Images Sentinel-2 Niveau 1C", - "Description": "La mission SENTINEL-2 est constituée de deux satellites d'imagerie optique équipés d’un imageur multispectral (MSI) en 13 bandes spectrales avec des résolutions de 10, 20 et 60 mètres et d'une fauchée unique de 290 km de large. La capacité d'observation des deux satellites permet de surveiller l'intégralité des terres émergées du globe tous les 5 jours", - "Tags": "copernicus esa eu msi radiance sentinel sentinel2", - "Developer": "Jérôme Gasperi", - "Contact": "jrom@snapplanet.io", - "Query": "Toulouse", - "Attribution": "European Union/ESA/Copernicus" - } - }, "providers": [ { "name": "European Union/ESA/Copernicus", diff --git a/resto-database-model/03_resto_target_model.sql b/resto-database-model/03_resto_target_model.sql index a2efbbbe..e7814a2c 100644 --- a/resto-database-model/03_resto_target_model.sql +++ b/resto-database-model/03_resto_target_model.sql @@ -15,6 +15,12 @@ CREATE TABLE IF NOT EXISTS __DATABASE_TARGET_SCHEMA__.collection ( -- Unique id for collection "id" TEXT PRIMARY KEY, + -- [STAC] Title + title TEXT, + + -- [STAC] Description + description TEXT, + -- Collection version version TEXT, @@ -63,43 +69,6 @@ CREATE TABLE IF NOT EXISTS __DATABASE_TARGET_SCHEMA__.collection ( ); --- --- osdescriptions table describe all RESTo collections --- -CREATE TABLE IF NOT EXISTS __DATABASE_TARGET_SCHEMA__.osdescription ( - - -- Reference __DATABASE_TARGET_SCHEMA__.collection.id - collection TEXT REFERENCES __DATABASE_TARGET_SCHEMA__.collection (id) ON DELETE CASCADE, - - -- OpenSearch description lang - lang TEXT, - - -- Contains a brief human-readable title that identifies this search engine. - shortname TEXT, - - -- Contains an extended human-readable title that identifies this search engine. - longname TEXT, - - -- Contains a human-readable text description of the search engine. - description TEXT, - - -- Contains a set of words that are used as keywords to identify and categorize this search content. Tags must be a single word and are delimited by the space character - tags TEXT, - - -- Contains the human-readable name or identifier of the creator or maintainer of the description document. - Developer TEXT, - - -- Contains an email address at which the maintainer of the description document can be reached. - contact TEXT, - - -- Defines a search query that can be performed by search clients. Please see the OpenSearch Query element specification for more information. - query TEXT, - - -- Contains a list of all sources or entities that should be credited for the content contained in the search feed - attribution TEXT - -); - -- -- Collection aliases -- @@ -426,12 +395,6 @@ CREATE INDEX IF NOT EXISTS idx_visibility_collection ON __DATABASE_TARGET_SCHEMA CREATE INDEX IF NOT EXISTS idx_created_collection ON __DATABASE_TARGET_SCHEMA__.collection (created); CREATE INDEX IF NOT EXISTS idx_keywords_collection ON __DATABASE_TARGET_SCHEMA__.collection USING GIN (keywords); --- [TABLE __DATABASE_TARGET_SCHEMA__.osdescription] -ALTER TABLE __DATABASE_TARGET_SCHEMA__.osdescription DROP CONSTRAINT IF EXISTS cl_collection; -ALTER TABLE ONLY __DATABASE_TARGET_SCHEMA__.osdescription ADD CONSTRAINT cl_collection UNIQUE(collection, lang); - -CREATE INDEX IF NOT EXISTS idx_collection_osdescription ON __DATABASE_TARGET_SCHEMA__.osdescription (collection); - -- [TABLE __DATABASE_TARGET_SCHEMA__.catalog] CREATE INDEX IF NOT EXISTS idx_description_catalog ON __DATABASE_TARGET_SCHEMA__.catalog USING GIN (public.normalize(description) gin_trgm_ops); CREATE INDEX IF NOT EXISTS idx_level_catalog ON __DATABASE_TARGET_SCHEMA__.catalog USING btree (level);