Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,48 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.ht

---

## [2.42.1] — 2026-09-17

Der empfohlene Harvest-Endpunkt lieferte Turtle, das **kein RDF-Parser lesen konnte** — und
zwar seit es den Qualitätsblock gibt. Aufgefallen beim Testen der Live-Ausgabe.

### 🐛 Fixed
- **Ungültiges Turtle im gesamten Katalog.** Die Dimensionsnamen im `odw:qualityScore`-Block
standen als nackte Bezeichner (`findability [ … ]`) da. Ein Prädikat muss in Turtle ein IRI
oder ein Präfixname sein; der Parser bricht an dieser Stelle ab und verwirft **das ganze
Dokument**, nicht nur die eine Zeile. Belegt mit dem echten Abruf einer Installation:
`Unexpected "findability" on line 34` — dasselbe Dokument ohne den Block parst mit 25 Tripeln
sauber. Die Namen tragen jetzt den `odw:`-Präfix.
In JSON-LD fiel derselbe Schlüssel stillschweigend weg (kein Eintrag im `@context`), die
Dimensionen fehlten dort also schon immer in der Ausgabe.
- **Der Serializer kann kein ungültiges Prädikat mehr erzeugen.** `ODW_Rdf` schreibt einen
Schlüssel nur noch aus, wenn er ein IRI oder ein Präfixname ist, und lässt ihn sonst weg. Eine
einzelne verlorene Zusatzangabe ist ungleich harmloser als ein Katalog, den niemand einlesen
kann. Ein vollständiges IRI als Schlüssel wird dabei korrekt in spitze Klammern gesetzt.
- **Die Datensatz-Fixtures hatten keine `@prefix`-Zeilen.** Sie wurden ohne `@context`
serialisiert und waren damit ebenfalls unparsbar — was nie auffiel, siehe unten. Der neue
`ODW_Rest_API::build_dataset_document()` setzt den Kontext an einer Stelle, die Fixture und
Endpunkt gemeinsam nutzen.

### 🧪 Warum das grün durch die CI kam
Der SHACL-Job erzeugte zwar Turtle-Dateien, **validierte aber nur die JSON-LD-Fixtures** — die
`.ttl`-Dateien wurden nie geparst. Dazu kam, dass der Qualitätsblock in den Fixtures überhaupt
nicht vorkam, weil der Generator den Filter `odw_dataset_jsonld` nicht durchlief. Die einzige
kaputte Stelle lag damit exakt im blinden Fleck. Beides ist behoben:
- `tests/shacl/validate.mjs` prüft jetzt **beide Serialisierungen**; ein Syntaxfehler in der
Turtle-Datei lässt den Lauf scheitern.
- Die maximale Fixture trägt einen gespeicherten MQA-Stand und durchläuft
`ODW_Quality::append_to_jsonld()` — der Qualitätsblock steht also in der geprüften Ausgabe.
- Zwei Unit-Tests halten die Stellen fest: die Dimensionsnamen als Präfixnamen und das
Verwerfen ungültiger Prädikate im Serializer. Der Serializer-Schutz allein hätte den Fehler
in ein stilles `odw:dimensions []` verwandelt — deshalb beides.
- Ein bestehender Test *hatte* den Qualitätsblock geprüft — und die falsche Form festgeschrieben:
Er verlangte den Schlüssel `findability`. Eine Zusicherung, die den Ist-Zustand abbildet statt
einer Anforderung, meldet den Fehler nicht, sondern hält ihn fest. Die Zusicherung steht jetzt
auf `odw:findability`.

---

## [2.42.0] — 2026-09-17

Beim Testen des Harvestings kam ein nacktes `rest_no_route` zurück — ohne jeden Hinweis, woran
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,7 @@ Update **both** locations:
- MINOR: New features (backward-compatible)
- PATCH: Bug fixes only

Current: **v2.42.0**
Current: **v2.42.1**

---

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<p align="center">
<img src="https://img.shields.io/github/license/daimpad/OpenDataWizard?style=flat-square&color=03FF9A&labelColor=101010&label=Lizenz" alt="Lizenz">
<img src="https://img.shields.io/badge/Version-2.42.0-03FF9A?style=flat-square&labelColor=101010" alt="Version">
<img src="https://img.shields.io/badge/Version-2.42.1-03FF9A?style=flat-square&labelColor=101010" alt="Version">
<img src="https://img.shields.io/badge/DCAT--AP-3.0-03FF9A?style=flat-square&labelColor=101010" alt="DCAT-AP 3.0">
<img src="https://img.shields.io/badge/PHP-%3E%3D%208.1-03FF9A?style=flat-square&labelColor=101010&logo=php&logoColor=white" alt="PHP >= 8.1">
<img src="https://img.shields.io/badge/WordPress-6.4%2B-03FF9A?style=flat-square&labelColor=101010&logo=wordpress&logoColor=white" alt="WordPress 6.4+">
Expand Down
7 changes: 6 additions & 1 deletion includes/class-quality.php
Original file line number Diff line number Diff line change
Expand Up @@ -677,9 +677,14 @@ public static function append_to_jsonld( array $dataset, int $post_id ): array {
return $dataset;
}

