feat(gis-integration): implement spec (#462)#483
Conversation
Quality Report — ConductionNL/procest @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 420/420 | |||
| PHPUnit | ❌ | ||||
| Newman | ⏭️ | ||||
| Playwright | ❌ |
Coverage: 0% (0/81 statements)
Spec coverage: 6% (30 tests / 512 specs)
Quality workflow — 2026-05-19 02:55 UTC
Download the full PDF report from the workflow artifacts.
Security Review — Clyde BarcodeResult: PASS (0 fixed, 0 unfixed, 0 blocking)
All 14 hydra gates green. Semgrep (p/security-audit, p/owasp-top-ten, p/secrets) — 0 findings. composer audit — no PHP CVEs. gitleaks — 303 total findings, 0 in PR diff files (inherited debt). npm audit — 16 CVEs all pre-existing in Vue 2 / @nextcloud/* ecosystem (not introduced by this PR). PHPUnit 13/13 pass. Manual OWASP review: auth annotations correct and consistent with sibling GisProxyController; input validation thorough (allowlisting, hard caps, floatval+bounds for bbox); no raw SQL; JSON-only responses (no XSS surface); GET endpoints (no CSRF concern); Host header validated by Nextcloud TrustedDomainMiddleware before controller runs. Out-of-scope inherited debt (informational, non-blocking)
🤖 Changes Clyde Barcode applied
View full diff · 1 file changed, 1 insertion(+), 1 deletion(-) |
Quality Report — ConductionNL/procest @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ✅ | ||||
| psalm | ❌ | ||||
| phpstan | ❌ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ❌ | |||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ |
Quality workflow — 2026-05-19 04:08 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/procest @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 420/420 | |||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Coverage: 0% (0/81 statements)
Quality workflow — 2026-05-19 04:13 UTC
Download the full PDF report from the workflow artifacts.
| * - status: Filter by case status (optional) | ||
| * - caseType: Filter by case type name (optional) | ||
| * | ||
| * @NoAdminRequired |
There was a problem hiding this comment.
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 SecurityMiddleware enforces CSRF for every request not annotated with @NoCSRFRequired, regardless of HTTP verb. passesCSRFCheck() requires either an OCS-APIRequest header or a valid requesttoken parameter — neither of which an external GIS client sends. Since WfsExportController extends Controller (not OCSController), the middleware throws CrossSiteRequestForgeryException → 403, and the WFS layer is completely inaccessible to external tools.
For comparison, MetricsController (Prometheus scraper), HealthController (container health checks), and all DrcController ZGW-API endpoints all carry @NoCSRFRequired for exactly this reason.
Fix: add * @NoCSRFRequired directly below * @NoAdminRequired here.
| /** | ||
| * Return WFS GetCapabilities descriptor for this endpoint. | ||
| * | ||
| * @NoAdminRequired |
There was a problem hiding this comment.
Same root cause as getFeatures(): getCapabilities() is also missing @NoCSRFRequired.
The standard WFS client discovery flow calls /capabilities first. If this returns a CSRF 403, external GIS tools (QGIS, ArcGIS, etc.) cannot even discover the feature types, let alone add the layer — the entire integration is dead on arrival.
Fix: add * @NoCSRFRequired directly below * @NoAdminRequired here.
| return []; | ||
| } | ||
|
|
||
| $params = ['_limit' => $limit]; |
There was a problem hiding this comment.
fetchLocations() always fetches up to $limit (max 2000) location records with only ['_limit' => $limit], then applies status and caseType filters in PHP memory via applyFilters().
This means a request for status=closed&maxFeatures=10 still loads 2000 rows from the database before discarding most of them. Not a blocker given the 2000 hard cap, but as case location datasets grow for active municipalities this will become noticeably expensive.
Suggested follow-up: pass caseStatus and caseType directly into $params so OpenRegister can filter at query time:
if ($status !== null) {
$params['caseStatus'] = $status;
}
if ($caseType !== null) {
$params['caseType'] = $caseType;
}
WilcoLouwerse
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES — 2 blocking issues, 1 informational concern.
Blockers
Both WFS endpoints (getFeatures and getCapabilities) are missing @NoCSRFRequired. Nextcloud's SecurityMiddleware enforces CSRF on every route not annotated with that tag, regardless of HTTP verb. External GIS applications (QGIS, ArcGIS, MapInfo) connecting via HTTP Basic Auth or OIDC — the explicitly stated use case in the doc-block and design.md — will receive a 403 CrossSiteRequestForgeryException on every request, making the WFS layer completely inaccessible.
Fix: add * @NoCSRFRequired below * @NoAdminRequired on both methods. Compare MetricsController, HealthController, and DrcController which all carry this annotation for the same reason (external consumers without Nextcloud session cookies).
Informational
- 🟡 Client-side
status/caseTypefiltering after full fetch — no DB-level pushdown — not a blocker given the 2000 hard cap, worth a follow-up task.
Positives
- Architecture is correct:
WfsExportServicereads exclusively through OpenRegister viaSettingsService.getObjectService()— no direct outbound HTTP calls, no PDOK bypass. - All new PHP files carry correct SPDX headers, EUPL-1.2 license, and copyright blocks.
- Tasks 18–24 are all implemented and verified; AC 6 (WFS GeoJSON export) is complete.
- All CI checks pass (PHPUnit PHP 8.3/8.4 × NC stable31/32, CodeQL, ESLint, Psalm, PHPStan).
Ports the unique outbound WFS-export capability from PR #483 (feature/462/gis-integration) onto the current development baseline. That branch had diverged from an unrelated history root and could not be merged directly without reverting large amounts of merged dev work, so only the genuinely-unique, self-contained additions are carried over: - lib/Service/WfsExportService.php — builds a GeoJSON FeatureCollection of case locations from OpenRegister (uses SettingsService.getObjectService + findObjects, already on dev) plus a WFS GetCapabilities descriptor. - lib/Controller/WfsExportController.php — GET /api/gis/wfs (GetFeature) and /api/gis/wfs/capabilities, NoAdminRequired with an authenticated- session guard, typeName/outputFormat/maxFeatures/bbox/status/caseType params. - appinfo/routes.php — two additive wfsExport routes alongside the existing gis_proxy / wms_wfs proxy routes. - Unit tests for both classes. This complements dev's existing proxy-based GIS work (GisProxy/WmsWfs); the WFS export (procest cases AS a layer) was absent on dev.
|
Superseded by #604 (merged), which ports the unique work from this PR onto the current This branch had diverged from an unrelated history root (~117 commits behind dev) and could not be merged directly — its shared-file changes (routes.php, openspec change dirs) would have reverted merged dev work, and dev had independently grown a proxy-based GIS implementation (GisProxy/WmsWfs + LocationService + the gis-integration change dir with builds). The genuinely-unique, self-contained additions — No work lost. Closing in favour of #604. |
Closes #462
Summary
Auto-generated draft PR for OpenSpec change
gis-integration.The Hydra builder ran the spec but could not run
gh pr createitself(Phase D+E credential strip — Claude has no
GH_TOKENby design).The entrypoint detected commits on the feature branch with no PR and
created this draft so the reviewer + security + applier can proceed.
Spec Reference
/spec//spec/proposal.mdCommits on this branch
Files changed
appinfo/routes.phplib/Controller/WfsExportController.phplib/Service/WfsExportService.phpopenspec/changes/gis-integration/design.mdopenspec/changes/gis-integration/tasks.mdtask-audit.jsontests/Unit/Controller/WfsExportControllerTest.phptests/Unit/Controller/WmsWfsControllerTest.phptests/Unit/Service/LocationServiceTest.phptests/Unit/Service/WfsExportServiceTest.phpPR auto-created by Hydra builder entrypoint (
hydra_ensure_pr_exists)because Claude's session closed without running
gh pr create.Reviewer + applier follow as normal.