'Version' ) ); define( 'AIJE_PLUGIN_VERSION', ! empty( $aije_file_data['Version'] ) ? $aije_file_data['Version'] : '1.3.8' ); /* -------- Guard: keine Doppel-Loads -------- */ if ( defined( 'AIJE_LITE_1_0_0' ) ) { return; } define( 'AIJE_LITE_1_0_0', true ); /* -------- Activation / Deactivation Hooks -------- */ register_activation_hook( __FILE__, function () { if ( ! get_option( 'aije_activated_at' ) ) { update_option( 'aije_activated_at', time(), false ); } add_rewrite_rule( '^llms\.txt$', 'index.php?aije_llms=1', 'top' ); flush_rewrite_rules(); // Dieser Flush läuft in einem Spezial-Request, in dem plugins_loaded (und // damit boot() mit dem generate_rewrite_rules-Filter für remove_category_base) // noch NICHT gelaufen ist — der gecachte Regelsatz kann die kategorielose // Regel also fehlen, selbst wenn das Setting an ist. Flag zurücksetzen, // damit maybe_force_category_rewrite_flush() beim nächsten normalen // Seitenaufruf (boot() ist dann korrekt gelaufen) automatisch nachflusht, // statt für "schon erledigt" gehalten zu werden. delete_option( 'aije_cat_rewrite_fix_v2' ); } ); register_activation_hook( __FILE__, function () { global $wp_version; if ( version_compare( PHP_VERSION, '7.2', '<' ) ) { deactivate_plugins( plugin_basename( __FILE__ ) ); wp_die( esc_html__( 'WP Rankify benötigt mindestens PHP 7.2.', 'ai-json-extractor-auto-embed-lite' ) ); } if ( ! isset( $wp_version ) || version_compare( $wp_version, '5.2', '<' ) ) { deactivate_plugins( plugin_basename( __FILE__ ) ); wp_die( esc_html__( 'WP Rankify benötigt mindestens WordPress 5.2.', 'ai-json-extractor-auto-embed-lite' ) ); } } ); register_activation_hook( __FILE__, function () { // Opt-out respektieren (bei Reaktivierung bleibt die Wahl erhalten). if ( get_option( 'aije_stats_optout' ) ) { return; } $payload = array( 'event' => 'plugin_activation', 'site_name' => get_bloginfo( 'name' ), 'site_url' => home_url(), 'php_version' => PHP_VERSION, 'wp_version' => get_bloginfo( 'version' ), 'plugin_version' => AIJE_PLUGIN_VERSION, 'secret' => AIJE_Lite_100::stats_secret(), 'time' => date_i18n( 'c' ), ); @wp_remote_post( AIJE_Lite_100::STATS_ENDPOINT . 'activate', array( 'method' => 'POST', 'timeout' => 8, 'redirection' => 5, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $payload ), 'sslverify' => true, ) ); } ); register_uninstall_hook( __FILE__, 'aije_on_uninstall' ); function aije_on_uninstall() { $pv = defined( 'AIJE_PLUGIN_VERSION' ) ? AIJE_PLUGIN_VERSION : '1.3.8'; if ( ! get_option( 'aije_stats_optout' ) ) wp_remote_post( 'https://www.muench-media.com/aije-stats/heartbeat', array( 'timeout' => 5, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'secret' => hash( 'sha256', home_url() . '1111111' ), 'site_url' => home_url(), 'plugin_version' => $pv, 'event' => 'uninstalled' ) ), 'blocking' => true, 'sslverify' => true, ) ); // KI-Besuche: Log-Tabelle und Optionen entfernen global $wpdb; $wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'aije_ai_visits' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery delete_option( 'aije_ai_visits_db' ); delete_option( 'aije_ai_visits_enabled' ); delete_option( 'aije_ai_visits_retention' ); delete_option( 'aije_ai_debug_until' ); delete_option( 'aije_ai_debug_log' ); } register_deactivation_hook( __FILE__, function () { delete_option( 'aije_stats_notice_dismissed' ); delete_option( 'aije_first_ping_sent' ); delete_option( 'aije_last_heartbeat' ); // WICHTIG: In DIESEM Request läuft der Plugin-Code noch ganz normal weiter // (WordPress muss die Datei laden, um den Deaktivierungs-Hook auszuführen), // d.h. boot() hat den generate_rewrite_rules-/category_link-Filter bereits // registriert. Ein einfacher flush_rewrite_rules() würde die kategorielose // Regel deshalb nur erneut mitschreiben statt sie zu entfernen — die Filter // müssen VOR dem Flush explizit runter, damit der neu berechnete Regelsatz // wirklich "sauber" ist (Kategorie-Slug wieder wie im WordPress-Standard). if ( ! empty( $GLOBALS['aije_lite_instance'] ) ) { remove_filter( 'generate_rewrite_rules', array( $GLOBALS['aije_lite_instance'], 'maybe_add_category_rewrite_rules' ) ); remove_filter( 'category_link', array( $GLOBALS['aije_lite_instance'], 'maybe_strip_category_base_from_link' ), 10 ); } delete_option( 'rewrite_rules' ); flush_rewrite_rules(); $pv = defined( 'AIJE_PLUGIN_VERSION' ) ? AIJE_PLUGIN_VERSION : '1.3.8'; if ( get_option( 'aije_stats_optout' ) ) { return; } wp_remote_post( 'https://www.muench-media.com/aije-stats/heartbeat', array( 'timeout' => 5, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'secret' => hash( 'sha256', home_url() . '1111111' ), 'site_url' => home_url(), 'plugin_version' => $pv, 'event' => 'deactivated' ) ), 'blocking' => true, 'sslverify' => true, ) ); } ); /* -------- Plugin-Update: Heartbeat -------- */ add_action( 'upgrader_process_complete', function( $upgrader, $options ) { if ( $options['action'] === 'update' && $options['type'] === 'plugin' && isset( $options['plugins'] ) ) { foreach ( (array) $options['plugins'] as $plugin ) { if ( strpos( $plugin, 'ai-json-extractor-auto-embed' ) !== false ) { delete_option( 'aije_last_heartbeat' ); $instance = new AIJE_Lite_100(); if ( method_exists( $instance, 'cron_heartbeat_runner' ) ) { $instance->cron_heartbeat_runner(); } break; } } } }, 10, 2 ); /* -------- Erster Ping nach Aktivierung -------- */ add_action( 'admin_init', function() { if ( ! get_option( 'aije_first_ping_sent' ) ) { if ( ! wp_next_scheduled( 'aije_first_heartbeat' ) ) { wp_schedule_single_event( time() + 5, 'aije_first_heartbeat' ); } } } ); add_action( 'aije_first_heartbeat', function() { update_option( 'aije_first_ping_sent', time(), false ); update_option( 'aije_last_heartbeat', time(), false ); if ( class_exists( 'AIJE_Lite_100' ) ) { $instance = new AIJE_Lite_100(); $instance->send_first_ping(); } } ); /* ==================================================================== * Traits laden * ==================================================================== */ $aije_includes = plugin_dir_path( __FILE__ ) . 'includes/'; require_once $aije_includes . 'trait-options.php'; require_once $aije_includes . 'trait-score.php'; require_once $aije_includes . 'trait-schema.php'; require_once $aije_includes . 'trait-stats.php'; require_once $aije_includes . 'trait-export.php'; require_once $aije_includes . 'trait-admin.php'; require_once $aije_includes . 'trait-meta.php'; // Meta-Generator (separates Trait) require_once $aije_includes . 'trait-gsc.php'; // GSC OAuth + Daten require_once $aije_includes . 'trait-sitemap.php'; // XML Sitemap require_once $aije_includes . 'trait-review-notice.php'; // Review-Bitte im Backend require_once $aije_includes . 'trait-ai-visits.php'; // KI-Crawler-Besuche protokollieren /* ==================================================================== * Haupt-Klasse * ==================================================================== */ if ( ! class_exists( 'AIJE_Lite_100' ) ) : class AIJE_Lite_100 { use AIJE_Options; use AIJE_Score; use AIJE_Schema; use AIJE_Stats; use AIJE_Export; use AIJE_Admin; use AIJE_Meta; use AIJE_GSC; use WP_Rankify_Sitemap; use AIJE_Review_Notice; use AIJE_AI_Visits; /* ===== Konstanten ===== */ const OPT_KEY = 'aije_pro_opts'; const OPT_LICENSE = 'aije_license_code'; const META_NO_EMBED = '_aije_no_embed'; const EXPORT_DIR = 'ai-json-exports'; const LOG_FILE = 'ai-json-export-log.csv'; const CRON_EXPORT = 'aije_pro_cron_export'; const CRON_PING = 'aije_pro_cron_ping'; const CRON_HEARTBEAT = 'aije_pro_cron_heartbeat'; const PING_QUEUE_OPT = 'aije_pro_ping_queue'; const PING_ERRORS_OPT = 'aije_pro_last_ping_errors'; const LAST_ARGS_OPT = 'aije_pro_last_export'; const BRAND_LOGO_URL = 'https://www.muench-media.com/img/MMLogo.jpg'; const STATS_SALT = '1111111'; const STATS_ENDPOINT = 'https://www.muench-media.com/aije-stats/'; const AI_PROXY_URL = 'https://www.muench-media.com/aije-proxy/'; const OPT_STATS_OPTOUT = 'aije_stats_optout'; const TRIAL_DAYS = 14; const USER_META_JOB_TITLE = 'aije_job_title'; const USER_META_EXPERTISE = 'aije_expertise'; const USER_META_WEBSITE = 'aije_expert_website'; const USER_META_SAME_AS = 'aije_same_as_links'; const REVIEW_URL = 'https://wordpress.org/support/plugin/ai-json-extractor-auto-embed-lite/reviews/#new-post'; const REVIEW_NOTICE_DELAY_DAYS = 10; const REVIEW_NOTICE_SNOOZE_DAYS = 14; /* ===== Konstruktor ===== */ public function __construct() { add_action( 'plugins_loaded', array( $this, 'boot' ), 20 ); } /* ===== Boot – alle Hooks registrieren ===== */ public function boot() { // Übersetzungen laden — WICHTIG: das fehlte bisher komplett, wodurch // selbst die wenigen vorhandenen __()/_e()-Aufrufe wirkungslos waren, // weil WordPress nie wusste, wo die .mo-Dateien liegen. load_plugin_textdomain( 'ai-json-extractor-auto-embed-lite', false, dirname( plugin_basename( AIJE_PLUGIN_FILE ) ) . '/languages' ); // Erzwungene Backend-Sprache (Einstellung in tab-settings.php) — NICHT über // den 'plugin_locale'-Filter lösbar: Seit WordPress 6.7 lädt WP .mo-Dateien // nur noch "just in time" beim ersten __()-Aufruf, direkt über // determine_locale() — der 'plugin_locale'-Filter wird dabei gar nicht mehr // aufgerufen (gegen den aktuellen WP-Core-Quellcode geprüft: kein einziger // Treffer für 'plugin_locale' mehr in wp-includes/l10n.php). Deshalb klinken // wir uns stattdessen dort ein, wo WP JEDE übersetzte Zeichenkette für // unsere Text-Domain ohnehin durchreicht: dem 'gettext_{domain}'-Filter // (greift für __()/_e()/esc_html__() usw., die alle intern über translate() // laufen). So bestimmen wir die Sprache selbst, unabhängig davon wie // WP-Core seine .mo-Dateien lädt oder welche Version das gerade tut. add_filter( 'gettext_ai-json-extractor-auto-embed-lite', array( $this, 'filter_backend_gettext' ), 10, 3 ); // Pendant für _n()/_n_noop() (Plural-Strings): WP ruft für diese NICHT // 'gettext_{domain}', sondern 'ngettext_{domain}' auf — ein eigener // Filter mit eigener Signatur. Ohne diesen Hook bleiben alle _n()-Aufrufe // unserer Domain von der oben erzwungenen Backend-Sprache unberührt und // fallen auf das zurück, was WP-Core selbst auflöst (bei uns: der // deutsche Quelltext, da die Domain sonst nirgends geladen wird) — genau // der Bug, der die 4 Plural-Strings in der internen Verlinkung trotz // korrekter .mo-Datei auf Deutsch stehen ließ. add_filter( 'ngettext_ai-json-extractor-auto-embed-lite', array( $this, 'filter_backend_ngettext' ), 10, 5 ); // AJAX add_action( 'wp_ajax_aije_generate_meta', array( $this, 'aije_generate_meta_handler' ) ); add_action( 'wp_ajax_aije_save_meta', array( $this, 'aije_save_meta_handler' ) ); add_action( 'wp_ajax_aije_topic_analysis', array( $this, 'aije_topic_analysis_handler' ) ); add_action( 'wp_ajax_aije_check_status', array( $this, 'aije_check_status_handler' ) ); add_action( 'wp_ajax_aije_insert_link', array( $this, 'aije_insert_link_handler' ) ); add_action( 'wp_ajax_aije_exclude_post', array( $this, 'aije_exclude_post_handler' ) ); add_action( 'wp_ajax_aije_include_post', array( $this, 'aije_include_post_handler' ) ); // Admin add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) ); add_action( 'admin_notices', array( $this, 'stats_consent_notice' ) ); add_action( 'admin_notices', array( $this, 'maybe_render_review_notice' ) ); add_action( 'admin_post_aije_stats_optout', array( $this, 'handle_stats_optout' ) ); add_action( 'admin_post_aije_stats_optin', array( $this, 'handle_stats_optin' ) ); add_action( 'add_meta_boxes', array( $this, 'add_score_metabox' ) ); // Experten-Profil add_action( 'show_user_profile', array( $this, 'add_expert_profile_fields' ) ); add_action( 'edit_user_profile', array( $this, 'add_expert_profile_fields' ) ); add_action( 'personal_options_update', array( $this, 'save_expert_profile_fields' ) ); add_action( 'edit_user_profile_update', array( $this, 'save_expert_profile_fields' ) ); // Frontend add_action( 'wp_head', array( $this, 'output_person_schema_in_head' ), 5 ); add_action( 'wp_head', array( $this, 'output_aije_meta_description' ), 4 ); add_action( 'wp_head', array( $this, 'output_tracking_head' ), 1 ); add_action( 'wp_head', array( $this, 'auto_embed' ) ); add_action( 'wp_body_open', array( $this, 'auto_embed' ) ); // Telemetrie / Activity add_action( 'wp_login', array( $this, 'on_admin_login' ), 10, 2 ); add_action( 'admin_init', array( $this, 'on_admin_activity' ) ); if ( is_admin() ) { ob_start(); } // SEO-Meta-Box add_action( 'add_meta_boxes', array( $this, 'add_seo_metabox' ) ); add_action( 'save_post', array( $this, 'save_seo_metabox' ), 10, 1 ); if ( is_admin() ) { add_action( 'admin_menu', array( $this, 'tools_menu' ) ); add_action( 'admin_init', array( $this, 'register_settings' ) ); add_action( 'add_meta_boxes', array( $this, 'add_toggle_metabox' ) ); add_action( 'add_meta_boxes', array( $this, 'add_ki_highlight_metabox' ) ); add_action( 'add_meta_boxes', array( $this, 'add_preview_metabox' ) ); add_action( 'save_post', array( $this, 'save_post_meta' ) ); add_action( 'admin_post_aije_export', array( $this, 'handle_export' ) ); add_action( 'admin_post_aije_run_cron_now', array( $this, 'handle_run_cron_now' ) ); add_action( 'admin_post_aije_clear_cron', array( $this, 'handle_clear_cron' ) ); add_action( 'admin_post_aije_test_ping', array( $this, 'handle_test_ping' ) ); add_action( 'admin_post_aije_flush_queue', array( $this, 'handle_flush_queue' ) ); add_action( 'admin_post_aije_review_leave', array( $this, 'handle_review_notice_leave' ) ); add_action( 'admin_post_aije_review_snooze', array( $this, 'handle_review_notice_snooze' ) ); add_action( 'admin_post_aije_review_dismiss', array( $this, 'handle_review_notice_dismiss' ) ); add_action( 'admin_post_aije_ai_visits_settings', array( $this, 'handle_ai_visits_settings' ) ); add_action( 'admin_init', array( $this, 'maybe_install_ai_visits_table' ) ); } // Cron add_filter( 'cron_schedules', array( $this, 'add_schedules' ) ); add_action( self::CRON_EXPORT, array( $this, 'cron_export_runner' ) ); add_action( self::CRON_PING, array( $this, 'cron_ping_runner' ) ); add_action( self::CRON_HEARTBEAT, array( $this, 'cron_heartbeat_runner' ) ); // Post-Übergänge add_action( 'transition_post_status', array( $this, 'on_transition_status' ), 10, 3 ); add_action( 'post_updated', array( $this, 'on_post_updated' ), 10, 3 ); // Sitemap $this->sitemap_boot(); // llms.txt / robots.txt / GSC add_action( 'init', array( $this, 'register_llms_rewrite' ) ); add_action( 'init', array( $this, 'maybe_answer_key_verification' ), 1 ); // Domain-Verifizierung für den KI-Proxy add_action( 'admin_init', array( $this, 'handle_gsc_oauth_callback' ) ); add_action( 'template_redirect', array( $this, 'log_ai_visit' ), 0 ); // KI-Besuche – vor serve_llms_txt, damit auch llms.txt-Abrufe gezählt werden add_action( 'template_redirect', array( $this, 'serve_llms_txt' ) ); add_filter( 'robots_txt', array( $this, 'append_llms_to_robots' ), 10, 2 ); // Kategorie-Slug entfernen (remove_category_base) – Funktionen liegen in // trait-options.php, waren dort aber ohne Hook-Registrierung wirkungslos. // Deshalb blieb der Haken zwar gespeichert, hatte aber nie einen Effekt. add_filter( 'category_link', array( $this, 'maybe_strip_category_base_from_link' ), 10, 2 ); add_filter( 'generate_rewrite_rules', array( $this, 'maybe_add_category_rewrite_rules' ) ); // WICHTIG: NICHT direkt hier (plugins_loaded) aufrufen — WordPress rät // ausdrücklich davon ab, flush_rewrite_rules() vor 'init' zu nutzen, weil // zu diesem frühen Zeitpunkt die Rewrite-Infrastruktur noch nicht sicher // vollständig aufgebaut ist. Das war die eigentliche Ursache dafür, dass // der Korrektur-Flush nach Reaktivieren teils nicht griff. add_action( 'init', array( $this, 'maybe_force_category_rewrite_flush' ) ); // 301 von der alten /category/...-URL auf die neue, damit nicht beide // Varianten parallel erreichbar bleiben (Duplicate Content). add_action( 'template_redirect', array( $this, 'maybe_redirect_old_category_base_url' ), 5 ); // Init $o = $this->get_opts(); $this->maybe_reschedule_export( ! empty( $o['cron_enabled'] ), $o['cron_frequency'] ); $this->ensure_ping_cron(); $this->ensure_heartbeat_cron(); } /* ===== Cron-Schedules ===== */ public function add_schedules( $s ) { $s['aije_daily'] = array( 'interval' => DAY_IN_SECONDS, 'display' => __( 'AI JSON – täglich', 'ai-json-extractor-auto-embed-lite' ) ); $s['aije_weekly'] = array( 'interval' => WEEK_IN_SECONDS, 'display' => __( 'AI JSON – wöchentlich', 'ai-json-extractor-auto-embed-lite' ) ); $s['aije_15min'] = array( 'interval' => 900, 'display' => __( 'AI JSON – alle 15 Minuten', 'ai-json-extractor-auto-embed-lite' ) ); return $s; } } endif; /* ===== Bootstrap ===== */ $GLOBALS['aije_lite_instance'] = new AIJE_Lite_100(); if ( ! function_exists( 'aije_pro_enabled' ) ) { function aije_pro_enabled() { return true; } }