// Der Dimensionsname muss ein Präfixname sein, kein nackter Bezeichner:
// In JSON-LD fällt ein Schlüssel ohne Zuordnung im @context stillschweigend
// weg, in Turtle ergäbe er ein ungültiges Prädikat — und damit ein Dokument,
// das kein Harvester mehr liest. Bis v2.42.0 stand hier „findability" statt
// „odw:findability", wodurch der gesamte Turtle-Katalog unparsbar war.
$dimensions = array();
foreach ( (array) ( $quality['dimensions'] ?? array() ) as $dim => $data ) {
$dimensions[ $dim ] = array(
$dimensions[ 'odw:' . $dim ] = array(
'odw:score' => (int) ( $data['achieved'] ?? 0 ),
'odw:assessable' => (int) ( $data['assessable'] ?? 0 ),
'odw:maxScore' => (int) ( $data['max'] ?? 0 ),
Expand Down
41 changes: 40 additions & 1 deletion includes/class-rdf.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,15 @@ private static function render_predicates( array $node, array &$subjects, array
continue;
}

// Ein Prädikat muss in Turtle ein IRI oder ein Präfixname sein. Ein
// nackter Bezeichner ist beides nicht — und er beschädigt nicht bloß
// seine eigene Zeile, sondern macht das ganze Dokument unparsbar.
// Lieber die eine Aussage auslassen als den Katalog unbrauchbar machen.
$predicate = self::predicate( (string) $key );
if ( '' === $predicate ) {
continue;
}

$object = self::render_object( $value, $subjects, $seen );

// Eine leere Liste (z. B. ein Katalog ohne veröffentlichte Datensätze)
Expand All @@ -150,7 +159,7 @@ private static function render_predicates( array $node, array &$subjects, array
continue;
}

$parts[] = $key . ' ' . $object;
$parts[] = $predicate . ' ' . $object;
}

return $parts;
Expand Down Expand Up @@ -259,6 +268,36 @@ private static function has_properties( array $node ): bool {
return false;
}

/**
* Einen Schlüssel als Turtle-Prädikat ausgeben — oder '' , wenn er keines sein kann.
*
* Zulässig sind ein absolutes IRI (kommt in spitze Klammern) und ein
* Präfixname wie `dcat:theme`. Bewusst enger gefasst als die Turtle-Grammatik
* (keine Escape-Sequenzen, kein Punkt am Ende des lokalen Teils): Was hier
* durchfällt, stammt aus einem Fehler im Aufbau des Dokuments und gehört
* nicht in die Ausgabe.
*
* @param string $key Schlüssel aus dem JSON-LD-Knoten.
* @return string Prädikat oder '' , wenn der Schlüssel keines ergibt.
*/
private static function predicate( string $key ): string {
$key = trim( $key );

if ( '' === $key ) {
return '';
}

if ( preg_match( '#^[a-zA-Z][a-zA-Z0-9+.-]*://#', $key ) ) {
return '<' . self::escape_iri( $key ) . '>';
}

if ( preg_match( '/^[A-Za-z][A-Za-z0-9._-]*:[A-Za-z0-9_](?:[A-Za-z0-9._-]*[A-Za-z0-9_-])?$/', $key ) ) {
return $key;
}

return '';
}

/**
* Render a type/predicate term: a CURIE (`dcat:Dataset`) stays as-is, a full
* IRI becomes `<IRI>`.
Expand Down
20 changes: 16 additions & 4 deletions includes/class-rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,21 @@ public static function get_catalog( WP_REST_Request $request ): WP_REST_Response
return self::catalog_response( $catalog, $total, $pages, $format, 'MISS', $cache_key );
}

/**
* Setzt den @context vor einen Datensatz-Knoten.
*
* Ausgelagert, damit die SHACL-Fixtures denselben Weg gehen wie die Antwort
* des Endpunkts. Vorher baute die Fixture den Knoten ohne Kontext — die
* erzeugte Turtle-Datei hatte dadurch keine @prefix-Zeilen und war gar nicht
* parsbar, was niemandem auffiel, weil die Prüfung nur das JSON-LD ansah.
*
* @param array<string, mixed> $dataset Dataset JSON-LD node.
* @return array<string, mixed>
*/
public static function build_dataset_document( array $dataset ): array {
return array_merge( array( '@context' => self::JSONLD_CONTEXT ), $dataset );
}

/**
* Assemble the dcat:Catalog JSON-LD document from the given dataset nodes.
*
Expand Down Expand Up @@ -617,10 +632,7 @@ public static function get_dataset( WP_REST_Request $request ): WP_REST_Response
);
}

$body = array_merge(
array( '@context' => self::JSONLD_CONTEXT ),
$dataset
);
$body = self::build_dataset_document( $dataset );

set_transient( $cache_key, $body, self::get_cache_ttl() );

Expand Down
4 changes: 2 additions & 2 deletions open-data-wizard.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.42.0
* Version: 2.42.1
* Requires at least: 6.4
* Requires PHP: 8.1
* Author: nozilla
Expand All @@ -26,7 +26,7 @@
exit;
}

define( 'ODW_VERSION', '2.42.0' );
define( 'ODW_VERSION', '2.42.1' );
define( 'ODW_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'ODW_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'ODW_PLUGIN_FILE', __FILE__ );
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "open-data-wizard",
"version": "2.42.0",
"version": "2.42.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",
Expand Down
47 changes: 47 additions & 0 deletions tests/shacl/generate-fixtures.php
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ function set_transient( string $key, $value, int $expiry = 0 ): bool { // phpcs:
require_once ODW_PLUGIN_DIR . 'includes/class-fields.php';
require_once ODW_PLUGIN_DIR . 'includes/class-rdf.php';
require_once ODW_PLUGIN_DIR . 'includes/class-rest-api.php';
require_once ODW_PLUGIN_DIR . 'includes/class-quality.php';

// ----------------------------------------------------------
// Fixture data definitions
Expand Down Expand Up @@ -284,6 +285,45 @@ function fixture_maximal(): array {
'odw_conforms_to' => 'https://schema.org/Dataset',
'odw_provenance' => 'Daten aus amtlichen Erhebungen gemäß Bundesstatistikgesetz',
'odw_access_rights' => 'http://publications.europa.eu/resource/authority/access-right/PUBLIC',

// Gespeicherter MQA-Stand: ODW_Quality::append_to_jsonld() hängt daraus den
// odw:qualityScore-Block an. Der gehört in die Fixture, weil er in echten
// Antworten steht — und weil genau dieser Block in v2.42.0 ungültiges
// Turtle erzeugte, ohne dass eine Prüfung es bemerkt hätte.
'_odw_mqa' => array(
'achieved' => 165,
'assessable' => 295,
'max' => 405,
'rating' => 'good',
'calculated_at' => '2026-09-17 12:00:00',
'dimensions' => array(
'findability' => array(
'achieved' => 30,
'assessable' => 100,
'max' => 100,
),
'accessibility' => array(
'achieved' => 0,
'assessable' => 20,
'max' => 100,
),
'interoperability' => array(
'achieved' => 70,
'assessable' => 80,
'max' => 110,
),
'reusability' => array(
'achieved' => 55,
'assessable' => 75,
'max' => 75,
),
'contextuality' => array(
'achieved' => 10,
'assessable' => 20,
'max' => 20,
),
),
),
'odw_availability' => 'http://publications.europa.eu/resource/authority/planned-availability/STABLE',

// Distribution fields.
Expand Down Expand Up @@ -387,6 +427,13 @@ function generate_fixture( string $name, array $meta, bool $is_catalog = false )
$doc = build_catalog_fixture( $datasets );
} else {
$doc = odw_build_dataset_jsonld( 1 );
if ( null !== $doc ) {
// Denselben Weg gehen wie der Endpunkt: erst der Qualitätsblock (den
// das Plugin über den Filter odw_dataset_jsonld anhängt), dann der
// @context. Ohne den Kontext hätte die Turtle-Datei keine Präfixe.
$doc = ODW_Quality::append_to_jsonld( $doc, 1 );
$doc = ODW_Rest_API::build_dataset_document( $doc );
}
}

if ( null === $doc ) {
Expand Down
17 changes: 16 additions & 1 deletion tests/shacl/validate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,20 @@ const TIERS = {
};

// Fixtures to validate.
//
// Beide Serialisierungen, nicht nur JSON-LD: Die Turtle-Ausgabe ist das, was der
// Harvest-Endpunkt als empfohlenes Format liefert, und sie hatte bis v2.42.0
// überhaupt keine Prüfung. Ein ungültiges Prädikat macht in Turtle das *ganze*
// Dokument unparsbar, während JSON-LD denselben Schlüssel stillschweigend
// verwirft — grüne JSON-LD-Prüfung bei kaputtem Turtle ist also kein Widerspruch,
// sondern genau der Fall, der eintrat.
const FIXTURES = [
'build/shacl/dataset-minimal.jsonld',
'build/shacl/dataset-maximal.jsonld',
'build/shacl/catalog.jsonld',
'build/shacl/dataset-minimal.ttl',
'build/shacl/dataset-maximal.ttl',
'build/shacl/catalog.ttl',
];

/**
Expand Down Expand Up @@ -280,7 +290,12 @@ async function main() {
}

console.log(`Validating ${fixturePath}...`);
const dataGraph = await loadJsonLd(fixturePath);
// Ein Syntaxfehler in der Turtle-Datei wirft hier — und soll das auch:
// Ein Dokument, das kein Parser liest, ist schlimmer als eines mit
// SHACL-Verstößen.
const dataGraph = fixturePath.endsWith('.ttl')
? await loadRdf(fixturePath)
: await loadJsonLd(fixturePath);
const violationCount = await validateFixture(dataGraph, shapesGraph, fixturePath, allowlist);
totalViolations += violationCount;
}
Expand Down
60 changes: 59 additions & 1 deletion tests/test-quality.php
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,11 @@ public function test_append_to_jsonld_adds_mqa_data(): void {
$this->assertSame( 200, $result['odw:qualityScore']['odw:score'] );
$this->assertSame( 'good', $result['odw:qualityScore']['odw:rating'] );
$this->assertSame( 405, $result['odw:qualityScore']['odw:maxScore'] );
$this->assertArrayHasKey( 'findability', $result['odw:qualityScore']['odw:dimensions'] );
// Mit Präfix: Ohne ihn ist der Dimensionsname in Turtle kein gültiges
// Prädikat und in JSON-LD kein bekannter Begriff. Diese Zusicherung stand
// bis v2.42.0 auf dem nackten Namen und hat den Fehler dadurch festgehalten,
// statt ihn zu melden.
$this->assertArrayHasKey( 'odw:findability', $result['odw:qualityScore']['odw:dimensions'] );
}

/**
Expand Down Expand Up @@ -658,4 +662,58 @@ public function test_calculate_scores_a_fresh_form_at_zero(): void {
$this->assertSame( 0, $result['score'] );
$this->assertGreaterThan( 0, $result['assessable'], 'Die Metriken bleiben bewertbar, sie sind nur nicht erfüllt.' );
}

/**
* Die Dimensionsnamen im Qualitätsblock müssen Präfixnamen sein.
*
* Bis v2.42.0 standen dort nackte Bezeichner („findability"). In JSON-LD fällt
* ein Schlüssel ohne Zuordnung im @context stillschweigend weg, in Turtle
* ergibt er ein ungültiges Prädikat — das gesamte Dokument war damit für
* Harvester unlesbar. Der Serializer wirft solche Schlüssel inzwischen weg,
* das rettet aber nur die Syntax: Ohne diesen Test verschwänden die
* Dimensionen wieder unbemerkt aus der Ausgabe.
*/
public function test_quality_dimensions_use_prefixed_predicates(): void {
$this->load_class();

\WP_Mock::userFunction( 'get_post_meta' )->andReturn(
array(
'achieved' => 165,
'assessable' => 295,
'max' => 405,
'rating' => 'good',
'calculated_at' => '2026-09-17 12:00:00',
'dimensions' => array(
'findability' => array(
'achieved' => 30,
'assessable' => 100,
'max' => 100,
),
'accessibility' => array(
'achieved' => 0,
'assessable' => 20,
'max' => 100,
),
),
)
);

$dataset = ODW_Quality::append_to_jsonld( array( '@type' => 'dcat:Dataset' ), 42 );

$this->assertArrayHasKey( 'odw:qualityScore', $dataset );

$dimensionen = $dataset['odw:qualityScore']['odw:dimensions'];
$this->assertNotEmpty( $dimensionen );

foreach ( array_keys( $dimensionen ) as $schluessel ) {
$this->assertMatchesRegularExpression(
'/^odw:[A-Za-z]+$/',
(string) $schluessel,
'Dimensionsname ist kein Präfixname und damit kein gültiges Prädikat: ' . $schluessel
);
}

$this->assertArrayHasKey( 'odw:findability', $dimensionen );
$this->assertSame( 30, $dimensionen['odw:findability']['odw:score'] );
}
}
Loading