__('Not AI-generated / no label', 'aiactify-compliance'), 'ai' => __('AI-generated', 'aiactify-compliance'), 'ai_assisted' => __('AI-assisted (human edited)', 'aiactify-compliance'), ); } /* ───────────────────────────────────────────── * Meta box * ───────────────────────────────────────────── */ function aiactify_add_meta_box() { $post_types = get_post_types(array('public' => true)); unset($post_types['attachment']); foreach ($post_types as $post_type) { add_meta_box( 'aiactify_label_box', __('AI Content Label (EU AI Act)', 'aiactify-compliance'), 'aiactify_render_meta_box', $post_type, 'side', 'default' ); } } add_action('add_meta_boxes', 'aiactify_add_meta_box'); function aiactify_render_meta_box($post) { $current = get_post_meta($post->ID, '_aiactify_ai_status', true); wp_nonce_field('aiactify_save_label', 'aiactify_label_nonce'); echo '
' . esc_html__('Adds a visible label and machine-readable metadata (Schema.org / IPTC) to this post.', 'aiactify-compliance') . '
'; foreach (aiactify_valid_statuses() as $value => $label) { printf( '', esc_attr($value), checked($current, $value, false), esc_html($label) ); } echo '' . esc_html__('Transparency obligations under Art. 50 EU AI Act apply from 2 August 2026.', 'aiactify-compliance') . '
'; } function aiactify_save_meta_box($post_id) { if (!isset($_POST['aiactify_label_nonce']) || !wp_verify_nonce(sanitize_key($_POST['aiactify_label_nonce']), 'aiactify_save_label')) { return; } if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } if (!current_user_can('edit_post', $post_id)) { return; } $status = isset($_POST['aiactify_ai_status']) ? sanitize_key($_POST['aiactify_ai_status']) : ''; if (!array_key_exists($status, aiactify_valid_statuses())) { $status = ''; } if ('' === $status) { delete_post_meta($post_id, '_aiactify_ai_status'); } else { update_post_meta($post_id, '_aiactify_ai_status', $status); } aiactify_sync_status_to_platform($post_id, $status); } add_action('save_post', 'aiactify_save_meta_box'); /** * v2.1: Mirror the editor's decision to the AIActify platform (optional). * * Only runs when the site is connected (Site Key + API Key configured). * Blocking with a 3 s timeout: the response is the only place where a wrong * key, a missing permission or an unknown page ever becomes visible. * Status mapping: 'ai' stays AI; 'ai_assisted' (AI-edited human content) maps * to 'ai_modified' — the EU publishes a dedicated "AI MODIFIED" label for it. * Note: the Art. 50(4) editorial carve-out only covers TEXT on matters of * public interest, so AI-edited images/videos still need a visible label * (Commission guidelines, 20 July 2026). No selection maps to 'pending'. */ function aiactify_sync_status_to_platform($post_id, $status) { if (wp_is_post_revision($post_id) || 'publish' !== get_post_status($post_id)) { return; } $site_id = get_option('aiactify_site_id'); $api_key = get_option('aiactify_api_key'); if (empty($site_id) || empty($api_key)) { return; // Not connected — local labels work standalone. } $map = array( 'ai' => 'ai', 'ai_assisted' => 'ai_modified', '' => 'pending', ); if (!isset($map[$status])) { return; } // Blockierend mit kurzem Timeout statt fire-and-forget: mit // 'blocking' => false wird die Antwort nie gelesen — ein falscher API Key // (401), ein fehlendes Scope (403) oder ein "updated: 0" (URL im Backend // unbekannt) blieben damit voellig unsichtbar. Der Redakteur setzte sein // Label, sah es lokal korrekt, und Dashboard, Audit-Trail und PDF-Report // blieben stumm falsch. Bei einem Compliance-Produkt ist genau das der // teuerste Fehler. 3 s Timeout auf dem Speichern-Pfad sind der Preis dafuer. $response = wp_remote_post(AIACTIFY_API_BASE . '/wordpress/validate', array( 'timeout' => 3, 'blocking' => true, 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . sanitize_text_field($api_key), ), 'body' => wp_json_encode(array( 'siteKey' => sanitize_text_field($site_id), 'url' => get_permalink($post_id), 'status' => $map[$status], )), )); $error = aiactify_describe_sync_result($response); if (null === $error) { delete_option('aiactify_last_sync_error'); } else { update_option('aiactify_last_sync_error', array( 'message' => $error, 'post_id' => (int) $post_id, 'time' => time(), ), false); } } /** * Uebersetzt die Sync-Antwort in eine Meldung, die einem Redakteur etwas sagt. * Rueckgabe null = alles in Ordnung. */ function aiactify_describe_sync_result($response) { if (is_wp_error($response)) { return sprintf( /* translators: %s: network error message */ __('The AIActify platform could not be reached (%s). The label is shown on your site, but your dashboard has not been updated.', 'aiactify-compliance'), $response->get_error_message() ); } $code = (int) wp_remote_retrieve_response_code($response); $body = json_decode(wp_remote_retrieve_body($response), true); if (401 === $code) { return __('AIActify rejected your API Key (401). Check the API Key on the AIActify settings page.', 'aiactify-compliance'); } if (403 === $code) { return __('Your API Key is missing the "content:write" permission, or the Site Key belongs to a different account (403).', 'aiactify-compliance'); } if (404 === $code) { return __('AIActify does not know this Site Key (404). Check the Site Key on the AIActify settings page.', 'aiactify-compliance'); } if ($code < 200 || $code >= 300) { /* translators: %d: HTTP status code */ return sprintf(__('AIActify answered with HTTP %d. Your dashboard has not been updated.', 'aiactify-compliance'), $code); } if (is_array($body) && isset($body['updated']) && 0 === (int) $body['updated']) { return __('AIActify knows your site, but not this page yet — so nothing was updated in your dashboard. The page usually appears after it has been visited once with the AIActify script active.', 'aiactify-compliance'); } return null; } /** * Zeigt den letzten fehlgeschlagenen Sync im Admin an. Ohne diese Meldung * bliebe jeder Fehlschlag unsichtbar (siehe Kommentar oben). */ function aiactify_admin_notice_sync_error() { if (!current_user_can('edit_posts')) { return; } $last = get_option('aiactify_last_sync_error'); if (empty($last['message'])) { return; } // Nur die ersten 24 Stunden zeigen — danach ist die Meldung veraltet. if (isset($last['time']) && (time() - (int) $last['time']) > DAY_IN_SECONDS) { return; } echo 'AIActify: ' . esc_html($last['message']) . '