array( 'post', 'page' ), 'enable_schema' => 0, 'minify' => 0, 'acf_include' => 1, 'cron_enabled' => 0, 'cron_frequency' => 'daily', 'ping_enabled' => 1, 'ping_urls' => "# Beispiele (Platzhalter anpassen)\nhttps://webhook.site/your-uuid\n", 'ping_events' => array( 'publish', 'update' ), 'ping_secret' => '', 'ping_timeout' => 5, 'ping_retry' => 1, 'openai_prompt' => 'Analysiere den folgenden WordPress Beitrag. Antworte ausschließlich als JSON mit overall_score, issues[], suggested_headings[], faq[].', 'score_preset' => 'standard', 'score_weight_content' => 35, 'score_weight_eeat' => 30, 'score_weight_technical' => 20, 'score_weight_linking' => 15, 'seo_plugin' => 'auto', 'remove_category_base' => 0, 'gtm_id' => '', 'ga4_id' => '', 'gsc_verify' => '', 'meta_pixel' => '', 'gsc_client_id' => '', 'gsc_client_secret' => '', 'content_language' => 'auto', 'backend_language' => 'auto', ); } /** * NUR die erlaubten Codes, OHNE __()-Aufruf. Bewusst getrennt von * get_backend_language_choices() (die die übersetzten Labels fürs Dropdown * liefert): get_opts()/sanitize_settings() validieren hiermit, und dürfen * dabei NICHT __() aufrufen — sonst Endlosschleife, weil unser eigener * 'gettext_{domain}'-Filter (siehe filter_backend_gettext()) bei jedem * __()-Aufruf für unsere Domain wiederum get_opts() aufruft. */ private static function backend_language_codes() { return array( 'auto', 'de_DE', 'en_US' ); } /** * Erlaubte Werte für backend_language: 'auto' (WordPress-Profil-/Site-Sprache * des jeweiligen Nutzers entscheidet, wie bisher) oder eine feste Locale, die * die Plugin-Oberfläche (Dashboard, Einstellungen, Meta-Box im Editor, ...) * für ALLE Nutzer unabhängig von deren eigener WP-Spracheinstellung erzwingt. * Getrennt von content_language, das nur die KI-Ausgaben (Meta-Description, * Themen-Analyse) betrifft, nicht die Plugin-Oberfläche selbst. * NUR für die UI (Dropdown in tab-settings.php) — NICHT für Validierung * verwenden, siehe Hinweis bei backend_language_codes(). */ public static function get_backend_language_choices() { return array( 'auto' => __( 'Wie WordPress (Profil-/Website-Sprache)', 'ai-json-extractor-auto-embed-lite' ), 'de_DE' => __( 'Deutsch', 'ai-json-extractor-auto-embed-lite' ), 'en_US' => __( 'Englisch', 'ai-json-extractor-auto-embed-lite' ), ); } /** * Hook für den WP-Core-Filter 'gettext_{domain}' (nur für UNSERE Text-Domain * registriert, siehe boot() in der Haupt-Plugin-Datei). Läuft bei JEDEM * __()/_e()/esc_html__()-Aufruf im Plugin. Wenn der Nutzer in den * Einstellungen explizit Deutsch oder Englisch statt "Automatisch" gewählt * hat, überschreiben wir hier das Ergebnis — unabhängig davon, welche * Sprache WP-Core selbst anhand von determine_locale() (Profil-/ * Website-Sprache des jeweiligen Nutzers) ermittelt und geladen hat. */ public function filter_backend_gettext( $translation, $text, $domain ) { $o = $this->get_opts(); $lang = $o['backend_language'] ?? 'auto'; if ( $lang === 'de_DE' ) { // Quelltext ist ohnehin Deutsch — Original zurückgeben, unabhängig // davon was WP-Core (ggf. für eine andere Locale) aufgelöst hat. return $text; } if ( $lang === 'en_US' ) { $mo = $this->get_own_backend_mo( 'en_US' ); if ( $mo ) { $translated = $mo->translate( $text ); if ( $translated !== $text ) { return $translated; } } } return $translation; } /** * Pendant zu filter_backend_gettext() für Plural-Strings (_n()/_n_noop()). * WP-Core ruft für Plural-Übersetzungen NICHT den 'gettext_{domain}'-Filter * auf, sondern separat 'ngettext_{domain}' — mit eigener Signatur * ($single, $plural, $number statt nur $text). Ohne diesen eigenen Hook * bleiben _n()-Aufrufe von der hier erzwungenen Backend-Sprache unberührt. */ public function filter_backend_ngettext( $translation, $single, $plural, $number, $domain ) { $o = $this->get_opts(); $lang = $o['backend_language'] ?? 'auto'; if ( $lang === 'de_DE' ) { // Quelltext ist ohnehin Deutsch — unveränderte Originalform zurückgeben. return $number == 1 ? $single : $plural; } if ( $lang === 'en_US' ) { $mo = $this->get_own_backend_mo( 'en_US' ); if ( $mo ) { $fallback = $number == 1 ? $single : $plural; $translated = $mo->translate_plural( $single, $plural, $number ); if ( $translated !== $fallback ) { return $translated; } } } return $translation; } /** * Lädt die plugin-eigene .mo-Datei direkt über die WP-eigene MO-Klasse — * bewusst UNABHÄNGIG von load_plugin_textdomain()/WP_Translation_Controller, * deren "just in time"-Ladeverhalten (seit WP 6.7) sich nicht per Filter auf * eine feste Locale zwingen lässt. Wird pro Locale einmal pro Request geladen * und gecacht. */ private function get_own_backend_mo( $locale ) { static $cache = array(); if ( array_key_exists( $locale, $cache ) ) { return $cache[ $locale ]; } if ( ! class_exists( 'MO' ) ) { require_once ABSPATH . WPINC . '/pomo/mo.php'; } $file = dirname( AIJE_PLUGIN_FILE ) . '/languages/ai-json-extractor-auto-embed-lite-' . $locale . '.mo'; $mo = new MO(); if ( is_readable( $file ) && $mo->import_from_file( $file ) ) { $cache[ $locale ] = $mo; } else { $cache[ $locale ] = false; } return $cache[ $locale ]; } /** * NUR die erlaubten Codes, OHNE __()-Aufruf — siehe Hinweis bei * backend_language_codes() oben; derselbe Grund gilt hier. */ private static function content_language_codes() { return array( 'auto', 'de', 'en', 'fr', 'es', 'it', 'nl', 'pl', 'pt', 'ru', 'tr' ); } /** * Erlaubte Werte für content_language: 'auto' (Sprache pro Artikel automatisch * per KI erkennen) oder ein fester Sprachname, der bei jedem KI-Aufruf * (Meta-Description-Generator + Themen-Analyse) erzwungen wird — sorgt für * einheitliche Ausgabesprache unabhängig vom Artikelinhalt. * NUR für die UI — NICHT für Validierung verwenden (s.o.). */ public static function get_language_choices() { return array( 'auto' => __( 'Automatisch erkennen (aus Artikelinhalt)', 'ai-json-extractor-auto-embed-lite' ), 'de' => __( 'Deutsch', 'ai-json-extractor-auto-embed-lite' ), 'en' => __( 'Englisch', 'ai-json-extractor-auto-embed-lite' ), 'fr' => __( 'Französisch', 'ai-json-extractor-auto-embed-lite' ), 'es' => __( 'Spanisch', 'ai-json-extractor-auto-embed-lite' ), 'it' => __( 'Italienisch', 'ai-json-extractor-auto-embed-lite' ), 'nl' => __( 'Niederländisch', 'ai-json-extractor-auto-embed-lite' ), 'pl' => __( 'Polnisch', 'ai-json-extractor-auto-embed-lite' ), 'pt' => __( 'Portugiesisch', 'ai-json-extractor-auto-embed-lite' ), 'ru' => __( 'Russisch', 'ai-json-extractor-auto-embed-lite' ), 'tr' => __( 'Türkisch', 'ai-json-extractor-auto-embed-lite' ), ); } private function get_opts() { $o = get_option( self::OPT_KEY, array() ); if ( ! is_array( $o ) ) { $o = array(); } $defaults = self::defaults(); foreach ( $defaults as $k => $v ) { if ( ! isset( $o[ $k ] ) ) { $o[ $k ] = $v; } } $o['enabled_post_types'] = array_values( array_filter( array_map( 'sanitize_key', (array) $o['enabled_post_types'] ) ) ); $o['enable_schema'] = ! empty( $o['enable_schema'] ) ? 1 : 0; $o['minify'] = ! empty( $o['minify'] ) ? 1 : 0; $o['acf_include'] = ! empty( $o['acf_include'] ) ? 1 : 0; $o['cron_enabled'] = ! empty( $o['cron_enabled'] ) ? 1 : 0; $o['cron_frequency'] = in_array( $o['cron_frequency'], array( 'daily', 'weekly' ), true ) ? $o['cron_frequency'] : 'daily'; $o['ping_enabled'] = ! empty( $o['ping_enabled'] ) ? 1 : 0; $o['ping_urls'] = is_string( $o['ping_urls'] ) ? $o['ping_urls'] : ''; $o['ping_secret'] = is_string( $o['ping_secret'] ) ? $o['ping_secret'] : ''; $o['ping_timeout'] = max( 2, (int) $o['ping_timeout'] ); $o['ping_retry'] = ! empty( $o['ping_retry'] ) ? 1 : 0; if ( ! isset( $o['openai_api_key'] ) ) { $o['openai_api_key'] = ''; } $valid_presets = array( 'ymyl', 'standard', 'ecommerce', 'custom' ); $o['score_preset'] = in_array( $o['score_preset'] ?? 'standard', $valid_presets, true ) ? $o['score_preset'] : 'standard'; $o['score_weight_content'] = max( 5, (int) ( $o['score_weight_content'] ?? 35 ) ); $o['score_weight_eeat'] = max( 5, (int) ( $o['score_weight_eeat'] ?? 30 ) ); $o['score_weight_technical'] = max( 5, (int) ( $o['score_weight_technical'] ?? 20 ) ); $o['score_weight_linking'] = max( 5, (int) ( $o['score_weight_linking'] ?? 15 ) ); $valid_seo = array( 'auto', 'yoast', 'rankmath', 'aioseo', 'own' ); $o['seo_plugin'] = in_array( $o['seo_plugin'] ?? 'auto', $valid_seo, true ) ? $o['seo_plugin'] : 'auto'; $valid_languages = self::content_language_codes(); $o['content_language'] = in_array( $o['content_language'] ?? 'auto', $valid_languages, true ) ? $o['content_language'] : 'auto'; $valid_backend_languages = self::backend_language_codes(); $o['backend_language'] = in_array( $o['backend_language'] ?? 'auto', $valid_backend_languages, true ) ? $o['backend_language'] : 'auto'; return $o; } public function get_opts_public() { return $this->get_opts(); } public static function get_score_weights( $o = array() ) { $presets = array( 'ymyl' => array( 'content' => 30, 'eeat' => 40, 'technical' => 20, 'linking' => 10 ), 'standard' => array( 'content' => 35, 'eeat' => 30, 'technical' => 20, 'linking' => 15 ), 'ecommerce' => array( 'content' => 30, 'eeat' => 20, 'technical' => 35, 'linking' => 15 ), ); $preset = isset( $o['score_preset'] ) ? $o['score_preset'] : 'standard'; if ( $preset !== 'custom' ) { return $presets[ $preset ] ?? $presets['standard']; } $w = array( 'content' => max( 5, (int) ( $o['score_weight_content'] ?? 35 ) ), 'eeat' => max( 5, (int) ( $o['score_weight_eeat'] ?? 30 ) ), 'technical' => max( 5, (int) ( $o['score_weight_technical'] ?? 20 ) ), 'linking' => max( 5, (int) ( $o['score_weight_linking'] ?? 15 ) ), ); $sum = array_sum( $w ); if ( $sum !== 100 ) { foreach ( $w as $k => $v ) { $w[ $k ] = (int) round( $v * 100 / $sum ); } $w['content'] += ( 100 - array_sum( $w ) ); } return $w; } public function get_active_seo_plugin() { $o = $this->get_opts(); $choice = $o['seo_plugin'] ?? 'auto'; if ( $choice !== 'auto' ) { return $choice; } if ( defined( 'WPSEO_VERSION' ) ) { return 'yoast'; } if ( defined( 'RANK_MATH_VERSION' ) ) { return 'rankmath'; } if ( defined( 'AIOSEO_VERSION' ) ) { return 'aioseo'; } return 'own'; } public function register_settings() { register_setting( self::OPT_KEY, self::OPT_KEY, array( $this, 'sanitize_settings' ) ); register_setting( 'aije_ai_settings', 'aije_openai_api_key' ); } public function sanitize_settings( $in ) { $d = self::defaults(); $old_opts = get_option( self::OPT_KEY, array() ); if ( ! is_array( $old_opts ) ) { $old_opts = array(); } // WICHTIG: Jeder Tab (Einstellungen, Tracking, ...) ist ein EIGENES //
, alle speichern aber über dieselbe Options-API-Option // (self::OPT_KEY) — WordPress ruft diese Funktion also bei JEDEM Tab // auf, aber $in enthält jeweils nur die Felder DIESES einen Formulars. // Basis sind deshalb die ALTEN Werte; nur der Tab, der tatsächlich // abgeschickt wurde, überschreibt seine eigenen Felder — erkannt an // einem für ihn typischen Feld, das GARANTIERT im jeweiligen Formular // existiert (per Blick in admin/tab-*.php geprüft, nicht geraten). // // tab-settings.php hat KEIN cron_frequency-Feld (das war der Fehler im // vorigen Versuch — die Bedingung war dadurch immer falsch, der ganze // Tab speicherte gar nichts mehr). Verlässlicher Anker dort ist // score_preset: ein verstecktes Radio, von dem laut Formular immer // genau eines "checked" und damit im POST ist. // // gsc_verify liegt tatsächlich im Tracking-Formular (tab-tracking.php), // nicht im GSC-Tab. Der GSC-Tab (tab-gsc.php) speichert Client-ID/ // -Secret ohnehin über ein eigenes, direktes update_option() außerhalb // der Settings-API — läuft also gar nicht durch diese Funktion. // // cron_/ping_/openai_/seo_plugin-Felder haben aktuell in KEINEM Tab // ein Formularfeld — die werden hier bewusst nicht angefasst, damit // sie nicht bei jedem Speichern auf ihren Default zurückfallen. $o = array_merge( $d, $old_opts ); // -- Tab "Einstellungen" (tab-settings.php) -- if ( array_key_exists( 'score_preset', $in ) ) { $o['enabled_post_types'] = isset( $in['enabled_post_types'] ) ? array_values( array_map( 'sanitize_key', (array) $in['enabled_post_types'] ) ) : array(); $o['enable_schema'] = ! empty( $in['enable_schema'] ) ? 1 : 0; $o['remove_category_base'] = ! empty( $in['remove_category_base'] ) ? 1 : 0; $o['minify'] = ! empty( $in['minify'] ) ? 1 : 0; $o['acf_include'] = ! empty( $in['acf_include'] ) ? 1 : 0; $valid_languages = self::content_language_codes(); $o['content_language'] = isset( $in['content_language'] ) && in_array( $in['content_language'], $valid_languages, true ) ? $in['content_language'] : 'auto'; $valid_backend_languages = self::backend_language_codes(); $o['backend_language'] = isset( $in['backend_language'] ) && in_array( $in['backend_language'], $valid_backend_languages, true ) ? $in['backend_language'] : 'auto'; $valid_presets = array( 'ymyl', 'standard', 'ecommerce', 'custom' ); $o['score_preset'] = in_array( $in['score_preset'], $valid_presets, true ) ? $in['score_preset'] : 'standard'; $o['score_weight_content'] = isset( $in['score_weight_content'] ) ? max( 5, (int) $in['score_weight_content'] ) : 35; $o['score_weight_eeat'] = isset( $in['score_weight_eeat'] ) ? max( 5, (int) $in['score_weight_eeat'] ) : 30; $o['score_weight_technical'] = isset( $in['score_weight_technical'] ) ? max( 5, (int) $in['score_weight_technical'] ) : 20; $o['score_weight_linking'] = isset( $in['score_weight_linking'] ) ? max( 5, (int) $in['score_weight_linking'] ) : 15; $old_remove_cat_base = ! empty( $old_opts['remove_category_base'] ) ? 1 : 0; if ( $o['remove_category_base'] !== $old_remove_cat_base ) { add_action( 'shutdown', 'flush_rewrite_rules' ); } } // -- Tab "Tracking & Analytics" (tab-tracking.php) -- if ( array_key_exists( 'gtm_id', $in ) || array_key_exists( 'ga4_id', $in ) || array_key_exists( 'gsc_verify', $in ) || array_key_exists( 'meta_pixel', $in ) ) { $o['gtm_id'] = sanitize_text_field( $in['gtm_id'] ?? '' ); $o['ga4_id'] = sanitize_text_field( $in['ga4_id'] ?? '' ); $o['gsc_verify'] = sanitize_text_field( $in['gsc_verify'] ?? '' ); $o['meta_pixel'] = sanitize_text_field( $in['meta_pixel'] ?? '' ); } // -- GSC-Zugangsdaten (tab-gsc.php) — läuft normalerweise NICHT über // diese Funktion (eigenes update_option() dort), dieser Block ist nur // ein defensiver Fallback, falls doch mal über die Settings-API gespeichert wird. if ( array_key_exists( 'gsc_client_id', $in ) || array_key_exists( 'gsc_client_secret', $in ) ) { $o['gsc_client_id'] = sanitize_text_field( $in['gsc_client_id'] ?? '' ); $o['gsc_client_secret'] = sanitize_text_field( $in['gsc_client_secret'] ?? '' ); } // Cron-/Ping-Scheduling unabhängig vom Tab immer mit dem aktuellen // (alten oder neuen) Stand synchron halten — unverändert seit vorher, // nur nicht mehr an ein bestimmtes Tab-Formular gekoppelt. $this->maybe_reschedule_export( $o['cron_enabled'], $o['cron_frequency'] ); $this->ensure_ping_cron(); return $o; } /* ===== Kategorie-Slug entfernen ===== */ public function maybe_strip_category_base_from_link( $link, $term_id = 0 ) { $o = $this->get_opts(); if ( empty( $o['remove_category_base'] ) ) { return $link; } $cat_base = get_option( 'category_base' ); $cat_base = $cat_base ? trim( $cat_base, '/' ) : 'category'; if ( $cat_base === '' ) { return $link; } return str_replace( '/' . $cat_base . '/', '/', $link ); } public function maybe_add_category_rewrite_rules( $wp_rewrite ) { $o = $this->get_opts(); if ( empty( $o['remove_category_base'] ) ) { return $wp_rewrite; } $categories = get_categories( array( 'hide_empty' => false ) ); if ( empty( $categories ) ) { return $wp_rewrite; } $category_rewrite = array(); foreach ( $categories as $category ) { $nicename = $category->slug; if ( (int) $category->parent === (int) $category->term_id ) { $category->parent = 0; } elseif ( 0 !== (int) $category->parent ) { $nicename = get_category_parents( $category->parent, false, '/', true ) . $nicename; } // Nur der EXAKTE, bekannte Kategorie-Slug wird registriert — keine generische // Catch-all-Regel, damit Beitrags-URLs (gleiche Segmenttiefe) nicht kollidieren. $category_rewrite[ '(' . $nicename . ')/(?:feed/(feed|rdf|rss|rss2|atom)/?)?$' ] = 'index.php?category_name=$matches[1]&feed=$matches[2]'; $category_rewrite[ '(' . $nicename . ')/(?:page/([0-9]{1,})/?)?$' ] = 'index.php?category_name=$matches[1]&paged=$matches[2]'; $category_rewrite[ '(' . $nicename . ')/?$' ] = 'index.php?category_name=$matches[1]'; } $wp_rewrite->rules = $category_rewrite + $wp_rewrite->rules; // Muss zurückgegeben werden — der 'generate_rewrite_rules'-Filter erwartet // das (ggf. veränderte) $wp_rewrite-Objekt als Rückgabewert. Fehlte bisher, // dazu kam noch: der Hook war nirgends registriert (siehe Haupt-Datei-Fix). return $wp_rewrite; } /** * Redirected die ALTE /category/foo/-URL per 301 auf die neue /foo/-URL, * wenn remove_category_base aktiv ist. Ohne das bleibt die alte URL parallel * erreichbar (WordPress' Standard-Rewrite-Regeln werden durch das Voranstellen * der neuen Regeln ja nicht entfernt, nur ergänzt) — Duplicate-Content-Risiko. */ public function maybe_redirect_old_category_base_url() { $o = $this->get_opts(); if ( empty( $o['remove_category_base'] ) ) { return; } if ( is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) { return; } if ( ! is_category() ) { return; } $cat_base = get_option( 'category_base' ); $cat_base = $cat_base ? trim( $cat_base, '/' ) : 'category'; if ( $cat_base === '' ) { return; } // Home-Verzeichnis abziehen (z.B. "/magazin" bei Subdirectory-Installs), // sonst schlägt der Präfix-Vergleich unten fehl, weil REQUEST_URI dann // "magazin/category/..." statt "category/..." ist. $home_path = trim( (string) parse_url( home_url(), PHP_URL_PATH ), '/' ); $request_path = trim( (string) parse_url( $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH ), '/' ); if ( $home_path !== '' && strpos( $request_path, $home_path ) === 0 ) { $request_path = trim( substr( $request_path, strlen( $home_path ) ), '/' ); } $prefix = $cat_base . '/'; if ( strpos( $request_path, $prefix ) !== 0 ) { return; // schon die neue, kategorielose URL — nichts zu tun } $new_path = substr( $request_path, strlen( $prefix ) ); $target = home_url( '/' . $new_path . '/' ); if ( ! empty( $_SERVER['QUERY_STRING'] ) ) { $target .= '?' . $_SERVER['QUERY_STRING']; } // Explizit NICHT dauerhaft cachen lassen: die Umleitung hängt an einer // Plugin-Einstellung, die jederzeit umgeschaltet werden kann. Ohne diese // Header interpretieren manche Browser den 301-Status selbst schon als // "für immer merken" und fragen den Server danach gar nicht mehr an. nocache_headers(); wp_safe_redirect( $target, 301 ); exit; } /** * Erzwingt EINMALIG einen kompletten Rewrite-Flush (löscht den gecachten * `rewrite_rules`-Eintrag und schreibt ihn neu). Schützt davor, dass nach * einem Plugin-Update noch veraltete/kaputte Regeln aus dem Cache aktiv * bleiben, obwohl der PHP-Code bereits korrigiert wurde. Läuft nur einmal * pro Versionsstand (Flag-Option mit Versionsnummer im Namen). */ public function maybe_force_category_rewrite_flush() { $flag = 'aije_cat_rewrite_fix_v2'; if ( get_option( $flag ) ) { return; } delete_option( 'rewrite_rules' ); flush_rewrite_rules( false ); update_option( $flag, 1, false ); } /* ===== Lizenz / Trial ===== */ public static function trial_is_active() { $ts = (int) get_option( 'aije_activated_at', 0 ); if ( ! $ts ) { return true; } return ( time() <= $ts + self::TRIAL_DAYS * DAY_IN_SECONDS ); } public static function trial_end_timestamp() { $ts = (int) get_option( 'aije_activated_at', 0 ); if ( ! $ts ) { return 0; } return $ts + self::TRIAL_DAYS * DAY_IN_SECONDS; } private static function get_normalized_host() { $home = function_exists( 'home_url' ) ? home_url() : ''; $site = function_exists( 'site_url' ) ? site_url() : ''; $url = $home ? $home : $site; $host = ''; if ( $url ) { $parsed = @parse_url( $url ); if ( is_array( $parsed ) && ! empty( $parsed['host'] ) ) { $host = strtolower( $parsed['host'] ); } } if ( $host !== '' && strpos( $host, 'www.' ) === 0 ) { $host = substr( $host, 4 ); } return $host; } private static function base_domain( $host ) { if ( ! $host ) { return ''; } $parts = explode( '.', $host ); $n = count( $parts ); if ( $n >= 2 ) { return $parts[ $n - 2 ] . '.' . $parts[ $n - 1 ]; } return $host; } private static function code_from_domain( $domain ) { $domain = strtolower( (string) $domain ); if ( $domain === '' ) { return ''; } return strtoupper( substr( hash( 'sha256', $domain ), 0, 16 ) ); } public static function license_is_valid( $code = null ) { $entered = strtoupper( trim( (string) ( $code ? $code : get_option( self::OPT_LICENSE, '' ) ) ) ); if ( $entered === '' ) { return false; } $host = self::get_normalized_host(); if ( $host === '' ) { return false; } $code1 = self::code_from_domain( $host ); if ( $code1 && hash_equals( $code1, $entered ) ) { return true; } $base = self::base_domain( $host ); if ( $base && $base !== $host ) { $code2 = self::code_from_domain( $base ); if ( $code2 && hash_equals( $code2, $entered ) ) { return true; } } return false; } public static function validate_license_remote( $license_key ) { if ( empty( $license_key ) ) { return array( 'valid' => false, 'error' => 'Kein Lizenzcode angegeben.' ); } $host = self::get_normalized_host(); $response = wp_remote_post( 'https://www.muench-media.com/aije-license/validate', array( 'timeout' => 10, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'license_key' => $license_key, 'domain' => $host ) ), ) ); if ( is_wp_error( $response ) ) { return array( 'valid' => false, 'error' => 'Server nicht erreichbar: ' . $response->get_error_message() ); } $body = json_decode( wp_remote_retrieve_body( $response ), true ); return is_array( $body ) ? $body : array( 'valid' => false, 'error' => 'Ungültige Server-Antwort.' ); } }