From 221c9ada1f840f46c9f3f10391a02798e0f38c38 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 13:27:15 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20MQA=20quality=20scoring=20=E2=80=94=20P?= =?UTF-8?q?hase=203a=20URL=20reachability=20(opt-in,=20v2.15.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the optional URL-reachability metrics of the MQA accessibility dimension. - New setting "URL-Erreichbarkeit prüfen" (Quality (MQA) section, default OFF). When enabled, dcat:accessURL and dcat:downloadURL are checked via HTTP HEAD (GET fallback on 405/501) on save; status 200–399 counts as reachable. - Results are cached for 24h in a per-URL transient, so repeated saves don't re-request. Short 5s timeout. - With the setting on, the assessable maximum rises from ~295 to ~375 of 405; only DCAT-AP SHACL (30) remains "not assessed". Privacy/perf: opt-in, requests go only to the dataset's own URLs (no third-party service). Meta-box "not assessed" hint made phase-agnostic (points to the "–" markers). i18n: 4 new strings; en_US.mo recompiled. Version 2.15.0. Tests: 151 pass (2 added: reachability cache-hit + 2xx mapping), PHPStan clean, PHPCS exit 0. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JB1xUQM892bVZ4Yv3MZjvq --- CHANGELOG.md | 26 +++++++++++ README.md | 2 +- includes/class-quality.php | 69 +++++++++++++++++++++++++++- includes/class-settings.php | 33 +++++++++++++ languages/open-data-wizard-en_US.mo | Bin 48722 -> 49479 bytes languages/open-data-wizard-en_US.po | 12 +++++ open-data-wizard.php | 4 +- tests/test-quality.php | 46 +++++++++++++++++++ 8 files changed, 187 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31d1d2c..8f2a257 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,32 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.ht --- +## [2.15.0] — 2026-07-07 + +MQA-Qualitäts-Scoring **Phase 3a**: optionale URL-Erreichbarkeitsprüfung. + +### ✨ Added — URL-Erreichbarkeit (opt-in) +- Neue Einstellung **„URL-Erreichbarkeit prüfen"** (Bereich *Qualitätsprüfung (MQA)*, + standardmäßig deaktiviert). Ist sie aktiv, prüft das Plugin `dcat:accessURL` und + `dcat:downloadURL` beim Speichern per **HTTP HEAD** (mit GET-Fallback bei 405/501) auf + Erreichbarkeit (Statuscode 200–399). +- Ergebnisse werden **24 Stunden** als Transient zwischengespeichert (Cache-Key je URL), + sodass wiederholte Speichervorgänge keine erneuten Requests auslösen. +- Damit steigt das bewertbare Maximum bei aktivierter Prüfung von ~295 auf **~375 von 405**. + Es verbleibt nur die **DCAT-AP-SHACL-Konformität** (30 P) als „nicht bewertet". + +### 🔒 Datenschutz/Performance +- Die Prüfung ist **opt-in** und sendet ausgehende Anfragen ausschließlich an die im Datensatz + hinterlegten URLs (kein Fremddienst). Kurzer Timeout (5 s) + 24h-Cache. + +### 🧾 i18n +- 4 neue UI-Strings; `en_US.mo` neu kompiliert. + +### ✅ Tests +- Erreichbarkeitslogik: Cache-Hit (kein Request) und 2xx→erreichbar (mit Caching). + +--- + ## [2.14.0] — 2026-07-07 MQA-Qualitäts-Scoring **Phase 2**: Vokabular-Prüfungen (offline). Siehe `docs/MQA-KONZEPT.md`. diff --git a/README.md b/README.md index 18ca9bb..1b46ea0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ![PHP Version](https://img.shields.io/badge/PHP-%3E%3D%208.1-8892BF?style=flat-square&logo=php&logoColor=white) ![WordPress](https://img.shields.io/badge/WordPress-compatible-21759B?style=flat-square&logo=wordpress&logoColor=white) ![DCAT-AP](https://img.shields.io/badge/DCAT--AP-3.0-brightgreen?style=flat-square) -![Version](https://img.shields.io/badge/Version-2.14.0-brightgreen?style=flat-square) +![Version](https://img.shields.io/badge/Version-2.15.0-brightgreen?style=flat-square) ![PRs Welcome](https://img.shields.io/badge/PRs-willkommen-brightgreen?style=flat-square) 📖 [Dokumentation](DOCUMENTATION.md) · 📐 [Technische Spezifikation](TECHNICAL-SPEC.md) · 📝 [Changelog](CHANGELOG.md) · 🛡️ [Security](SECURITY.md) · ⚖️ [Lizenz](LICENSE) diff --git a/includes/class-quality.php b/includes/class-quality.php index 84dc18a..6756788 100644 --- a/includes/class-quality.php +++ b/includes/class-quality.php @@ -200,10 +200,23 @@ private static function evaluate_metric( array $metric, \WP_Post $post ): ?bool return self::check_vocab_metric( (string) $metric['check'], $post ); } - // Erreichbarkeits- und SHACL-Prüfungen folgen in Phase 3. + if ( 'reachable' === $type && self::url_checks_enabled() ) { + return self::check_reachable_metric( (string) $metric['check'], $post ); + } + + // SHACL (und Erreichbarkeit bei deaktivierter Einstellung) folgt in Phase 3+. return null; } + /** + * Ist die opt-in-URL-Erreichbarkeitsprüfung aktiviert? + * + * @return bool + */ + private static function url_checks_enabled(): bool { + return class_exists( 'ODW_Settings' ) && (bool) ODW_Settings::get( 'mqa_check_urls' ); + } + /** * „Ist die Eigenschaft gesetzt?"-Prüfung je Metrik. * @@ -361,6 +374,58 @@ private static function license_in_vocab( string $uri ): bool { return isset( $extended[ $uri ] ); } + /** + * „Ist die referenzierte URL erreichbar?"-Prüfung (MQA Phase 3, opt-in). + * + * @param string $check Check-Schlüssel (access_url | download_url). + * @param \WP_Post $post Dataset post object. + * @return bool True wenn die URL per HTTP-HEAD einen 2xx/3xx-Status liefert. + */ + private static function check_reachable_metric( string $check, \WP_Post $post ): bool { + $field = 'download_url' === $check ? 'odw_download_url' : 'odw_access_url'; + $url = trim( (string) carbon_get_post_meta( $post->ID, $field ) ); + + if ( '' === $url || ! preg_match( '#^https?://#i', $url ) ) { + return false; + } + + return self::url_is_reachable( $url ); + } + + /** + * Prüft die Erreichbarkeit einer URL per HTTP HEAD (mit GET-Fallback), 24h gecacht. + * + * @param string $url Zu prüfende URL. + * @return bool True bei Statuscode 200–399. + */ + private static function url_is_reachable( string $url ): bool { + $cache_key = 'odw_mqa_reach_' . md5( $url ); + $cached = get_transient( $cache_key ); + if ( '1' === $cached || '0' === $cached ) { + return '1' === $cached; + } + + $args = array( + 'timeout' => 5, + 'redirection' => 3, + 'user-agent' => 'OpenDataWizard-MQA/1.0', + ); + + $response = wp_remote_head( $url, $args ); + $code = is_wp_error( $response ) ? 0 : (int) wp_remote_retrieve_response_code( $response ); + + // Manche Server lehnen HEAD ab (405/501) — dann ein leichtgewichtiges GET versuchen. + if ( 405 === $code || 501 === $code || 0 === $code ) { + $response = wp_remote_get( $url, $args ); + $code = is_wp_error( $response ) ? 0 : (int) wp_remote_retrieve_response_code( $response ); + } + + $ok = $code >= 200 && $code < 400; + set_transient( $cache_key, $ok ? '1' : '0', DAY_IN_SECONDS ); + + return $ok; + } + /** * Ermittelt die MQA-Bewertungsstufe aus erreichten/bewertbaren Punkten. * Die MQA-Schwellen (351/221/121 von 405) werden proportional auf das @@ -587,7 +652,7 @@ public static function render_meta_box( \WP_Post $post ): void { echo esc_html( sprintf( /* translators: %d: number of points not yet assessed */ - __( '%d Punkte werden derzeit nicht bewertet (URL-Erreichbarkeit und DCAT-AP-SHACL-Prüfung folgen).', 'open-data-wizard' ), + __( '%d Punkte konnten nicht automatisch bewertet werden (in der Tabelle mit „–" markiert).', 'open-data-wizard' ), $not_assessed ) ); diff --git a/includes/class-settings.php b/includes/class-settings.php index 2066702..db96203 100644 --- a/includes/class-settings.php +++ b/includes/class-settings.php @@ -166,6 +166,16 @@ static function (): void { add_settings_field( 'cache_ttl', __( 'Cache-Laufzeit (Sekunden)', 'open-data-wizard' ), array( self::class, 'field_cache_ttl' ), 'odw-settings', 'odw_section_api' ); + // --- Qualität (MQA) --- + add_settings_section( + 'odw_section_quality', + __( 'Qualitätsprüfung (MQA)', 'open-data-wizard' ), + null, + 'odw-settings' + ); + + add_settings_field( 'mqa_check_urls', __( 'URL-Erreichbarkeit prüfen', 'open-data-wizard' ), array( self::class, 'field_mqa_check_urls' ), 'odw-settings', 'odw_section_quality' ); + // --- Deinstallation --- add_settings_section( 'odw_section_uninstall', @@ -290,6 +300,27 @@ class="small-text" + +

+ +

+ '', 'cache_ttl' => 300, 'delete_on_uninstall' => '0', + 'mqa_check_urls' => '0', ); } } diff --git a/languages/open-data-wizard-en_US.mo b/languages/open-data-wizard-en_US.mo index 8dadac79acf0f5ad25aeb8db8679bc9cde2a2bd6..83e16473a79b9c7d543bfbc04c88d83c55632da0 100644 GIT binary patch delta 8803 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 delta 8059 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 P%XaK73@CGsR#f>PVY6*? diff --git a/languages/open-data-wizard-en_US.po b/languages/open-data-wizard-en_US.po index e7cf5ab..bce0e5a 100644 --- a/languages/open-data-wizard-en_US.po +++ b/languages/open-data-wizard-en_US.po @@ -18,6 +18,9 @@ msgstr " created and are then available for editing." msgid "%1$d von %2$d Pflichtangaben ausgefüllt" msgstr "%1$d of %2$d required fields completed" +msgid "%d Punkte konnten nicht automatisch bewertet werden (in der Tabelle mit „–\" markiert)." +msgstr "%d points could not be assessed automatically (marked with “–” in the table)." + msgid "%d Punkte werden derzeit nicht bewertet (URL-Erreichbarkeit und DCAT-AP-SHACL-Prüfung folgen)." msgstr "%d points are currently not assessed (URL reachability and DCAT-AP SHACL checks are coming)." @@ -1035,6 +1038,9 @@ msgstr "Keywords (dcat:keyword)" msgid "Sekunden" msgstr "Seconds" +msgid "Sendet ausgehende Anfragen an die angegebenen Datensatz-URLs. Ergebnisse werden 24 Stunden zwischengespeichert. Standardmäßig deaktiviert." +msgstr "Sends outbound requests to the given dataset URLs. Results are cached for 24 hours. Disabled by default." + msgid "Shortcode" msgstr "Shortcode" @@ -1143,6 +1149,9 @@ msgstr "URL of the project website or data portal with further information about msgid "URL zur Beschreibung des Qualitätssicherungs-Prozesses (optional)." msgstr "URL describing the quality-assurance process (optional)." +msgid "URL-Erreichbarkeit prüfen" +msgstr "Check URL reachability" + msgid "Um welchen Typ von Datensatz handelt es sich?" msgstr "What type of dataset is this?" @@ -1413,6 +1422,9 @@ msgstr "Access & further discoverability" msgid "Zugriff verweigert." msgstr "Access denied." +msgid "Zugriffs- und Download-URLs beim Speichern per HTTP-HEAD auf Erreichbarkeit prüfen (MQA-Zugänglichkeit, +80 Punkte)." +msgstr "Check access and download URLs for reachability via HTTP HEAD on save (MQA accessibility, +80 points)." + msgid "Zugriffs-Klassifikation des Datensatzes. Beispiel: Öffentlich, Eingeschränkt, Nicht öffentlich" msgstr "Access classification of the dataset. Example: Public, Restricted, Non-public" diff --git a/open-data-wizard.php b/open-data-wizard.php index f082152..7eb9357 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.14.0 + * Version: 2.15.0 * Requires at least: 6.4 * Requires PHP: 8.1 * Author: nozilla @@ -26,7 +26,7 @@ exit; } -define( 'ODW_VERSION', '2.14.0' ); +define( 'ODW_VERSION', '2.15.0' ); define( 'ODW_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'ODW_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); define( 'ODW_PLUGIN_FILE', __FILE__ ); diff --git a/tests/test-quality.php b/tests/test-quality.php index 8121d26..d46972a 100644 --- a/tests/test-quality.php +++ b/tests/test-quality.php @@ -403,4 +403,50 @@ public function test_append_to_jsonld_skips_when_no_data(): void { $this->assertArrayNotHasKey( 'odw:qualityScore', $result ); } + + // ------------------------------------------------------------------------- + // url_is_reachable() — MQA Phase 3 (opt-in reachability) + // ------------------------------------------------------------------------- + + /** + * Invokes the private static url_is_reachable() helper. + * + * @param string $url URL to check. + * @return bool + */ + private function call_url_is_reachable( string $url ): bool { + $ref = new \ReflectionMethod( 'ODW_Quality', 'url_is_reachable' ); + $ref->setAccessible( true ); + return (bool) $ref->invoke( null, $url ); + } + + /** + * A cached negative result short-circuits without a network request. + */ + public function test_url_is_reachable_uses_cached_result(): void { + $this->load_class(); + + \WP_Mock::userFunction( 'get_transient' )->andReturn( '0' ); + + $this->assertFalse( $this->call_url_is_reachable( 'https://example.com/data.csv' ) ); + } + + /** + * A 200 HEAD response marks the URL reachable and caches the result. + */ + public function test_url_is_reachable_maps_2xx_to_true(): void { + $this->load_class(); + + if ( ! defined( 'DAY_IN_SECONDS' ) ) { + define( 'DAY_IN_SECONDS', 86400 ); + } + + \WP_Mock::userFunction( 'get_transient' )->andReturn( false ); + \WP_Mock::userFunction( 'wp_remote_head' )->andReturn( array( 'response' => array( 'code' => 200 ) ) ); + \WP_Mock::userFunction( 'is_wp_error' )->andReturn( false ); + \WP_Mock::userFunction( 'wp_remote_retrieve_response_code' )->andReturn( 200 ); + \WP_Mock::userFunction( 'set_transient' )->once()->andReturn( true ); + + $this->assertTrue( $this->call_url_is_reachable( 'https://example.com/data.csv' ) ); + } }