From 67cef2b629cf93279aebb7e951b6d1ad395241fa Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 15:23:43 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20Passwortgesch=C3=BCtzte=20Datens?= =?UTF-8?q?=C3=A4tze=20nicht=20ernten,=20CI-Rechte,=20Abh=C3=A4ngigkeiten?= =?UTF-8?q?=20(v2.47.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - REST: /catalog und /delta lassen passwortgeschützte Datensätze weg, /datasets/ antwortet mit 404; ein nachträglich geschützter Datensatz erscheint in /delta als Löschmeldung. - CI: permissions: contents: read (7 CodeQL-Hinweise). - npm overrides für qs, body-parser, glob/minimatch (Dependabot-Hinweise, npm audit 0 Funde); nur Entwicklungswerkzeuge. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JB1xUQM892bVZ4Yv3MZjvq --- .github/workflows/ci.yml | 6 + CHANGELOG.md | 19 ++ CLAUDE.md | 2 +- DOCUMENTATION.md | 6 + README.md | 2 +- includes/class-rest-api.php | 44 +++- open-data-wizard.php | 4 +- package-lock.json | 384 +++++--------------------------- package.json | 7 +- tests/e2e/api-endpoints.spec.js | 15 ++ tests/test-rest-delta.php | 32 ++- 11 files changed, 180 insertions(+), 341 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e3a7dfa..eb72456c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,12 @@ on: pull_request: branches: ["main", "master"] +# Nur lesen: Kein Job schreibt ins Repository, legt Releases an oder +# kommentiert. Ohne diese Zeile erbt jeder Job die Standardrechte des +# Repositorys (CodeQL: „Workflow does not contain permissions"). +permissions: + contents: read + jobs: phpcs: name: WordPress Coding Standards diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c6b8736..19b3aa97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,25 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.ht --- +## [2.47.1] — 2026-11-02 + +### 🔒 Security +- **Passwortgeschützte Datensätze erscheinen in keinem Harvest-Endpunkt mehr.** Der Datenkatalog + auf der Website blendete sie schon aus, `/catalog` lieferte sie aber vollständig aus. + `/datasets/` antwortet jetzt mit derselben 404 wie für eine unbekannte ID. `/delta` führt + einen nachträglich geschützten Datensatz als Löschmeldung, damit auch inkrementell erntende + Portale ihn entfernen. +- **CI mit Leserechten:** `.github/workflows/ci.yml` setzt `permissions: contents: read`; vorher + erbte jeder Job die Standardrechte des Repositorys (7 CodeQL-Hinweise + „Workflow does not contain permissions"). +- **Abhängigkeiten der Testumgebung:** `overrides` in `package.json` heben `qs` (6.17.1), + `body-parser` (1.20.9), `glob` (14.0.3) und damit `minimatch` (11.0.4) auf Versionen ohne + bekannte Schwachstellen (u. a. die Dependabot-Hinweise zu `minimatch` und `qs`). `npm audit`: + 0 Funde. Betrifft nur die Entwicklungswerkzeuge (wp-env, Playwright, SHACL), nicht das + ausgelieferte Plugin. + +--- + ## [2.47.0] — 2026-11-02 Der Katalog startet leer. Beispieldaten lädt man bei Bedarf mit einem Klick — und sie landen diff --git a/CLAUDE.md b/CLAUDE.md index 0954ffc2..b2fba5ef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -779,7 +779,7 @@ Update **both** locations: - MINOR: New features (backward-compatible) - PATCH: Bug fixes only -Current: **v2.47.0** +Current: **v2.47.1** --- diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 4cf578c2..af88530e 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -172,6 +172,12 @@ GET /wp-json/datenatlas/v1/delta?since=2024-01-01T00:00:00Z Response enthält `dcat:dataset` (geänderte Datasets) und `odw:removed` (Tombstones). +#### Passwortgeschützte Datensätze + +Wie im Datenkatalog der Website: `/catalog` und `/delta` lassen sie weg, `/datasets/` +antwortet mit 404. Wird ein Datensatz nachträglich geschützt, erscheint er in `/delta` unter +`odw:removed`. + #### Beispieldaten Datensätze mit dem Meta-Schlüssel `_odw_sample` (geladen über „Beispieldaten laden" bzw. diff --git a/README.md b/README.md index 407ffb88..5583cc46 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

Lizenz - Version + Version DCAT-AP 3.0 PHP >= 8.1 WordPress 6.4+ diff --git a/includes/class-rest-api.php b/includes/class-rest-api.php index 5df3daac..6a700ed3 100644 --- a/includes/class-rest-api.php +++ b/includes/class-rest-api.php @@ -200,6 +200,9 @@ public static function get_catalog( WP_REST_Request $request ): WP_REST_Response $query_args = array( 'post_type' => 'odw_dataset', 'post_status' => 'publish', + // Passwortgeschützt heißt: nicht für alle. Der Datenkatalog auf der + // Website zeigt solche Datensätze nicht, ein Harvest also auch nicht. + 'has_password' => false, 'posts_per_page' => $per_page, 'paged' => $page, 'orderby' => 'modified', @@ -604,10 +607,11 @@ public static function get_dataset( WP_REST_Request $request ): WP_REST_Response ); } - if ( 'publish' !== $post->post_status || self::is_sample( $post_id ) ) { + if ( 'publish' !== $post->post_status || '' !== (string) $post->post_password || self::is_sample( $post_id ) ) { // Bewusst identisch zum Nicht-Existieren (404 + gleiche Meldung), // damit Entwurfs-IDs nicht von außen enumerierbar sind. Ebenso für - // Beispieldaten: Sie gehören in keinen Harvest. + // passwortgeschützte Datensätze und Beispieldaten: Sie gehören in + // keinen Harvest. return new WP_Error( 'odw_not_found', __( 'Datensatz nicht gefunden.', 'open-data-wizard' ), @@ -644,7 +648,7 @@ public static function get_dataset( WP_REST_Request $request ): WP_REST_Response * * Response body keys: * - dcat:dataset — array of full JSON-LD dataset objects modified after `since` - * - odw:removed — array of tombstone objects for datasets trashed after `since` + * - odw:removed — array of tombstone objects for datasets trashed or password-protected after `since` * * Pagination (page/per_page) applies only to the modified set; all tombstones for the * requested window are always included in full so harvesters don't miss deletions. @@ -698,6 +702,7 @@ public static function get_delta( WP_REST_Request $request ): WP_REST_Response|W array( 'post_type' => 'odw_dataset', 'post_status' => 'publish', + 'has_password' => false, 'posts_per_page' => $per_page, 'paged' => $page, 'orderby' => 'modified', @@ -734,6 +739,29 @@ public static function get_delta( WP_REST_Request $request ): WP_REST_Response|W ) ); + // Ein nachträglich mit Passwort geschützter Datensatz verschwindet aus + // /catalog. Wer inkrementell erntet, erfährt das nur über eine + // Löschmeldung — das Setzen des Passworts ändert post_modified, er + // liegt also im Fenster. + $protected_query = new WP_Query( + array( + 'post_type' => 'odw_dataset', + 'post_status' => 'publish', + 'has_password' => true, + 'posts_per_page' => -1, + 'orderby' => 'modified', + 'order' => 'DESC', + 'no_found_rows' => true, + 'meta_query' => array( self::sample_exclusion() ), + 'date_query' => array( + array( + 'column' => 'post_modified_gmt', + 'after' => $after_gmt, + ), + ), + ) + ); + $datasets = array(); foreach ( $modified_query->posts as $post ) { $jsonld = odw_build_dataset_jsonld( (int) $post->ID ); @@ -742,8 +770,16 @@ public static function get_delta( WP_REST_Request $request ): WP_REST_Response|W } } + $gone = array_merge( (array) $removed_query->posts, (array) $protected_query->posts ); + usort( + $gone, + static function ( $a, $b ): int { + return strcmp( (string) $b->post_modified_gmt, (string) $a->post_modified_gmt ); + } + ); + $removed = array(); - foreach ( $removed_query->posts as $post ) { + foreach ( $gone as $post ) { $removed[] = array( '@id' => rest_url( self::NAMESPACE . '/datasets/' . $post->ID ), '@type' => 'dcat:Dataset', diff --git a/open-data-wizard.php b/open-data-wizard.php index 4ebb8de2..30f87c73 100644 --- a/open-data-wizard.php +++ b/open-data-wizard.php @@ -3,7 +3,7 @@ * Plugin Name: Open Data Wizard * Plugin URI: https://github.com/daimpad/OpenDataWizard * Description: DCAT-AP 3.0 konforme Open Data Metadatenverwaltung für WordPress. Bereitstellung als maschinenlesbarer JSON-LD-Endpoint für offene Daten. - * Version: 2.47.0 + * Version: 2.47.1 * Requires at least: 6.4 * Requires PHP: 8.1 * Author: nozilla @@ -26,7 +26,7 @@ exit; } -define( 'ODW_VERSION', '2.47.0' ); +define( 'ODW_VERSION', '2.47.1' ); define( 'ODW_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'ODW_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); define( 'ODW_PLUGIN_FILE', __FILE__ ); diff --git a/package-lock.json b/package-lock.json index 45cd2c09..72182b24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-data-wizard", - "version": "2.47.0", + "version": "2.47.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-data-wizard", - "version": "2.47.0", + "version": "2.47.1", "license": "GPL-2.0-or-later", "devDependencies": { "@playwright/test": "^1.64.1", @@ -388,83 +388,12 @@ } } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", - "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "node_modules/@isaacs/nonrecursive-glob-match": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@isaacs/nonrecursive-glob-match/-/nonrecursive-glob-match-1.0.3.tgz", + "integrity": "sha512-SI7kyKHq4XXDRozw+r9GlRZkia1ZkFrpLW98r2IC2Bpr8YkFGE3fvjzmxCG0x78A++69HmDNrDvSMYBZmHGpKA==", "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } + "license": "BlueOak-1.0.0" }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", @@ -1383,17 +1312,6 @@ "node": ">=12" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@playwright/test": { "version": "1.64.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.1.tgz", @@ -2383,11 +2301,14 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/base64-js": { "version": "1.5.1", @@ -2416,9 +2337,9 @@ "license": "Apache-2.0" }, "node_modules/body-parser": { - "version": "1.20.8", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz", - "integrity": "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==", + "version": "1.20.9", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.9.tgz", + "integrity": "sha512-FISIQxV5u91Y9gvuFq3q/tA/qTqLJn3AhRcEvyozyhJTh5OBFFtv827AdHU7Rbyct4r+Ba1/99oFa1Y3yuFsdw==", "dev": true, "license": "MIT", "dependencies": { @@ -2470,23 +2391,6 @@ "dev": true, "license": "MIT" }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", - "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/bottleneck": { "version": "2.19.5", "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", @@ -2495,13 +2399,16 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", - "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" } }, "node_modules/btoa-lite": { @@ -3068,13 +2975,6 @@ "dev": true, "license": "MIT" }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -3092,13 +2992,6 @@ "dev": true, "license": "MIT" }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -3400,23 +3293,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3541,22 +3417,18 @@ } }, "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-14.0.3.tgz", + "integrity": "sha512-A1vjIwC3qpdCso45Iw127gnp7sPhqgY8lUQhDrArIs+GbmfaaVPFuoyy7WyLT9EPZCRZ7j7aLdwUhQmHWesEow==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "minimatch": "^11.0.2", + "minipass": "^7.1.3", + "path-scurry": "^3.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3919,22 +3791,6 @@ "node": ">=14.17" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/js-yaml": { "version": "3.15.2", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", @@ -4368,16 +4224,17 @@ } }, "node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-11.0.4.tgz", + "integrity": "sha512-/UJ1dEoVQTjPNDh6SGlUo+ueLMmoHTas4q+8yaAR/5t6nLAIiBDJtoYUh20IYleix7ehiQkgczb84PE3uzHkxg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.2" + "@isaacs/nonrecursive-glob-match": "^1.0.3", + "brace-expansion": "^5.0.12" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4598,13 +4455,6 @@ "node": ">=8" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -4649,28 +4499,31 @@ } }, "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-3.0.0.tgz", + "integrity": "sha512-1pTbQqH1wIwMi/eleQ5leFTTCkzFsPcgXNjwOA8YGGW/W4/kHT3dssE7Z6NH+zuqNLOzOB0uaD61yl7cAcVhgg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } }, "node_modules/path-to-regexp": { "version": "0.1.13", @@ -4766,9 +4619,9 @@ } }, "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "version": "6.17.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.17.1.tgz", + "integrity": "sha512-jd3COkaKVgGUW+ZHyEHCG9bH7RoaaiWuMgzsIBjUMVjLAaWGWpoUAVwljVvBHxRu/2qEHnadLGZrkutYUBPQJA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5420,76 +5273,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", - "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -5503,20 +5286,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strnum": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.2.tgz", @@ -5768,47 +5537,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", diff --git a/package.json b/package.json index 37565421..3220efcc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-data-wizard", - "version": "2.47.0", + "version": "2.47.1", "description": "WordPress plugin for publishing open data as DCAT-AP 3.0 compliant metadata", "author": "Open Data Wizard Team", "license": "GPL-2.0-or-later", @@ -33,5 +33,10 @@ "@wordpress/env": "^12.0.0", "@zazuko/env-node": "^3.1.0", "rdf-validate-shacl": "^0.6.5" + }, + "overrides": { + "body-parser": "^1.20.9", + "glob": "^14.0.2", + "qs": "^6.16.2" } } diff --git a/tests/e2e/api-endpoints.spec.js b/tests/e2e/api-endpoints.spec.js index bccbb637..7d31f990 100644 --- a/tests/e2e/api-endpoints.spec.js +++ b/tests/e2e/api-endpoints.spec.js @@ -128,6 +128,21 @@ test.describe('Harvest-Endpunkte', () => { expect(data).toHaveProperty('dcat:distribution'); }); + test('passwortgeschützte Datensätze erscheinen in keinem Endpunkt', async ({ request }) => { + const fixtures = await (await request.get('/wp-json/odw-e2e/v1/fixtures')).json(); + expect(fixtures.protected).toBeGreaterThan(0); + + const catalog = await request.get(`${API_BASE}/catalog?full=1`); + expect(catalog.status()).toBe(200); + expect(await catalog.text()).not.toContain('E2E: Passwortgeschützt'); + + const delta = await request.get(`${API_BASE}/delta?since=2000-01-01`); + expect(delta.status()).toBe(200); + expect(await delta.text()).not.toContain('E2E: Passwortgeschützt'); + + expect((await request.get(`${API_BASE}/datasets/${fixtures.protected}`)).status()).toBe(404); + }); + test('/datasets/{unbekannt} antwortet mit 404', async ({ request }) => { const response = await request.get(`${API_BASE}/datasets/99999999`); diff --git a/tests/test-rest-delta.php b/tests/test-rest-delta.php index 9ed679f6..dc0b57c8 100644 --- a/tests/test-rest-delta.php +++ b/tests/test-rest-delta.php @@ -293,6 +293,29 @@ private function make_request( array $params ): WP_REST_Request { // Beispieldaten // ------------------------------------------------------------------------- + /** + * Ein passwortgeschützter Datensatz ist nicht öffentlich und liefert + * /datasets/ deshalb nicht aus — wie der Datenkatalog der Website. + */ + public function test_get_dataset_hides_password_protected_datasets(): void { + $this->load_class(); + + $post = new \stdClass(); + $post->ID = 32; + $post->post_type = 'odw_dataset'; + $post->post_status = 'publish'; + $post->post_password = 'geheim'; + + \WP_Mock::userFunction( 'get_post' )->andReturn( $post ); + \WP_Mock::userFunction( 'get_transient' )->never(); + \WP_Mock::userFunction( '__' )->andReturnArg( 0 ); + + $result = ODW_Rest_API::get_dataset( $this->make_request( array( 'id' => 32 ) ) ); + + $this->assertInstanceOf( WP_Error::class, $result ); + $this->assertSame( 404, $result->data['status'] ); + } + /** * Ein Beispiel (ODW_Samples) liefert /datasets/ nicht aus — mit * derselben Antwort wie für eine unbekannte ID. @@ -300,10 +323,11 @@ private function make_request( array $params ): WP_REST_Request { public function test_get_dataset_hides_sample_datasets(): void { $this->load_class(); - $post = new \stdClass(); - $post->ID = 31; - $post->post_type = 'odw_dataset'; - $post->post_status = 'publish'; + $post = new \stdClass(); + $post->ID = 31; + $post->post_type = 'odw_dataset'; + $post->post_status = 'publish'; + $post->post_password = ''; \WP_Mock::userFunction( 'get_post' )->andReturn( $post ); \WP_Mock::userFunction( 'get_post_meta' )->with( 31, '_odw_sample', true )->andReturn( '1' ); From b19741b2ee617f1917c7b285db3f5d38bcb94159 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 15:28:52 +0000 Subject: [PATCH 2/2] docs: CLAUDE.md, README, DOCUMENTATION und TECHNICAL-SPEC auf Stand 2.47.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLAUDE.md: Testzahlen, Testdateien, E2E-Specs einzeln in der CI, CI-Rechte, Menüstruktur mit Batch-Import und Beispieldaten, REST- Antworten wie sie sind (format, full, odw:removed), Harvest-Ausschlüsse, npm-overrides, PR-/Release-Ablauf statt „direkt zu main", Erledigt-Liste bis 2.47.1, Versionsstellen. - README: Endpunkte liefern nur Öffentliches, Löschmeldungen, Roadmap. - DOCUMENTATION: Testbaum, npm-Abhängigkeiten, CI-Abschnitt. - TECHNICAL-SPEC: Harvest-Umfang im Umsetzungsstand. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JB1xUQM892bVZ4Yv3MZjvq --- CLAUDE.md | 95 +++++++++++++++++++++++++++++++++++++---------- DOCUMENTATION.md | 19 +++++++++- README.md | 7 +++- TECHNICAL-SPEC.md | 3 +- 4 files changed, 101 insertions(+), 23 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b2fba5ef..07b799e7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,7 +28,7 @@ wp plugin activate open-data-wizard # Entwicklung & Testing ./vendor/bin/phpcs --standard=config/phpcs.xml # Code-Style prüfen ./vendor/bin/phpcbf --standard=config/phpcs.xml includes/ # Auto-fix Style-Fehler -./vendor/bin/phpunit --configuration=config/phpunit.xml # Unit-Tests (243 Tests) +./vendor/bin/phpunit --configuration=config/phpunit.xml # Unit-Tests (500+ Tests) ./vendor/bin/phpstan analyse --configuration=config/phpstan.neon # Static analysis # Spezifische Tests @@ -50,6 +50,10 @@ npm run env:stop - Runs auf **PHP 8.1, 8.2, 8.3** via GitHub Actions (`.github/workflows/ci.yml`) - Sieben Jobs: PHPCS, PHPStan, PHPUnit (drei PHP-Versionen), SHACL, **Release-Paket**, **Übersetzungen**, **End-to-End** +- Der E2E-Job ruft die Spec-Dateien **einzeln** auf (`api-endpoints`, `admin-workflow`, `catalog`, + `samples`). **Eine neue Spec-Datei läuft nicht automatisch mit** — Schritt in `ci.yml` ergänzen. +- `ci.yml` läuft mit `permissions: contents: read` (kein Job schreibt). Releases baut + `.github/workflows/release.yml` mit eigenen Rechten (siehe Version Management). - **Pull Requests** werden geblocked, wenn CI fehlschlägt > **Warum ein eigener Paket-Job?** Alle übrigen Prüfungen laufen gegen das @@ -138,9 +142,16 @@ open-data-wizard/ │ ├── test-catalog.php # Datenkatalog: Attribute, Query-Argumente, Listen-Daten │ ├── test-catalog-filters.php # Datenkatalog: Filter, Facetten-/Such-SQL, Index-Felder │ ├── test-catalog-seo.php # Datenkatalog: Kopfbereich, Canonical, Cache-Action -│ ├── test-rest-delta.php # Delta Endpoint Tests +│ ├── test-rest-delta.php # REST: Delta, Content Negotiation, Ausschlüsse (Beispiele, Passwort) +│ ├── test-batch-import.php # Batch-Import: Parsing, Spalten, Vokabulare, Beispieldateien +│ ├── test-samples.php # Beispieldaten: Laden/Entfernen/Übernehmen, Demo-Migration │ ├── test-cli.php # CLI Commands Tests -│ └── class-stubs.php # WP_Mock Stubs +│ ├── test-validation.php, test-rdf.php, test-vocabulary.php, test-registry-schema.php, +│ │ test-field-catalog.php, test-shacl-shapes.php +│ ├── bootstrap.php # WP_Mock-Bootstrap, Konstanten +│ ├── shacl/ # SHACL-Fixtures + validate.mjs (CI-Job SHACL) +│ └── e2e/ # Playwright: api-endpoints, admin-workflow, catalog, samples +│ └── mu-plugins/odw-e2e-seed.php # Saat + Test-Routen (fixtures, samples-Reset) ├── languages/ │ ├── open-data-wizard.pot # i18n Translation Template │ ├── open-data-wizard-en_US.po # Englische Übersetzung (Quelle) @@ -196,7 +207,7 @@ Externe Harvester rufen /catalog, /datasets/, oder /delta ab | **ODW_Catalog_Filters** | Katalog Stufe 2/3a (Opt-in über `filters`/`search`/`license`): GET-Zustand, Facetten mit Trefferzahlen (GROUP BY über flache Index-Felder `_odw_license_index`/`_odw_format_index`/`_odw_theme_index`/`_odw_publisher_index`/`_odw_keyword_index`, dazu `_odw_period_from`/`_odw_period_to`, `_odw_open_index`, `_odw_hvd_index`), aktive Filter als Chips, LIKE-Suche über `posts_where`, Index-Sync auf `save_post`@30, bei Meta-Änderungen außerhalb (Import, gelöschter Anhang, Revision) am Request-Ende, + Migration in Stapeln über `_odw_index_state` (Versionswechsel, alle 12 h) | `state()`, `apply_to_query()`, `facet_sql()`, `render_form()`, `sync_index()` | | **ODW_Catalog_Seo** | Katalog Stufe 2: HTTP 404 für ungültige `odw_ds` und Seiten hinter dem Ende, Canonical/Titel/robots je Zustand für Core, Yoast, Rank Math und AIOSEO; Action `odw_catalog_changed` für Seiten-Caches | `head_state()`, `canonical_for()`, `send_headers()` | | **ODW_Batch_Import** | CSV/JSON Batch-Import: Parsing, Validierung, Bulk-Insert | `parse_file()`, `validate_row()`, `import_records()` | -| **ODW_CLI** | WP-CLI Befehle (Qualitäts-Recalc, Cache-Clear) | `quality_recalculate()`, `cache_clear()` | +| **ODW_CLI** | WP-CLI Befehle (Qualitäts-Recalc, Cache-Clear, Feld-Referenz, Beispieldaten) | `quality_recalculate()`, `cache_clear()`, `generate_docs()`, `samples_load()`, `samples_remove()`, `samples_status()` | ### Design Patterns @@ -350,13 +361,15 @@ und Datenkatalog (`ODW_Catalog`) teilen sich darüber dieselbe Logik. ### Menü-Struktur ``` WordPress Admin → Datensätze -├── Alle Datensätze (List View mit Spalten) -├── Einstieg (neu in v2.1.4!) +├── Alle Datensätze (List View mit Spalten; Beispiele mit „— Beispieldaten") +├── Neuen Datensatz erstellen (Create Form) +├── Einstieg │ └── Welcome/Intro Page mit: │ - What is Open Data Wizard? │ - How it works (5-step overview) │ - Link: "Neuen Datensatz erstellen" -├── Neue Datensatz (Create Form) +│ - Kasten „Beispieldaten zum Ausprobieren" (laden/entfernen, ODW_Samples) +├── Batch-Import (CSV/JSON, Vorschau, Import als Entwürfe; ebenfalls Beispieldaten-Kasten) ├── Einstellungen (Settings Page) │ ├── Katalog (Title, Publisher) │ ├── Standardwerte (Default Language, License) @@ -409,6 +422,21 @@ In `class-admin.php` Methode `register_introduction_page()`: - URLs: `esc_url_raw()` vor Output (blockt `javascript:`, `data:`) - Meta Values: `esc_attr()`, `esc_html()` bei Output +### Was nicht in die Harvest-Endpunkte gehört +`/catalog`, `/datasets/` und `/delta` liefern nur **öffentliche** Datensätze aus — wie der +Datenkatalog der Website: +- keine Entwürfe, privaten oder **passwortgeschützten** Datensätze (`has_password => false`; + `/datasets/` antwortet mit derselben 404 wie für eine unbekannte ID), +- keine **Beispieldaten** (`_odw_sample`, `meta_query` `NOT EXISTS`) — auch nicht als Löschmeldung. +- Ein nachträglich geschützter Datensatz erscheint in `/delta` unter `odw:removed`. + +Wer eine neue Abfrage auf Datensätze für einen Endpunkt baut, muss diese Ausschlüsse übernehmen. + +### Abhängigkeiten der Testumgebung +npm-Pakete (wp-env, Playwright, SHACL) sind reine Entwicklungswerkzeuge und nicht im Plugin-ZIP. +Sicherheitslücken in transitiven Paketen werden über `overrides` in `package.json` behoben, +danach `npm install --package-lock-only` und `npm audit --package-lock-only` (Ziel: 0 Funde). + --- ## 🌍 Internationalisierung (i18n) @@ -424,7 +452,7 @@ esc_attr__( 'Attribute Text', 'open-data-wizard' ) - **Source language**: German (the `__()` default strings are German) - **POT Template**: `languages/open-data-wizard.pot` - **English**: `languages/open-data-wizard-en_US.po` / `.mo` — overrides the German - source strings on `en_US` installs (336 strings) + source strings on `en_US` installs (rund 750 Einträge) - Loaded via `load_plugin_textdomain()` on `init` hook - The `.mo` is generated without gettext tooling; if strings change, regenerate the `.po`/`.mo` (see the generator approach in the v2.3.0 PR) @@ -447,12 +475,19 @@ esc_attr__( 'Attribute Text', 'open-data-wizard' ) - **test-quality.php** — Quality Scoring: 0-100 Score calculation, 4-level mapping - **test-settings.php** — Settings get/filter methods - **test-shortcode.php** — Shortcode rendering, byte formatting -- **test-rest-delta.php** — Delta endpoint validation, cache logic +- **test-rest-delta.php** — REST: Delta, Cache, Content Negotiation, Ausschlüsse (Beispiele, Passwort) +- **test-catalog.php / test-catalog-filters.php / test-catalog-seo.php** — Frontend-Datenkatalog +- **test-batch-import.php** — Batch-Import inkl. „die Beispieldateien laufen fehlerfrei durch" +- **test-samples.php** — Beispieldaten und Demo-Migration - **test-cli.php** — WP-CLI commands +- Weitere: Validierung, RDF/Turtle, Vokabulare, Feld-Registry, Feld-Katalog, SHACL-Shapes + +**E2E (Playwright, `tests/e2e/`):** läuft in der CI gegen ein deutsches wp-env-WordPress; siehe +Quick Start und CI/CD Pipeline. #### Running Tests ```bash -# Alle 243 Tests +# Alle Tests (500+) ./vendor/bin/phpunit --configuration=config/phpunit.xml # Spezifische Test-Datei @@ -505,14 +540,18 @@ class Test_ODW_Fields extends \WP_Mock\Tools\TestCase { ### Base Namespace All endpoints unter `/wp-json/datenatlas/v1/` +Ausgeliefert werden nur öffentliche Datensätze — keine Entwürfe, passwortgeschützten Datensätze +oder Beispieldaten (siehe „Was nicht in die Harvest-Endpunkte gehört"). + ### GET /catalog -**Alle veröffentlichten Datasets als dcat:Catalog (JSON-LD)** +**Alle öffentlichen Datasets als dcat:Catalog** Query-Parameter: - `page=1`, `per_page=20` — Pagination +- `full=1` — der ganze Katalog in einem Dokument (Harvest-Endpunkt) - `theme=Bildung` — Filter by theme - `license=cc-by` — Filter by license short code -- `format=json|jsonld` — Response format +- `format=jsonld|json|turtle` — Response format; ohne `format` entscheidet der `Accept`-Header Response (JSON-LD): ```json @@ -552,9 +591,10 @@ Query-Parameter: Response: ```json { - "datasets": [ { ... } ], // Geänderte Datasets - "tombstones": [ // Gelöschte Datasets - { "id": "post-id", "deleted": true } + "@type": "odw:DeltaCatalog", + "dcat:dataset": [ { ... } ], // Geänderte öffentliche Datasets (paginiert) + "odw:removed": [ // Papierkorb oder nachträglich passwortgeschützt + { "@id": ".../datasets/123", "@type": "dcat:Dataset", "odw:removedAt": "2026-…" } ] } ``` @@ -617,12 +657,16 @@ Abstände kommen vom Raster des Tab-Panels, nicht mehr von Margins je Feld — ## 📝 Git Workflow & Commits ### Branch Strategy -**WICHTIG (ab v2.1.4):** Alle Commits gehen **direkt zu `main`** +Änderungen laufen über einen Branch und einen Pull Request gegen `main`; gemergt wird erst, wenn +die CI grün ist. Jeder Push auf `main` startet `release.yml`: Ist die Version im Plugin-Header neu, +entstehen Tag `v`, Release mit ZIP (Notizen aus dem obersten CHANGELOG-Eintrag) und der +Branch `release` mit dem Inhalt des ZIPs (Quelle für Git Updater). ```bash # Lokal entwickeln git checkout main git pull origin main +git checkout -b # Änderungen machen # ... edit files ... @@ -638,8 +682,8 @@ Detaillierte Erklärung was sich ändert und warum. https://claude.ai/code/session_01JB1xUQM892bVZ4Yv3MZjvq" -# Pushen zu main -git push origin main +# Pushen und Pull Request öffnen +git push -u origin ``` ### Commit Message Conventions @@ -754,6 +798,11 @@ Known false positives from incomplete Carbon Fields stubs: ## 🚀 Version Management ### Version Bumping +Die Version steht an mehreren Stellen; `bin/verify-package.py` (CI-Job „Release-Paket") prüft, +dass Plugin-Header, `ODW_VERSION` und oberster CHANGELOG-Eintrag übereinstimmen. Außerdem: +`package.json`, `package-lock.json` (zwei Stellen), Versions-Badge in `README.md` und „Current" +unten in dieser Datei. + Update **both** locations: 1. Plugin header in `open-data-wizard.php` ```php @@ -825,6 +874,14 @@ Neue technische Festlegungen gehören dorthin, nicht in README oder CLAUDE.md. - **Content Negotiation (v2.35.0):** Alle drei Endpunkte liefern `jsonld`/`json`/`turtle`; ohne `?format=` entscheidet der `Accept`-Header (q-Werte, `Vary: Accept`). Explizites `?format=` hat Vorrang. +- **Datenkatalog im Frontend (v2.43.0–v2.45.0):** `[odw_catalog]` / Block „Datenkatalog" mit + Detailansicht, Filtern mit Trefferzahlen, Suche, eigenem SEO-Kopf, Herausgeber, Schlagworten, + Zeitraum, „Nur Open Data", „Nur HVD" und Filter-Chips. +- **Batch-Import mit allen wichtigen Feldern (v2.46.0):** 18 weitere Spalten, Vokabular-Auflösung, + 42 vollständige Beispieldatensätze in `samples/`. +- **Beispieldaten statt Demo-Datensatz (v2.47.0):** leerer Start, Laden/Entfernen per Klick oder + WP-CLI, nie in einem Harvest-Endpunkt, im Frontend nur für die Redaktion. +- **Nur öffentliche Datensätze in den Endpunkten, CI mit Leserechten (v2.47.1).** ### ☐ Noch offen / geplant @@ -924,6 +981,6 @@ Neue technische Festlegungen gehören dorthin, nicht in README oder CLAUDE.md. --- -**Zuletzt aktualisiert**: Version 2.39.1 (August 2026) +**Zuletzt aktualisiert**: Version 2.47.1 (November 2026) **Autor**: Open Data Wizard Team (nozilla) **License**: GPL-2.0-or-later diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index af88530e..07038669 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -68,7 +68,12 @@ open-data-wizard/ │ ├── test-catalog-filters.php │ ├── test-catalog-seo.php │ ├── test-rest-delta.php -│ └── test-cli.php +│ ├── test-batch-import.php +│ ├── test-samples.php +│ ├── test-validation.php, test-rdf.php, test-vocabulary.php, … +│ ├── test-cli.php +│ ├── shacl/ # SHACL-Fixtures + validate.mjs +│ └── e2e/ # Playwright-Specs + mu-plugins/odw-e2e-seed.php └── .github/workflows/ci.yml # CI: PHPCS + PHPStan + PHPUnit (PHP 8.1–8.3) ``` @@ -287,6 +292,18 @@ wp open-data-wizard samples remove --yes | [PHPStan](https://phpstan.org/) + WordPress-Stubs | ^2.0 | Statische Analyse Level 6 (dev) | | [WPCS](https://github.com/WordPress/WordPress-Coding-Standards) | ^3.1 | Coding Standards (dev) | +Dazu npm-Pakete für die Testumgebung (wp-env, Playwright, SHACL-Prüfung). Sie landen nicht im +Plugin-ZIP. Sicherheitslücken in ihren transitiven Abhängigkeiten werden über `overrides` in +`package.json` behoben (derzeit `qs`, `body-parser`, `glob`/`minimatch`); `npm audit +--package-lock-only` soll 0 Funde melden. + +### CI + +`.github/workflows/ci.yml` läuft mit `permissions: contents: read` und prüft PHPCS, PHPStan, +PHPUnit (PHP 8.1–8.3), SHACL, das Release-Paket, die Übersetzungen und End-to-End (Playwright gegen +wp-env auf Deutsch). Die E2E-Specs stehen einzeln im Workflow — eine neue Spec-Datei braucht einen +eigenen Schritt. + ### Release bauen Ein installationsfertiges Plugin-ZIP (nur Laufzeit-Dateien inkl. Produktions-`vendor/` mit Carbon diff --git a/README.md b/README.md index 5583cc46..192e6b02 100644 --- a/README.md +++ b/README.md @@ -368,7 +368,9 @@ Diese URLs können bei einer Open-Data-Plattform als Harvest-Quelle eingetragen **Content Negotiation:** Alle drei Endpunkte liefern `jsonld`, `json` und `turtle`. Ohne `?format=` entscheidet der **`Accept`-Header** (mit q-Werten); ein explizites `?format=` hat immer Vorrang. Antworten tragen `Vary: Accept`. -**Delta-Parameter:** `since` (erforderlich, ISO 8601), `page`, `per_page`, `format` — liefert nur Datensätze, die nach dem angegebenen Zeitstempel geändert wurden, plus Tombstones für gelöschte Datensätze +**Delta-Parameter:** `since` (erforderlich, ISO 8601), `page`, `per_page`, `format` — liefert nur Datensätze, die nach dem angegebenen Zeitstempel geändert wurden, plus Löschmeldungen (`odw:removed`) für Datensätze, die seitdem im Papierkorb gelandet sind oder ein Passwort bekommen haben + +**Nur Öffentliches:** Die Endpunkte liefern genau die Datensätze, die auch der Datenkatalog der Website Besuchern zeigt. Entwürfe, private und **passwortgeschützte** Datensätze sowie [Beispieldaten](#-beispieldaten-zum-ausprobieren) erscheinen nicht; `/datasets/` antwortet für sie mit derselben 404 wie für eine unbekannte ID. ### 🌾 Harvesting durch externe Open-Data-Portale @@ -382,7 +384,7 @@ GET https://deine-website.de/wp-json/datenatlas/v1/catalog?full=1&format=turtle GET https://deine-website.de/wp-json/datenatlas/v1/catalog?full=1 → application/ld+json ``` -- **`full=1`** liefert **alle veröffentlichten Datensätze in einem Abruf** (ohne Paginierung) als `dcat:Catalog` → `dcat:Dataset` → `dcat:Distribution` — das Muster, das RDF-Harvester erwarten. +- **`full=1`** liefert **alle öffentlichen Datensätze in einem Abruf** (ohne Paginierung) als `dcat:Catalog` → `dcat:Dataset` → `dcat:Distribution` — das Muster, das RDF-Harvester erwarten. - **`format=turtle`** serialisiert denselben Graphen als **Turtle** (`text/turtle`) — ohne externe RDF-Bibliothek. JSON-LD (`application/ld+json`) und `json` bleiben verfügbar. Harvester, die rein über Header aushandeln, senden stattdessen `Accept: text/turtle`. - Die Datensatz-URIs (`@id`) sind **über Releases stabil** (an die Post-ID gebunden), sodass Harvester Aktualisierungen/Löschungen korrekt zuordnen und keine Duplikate anlegen. @@ -574,6 +576,7 @@ Neue technische Festlegungen gehören in dieses Dokument (nicht in die anwendero - [x] Datenkatalog: Herausgeber, Schlagworte, Zeitraum, „Nur Open Data", „Nur HVD", aktive Filter als Chips (v2.45.0) - [x] Batch-Import mit allen wichtigen Feldern (Zeitraum, Kontakt, Aktualisierung, HVD …) und 42 vollständigen Beispieldatensätzen (v2.46.0) - [x] Beispieldaten mit einem Klick laden und entfernen — nie in einem Harvest-Endpunkt (v2.47.0) +- [x] Harvest-Endpunkte nur mit öffentlichen Datensätzen (auch ohne passwortgeschützte), CI mit Leserechten (v2.47.1) - [x] Einstellungsseite (Catalog-Titel, Defaults, API, Cleanup) - [x] Erweiterte DCAT-AP Felder (Tab 4) - [x] Nativer wp.media Upload-Widget diff --git a/TECHNICAL-SPEC.md b/TECHNICAL-SPEC.md index 11ca66c2..49ccc2a5 100644 --- a/TECHNICAL-SPEC.md +++ b/TECHNICAL-SPEC.md @@ -4,7 +4,7 @@ ## 📌 Umsetzungsstand -Stand: **v2.5.1**. Abgeleitet aus der Analyse des piveau Data Provider Interface (siehe Abschnitt 1). +Stand der Tabelle: **v2.5.1** (DCAT-AP-Phasen). Plugin aktuell: **v2.47.1**. Abgeleitet aus der Analyse des piveau Data Provider Interface (siehe Abschnitt 1). | Phase | Inhalt | Status | |---|---|---| @@ -14,6 +14,7 @@ Stand: **v2.5.1**. Abgeleitet aus der Analyse des piveau Data Provider Interface | **D** | Mehrsprachige Literale (`@language`/`@value`) für title/description/keyword (Output-Tagging, ohne Migration) | ✅ v2.9.0 | | **E** | Multi-Distribution (opt-in, wiederholbare Distributionen) | ✅ v2.22.0 (additiv) | | — | Profi-UX (ausklappbarer „Erweiterte Angaben"-Bereich) ✅ v2.8.0; Hilfe-Tooltips + Live-Vorschau ✅ v2.11.0; Registry-getriebenes Formular/JSON-LD ☐ | teils | +| — | Harvest-Umfang: Endpunkte liefern nur öffentliche Datensätze (keine Entwürfe, privaten, passwortgeschützten oder Beispieldatensätze); nachträglich geschützte als Löschmeldung in `/delta` | ✅ v2.47.0 / 2.47.1 | Details zur phasierten Umsetzungsplanung in Abschnitt 7.