-
Notifications
You must be signed in to change notification settings - Fork 0
feat(gis-integration): implement spec (#462) #483
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b308c93
8b50eff
80e6b92
f8a35e3
8bda74f
699a218
aa3b60c
82e0773
93fd1db
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * Procest WFS Export Controller | ||
| * | ||
| * Exposes case locations as a standard WFS endpoint so external GIS | ||
| * applications (QGIS, ArcGIS, MapInfo, etc.) can consume Procest case data | ||
| * as a map layer. Implements AC 6 of the gis-integration spec. | ||
| * | ||
| * Endpoint: GET /api/gis/wfs | ||
| * Auth: NoAdminRequired — any authenticated Nextcloud user (external GIS apps | ||
| * authenticate via HTTP Basic Auth or OIDC). | ||
| * | ||
| * @category Controller | ||
| * @package OCA\Procest\Controller | ||
| * | ||
| * @author Conduction Development Team <info@conduction.nl> | ||
| * @copyright 2026 Conduction B.V. | ||
| * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 | ||
| * | ||
| * @link https://conduction.nl | ||
| * | ||
| * @spec openspec/changes/gis-integration/tasks.md#task-19 | ||
| * | ||
| * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl> | ||
| * SPDX-License-Identifier: EUPL-1.2 | ||
| */ | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace OCA\Procest\Controller; | ||
|
|
||
| use OCA\Procest\Service\WfsExportService; | ||
| use OCP\AppFramework\Controller; | ||
| use OCP\AppFramework\Http\JSONResponse; | ||
| use OCP\AppFramework\OCS\OCSForbiddenException; | ||
| use OCP\IRequest; | ||
| use OCP\IUserSession; | ||
|
|
||
| /** | ||
| * Controller exposing case locations as a WFS GeoJSON endpoint. | ||
| * | ||
| * @spec openspec/changes/gis-integration/tasks.md#task-19 | ||
| */ | ||
| class WfsExportController extends Controller | ||
| { | ||
| /** | ||
| * Constructor. | ||
| * | ||
| * @param string $appName The application name | ||
| * @param IRequest $request The request object | ||
| * @param WfsExportService $wfsExportService The WFS export service | ||
| * @param IUserSession $userSession The user session | ||
| * | ||
| * @return void | ||
| */ | ||
| public function __construct( | ||
| string $appName, | ||
| IRequest $request, | ||
| private WfsExportService $wfsExportService, | ||
| private IUserSession $userSession, | ||
| ) { | ||
| parent::__construct(appName: $appName, request: $request); | ||
| }//end __construct() | ||
|
|
||
| /** | ||
| * Return case locations as a GeoJSON FeatureCollection (WFS GetFeature). | ||
| * | ||
| * Query parameters: | ||
| * - typeName: Feature type to return (default: procest:cases) | ||
| * - outputFormat: Output format, only application/json supported (default: application/json) | ||
| * - maxFeatures: Maximum features to return (default: 500, hard cap: 2000) | ||
| * - bbox: Bounding box filter as "minLon,minLat,maxLon,maxLat" in WGS84 (optional) | ||
| * - status: Filter by case status (optional) | ||
| * - caseType: Filter by case type name (optional) | ||
| * | ||
| * @NoAdminRequired | ||
| * | ||
| * @return JSONResponse GeoJSON FeatureCollection | ||
| * | ||
| * @throws OCSForbiddenException When user session is not authenticated | ||
| * | ||
| * @spec openspec/changes/gis-integration/tasks.md#task-19 | ||
| */ | ||
| public function getFeatures(): JSONResponse | ||
| { | ||
| if ($this->userSession->getUser() === null) { | ||
| throw new OCSForbiddenException('Authentication required'); | ||
| } | ||
|
|
||
| $typeName = (string) $this->request->getParam('typeName', WfsExportService::TYPE_NAME_CASES); | ||
| $outputFormat = (string) $this->request->getParam('outputFormat', 'application/json'); | ||
| $maxFeatures = (int) $this->request->getParam('maxFeatures', WfsExportService::DEFAULT_MAX_FEATURES); | ||
| $bboxParam = $this->request->getParam('bbox', null); | ||
| $statusRaw = $this->request->getParam('status', null); | ||
| $caseTypeRaw = $this->request->getParam('caseType', null); | ||
|
|
||
| $status = null; | ||
| if ($statusRaw !== null) { | ||
| $status = (string) $statusRaw; | ||
| } | ||
|
|
||
| $caseType = null; | ||
| if ($caseTypeRaw !== null) { | ||
| $caseType = (string) $caseTypeRaw; | ||
| } | ||
|
|
||
| if ($typeName !== WfsExportService::TYPE_NAME_CASES) { | ||
| return new JSONResponse( | ||
| ['error' => 'Unsupported typeName: '.$typeName.'. Supported: '.WfsExportService::TYPE_NAME_CASES], | ||
| 400 | ||
| ); | ||
| } | ||
|
|
||
| if ($outputFormat !== 'application/json') { | ||
| return new JSONResponse( | ||
| ['error' => 'Unsupported outputFormat: '.$outputFormat.'. Supported: application/json'], | ||
| 400 | ||
| ); | ||
| } | ||
|
|
||
| if ($maxFeatures <= 0) { | ||
| $maxFeatures = WfsExportService::DEFAULT_MAX_FEATURES; | ||
| } | ||
|
|
||
| $bbox = null; | ||
| if ($bboxParam !== null && $bboxParam !== '') { | ||
| $parts = array_map('floatval', explode(',', (string) $bboxParam)); | ||
| if (count($parts) === 4) { | ||
| $bbox = $parts; | ||
| } | ||
| } | ||
|
|
||
| $collection = $this->wfsExportService->buildFeatureCollection( | ||
| maxFeatures: $maxFeatures, | ||
| bbox: $bbox, | ||
| status: $status, | ||
| caseType: $caseType, | ||
| ); | ||
|
|
||
| return new JSONResponse($collection); | ||
| }//end getFeatures() | ||
|
|
||
| /** | ||
| * Return WFS GetCapabilities descriptor for this endpoint. | ||
| * | ||
| * @NoAdminRequired | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same root cause as The standard WFS client discovery flow calls Fix: add |
||
| * | ||
| * @return JSONResponse WFS capabilities descriptor | ||
| * | ||
| * @throws OCSForbiddenException When user session is not authenticated | ||
| * | ||
| * @spec openspec/changes/gis-integration/tasks.md#task-19 | ||
| */ | ||
| public function getCapabilities(): JSONResponse | ||
| { | ||
| if ($this->userSession->getUser() === null) { | ||
| throw new OCSForbiddenException('Authentication required'); | ||
| } | ||
|
|
||
| $baseUrl = $this->request->getServerProtocol().'://'.$this->request->getServerHost(); | ||
| $capabilities = $this->wfsExportService->buildCapabilities( | ||
| baseUrl: $baseUrl.'/index.php/apps/procest/api/gis/wfs' | ||
| ); | ||
|
|
||
| return new JSONResponse($capabilities); | ||
| }//end getCapabilities() | ||
| }//end class | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The WfsExport endpoint is explicitly designed for external GIS applications (QGIS, ArcGIS, MapInfo) that authenticate via HTTP Basic Auth or OIDC — as stated in both the controller doc-block and
design.md. However,getFeatures()is missing@NoCSRFRequired.Nextcloud's
SecurityMiddlewareenforces CSRF for every request not annotated with@NoCSRFRequired, regardless of HTTP verb.passesCSRFCheck()requires either anOCS-APIRequestheader or a validrequesttokenparameter — neither of which an external GIS client sends. SinceWfsExportControllerextendsController(notOCSController), the middleware throwsCrossSiteRequestForgeryException→ 403, and the WFS layer is completely inaccessible to external tools.For comparison,
MetricsController(Prometheus scraper),HealthController(container health checks), and allDrcControllerZGW-API endpoints all carry@NoCSRFRequiredfor exactly this reason.Fix: add
* @NoCSRFRequireddirectly below* @NoAdminRequiredhere.