'Keine Berechtigung.' ) ); } $post_id = intval( $_POST['post_id'] ?? 0 ); $post = get_post( $post_id ); if ( ! $post ) { wp_send_json_error( array( 'msg' => 'Beitrag nicht gefunden.' ) ); } // SICHERHEIT: Rechte für GENAU diesen Beitrag prüfen, nicht nur "edit_posts" — // sonst kann ein Mitarbeiter fremde Entwürfe/private Beiträge auslesen lassen. if ( ! current_user_can( 'edit_post', $post_id ) ) { wp_send_json_error( array( 'msg' => 'Keine Berechtigung.' ) ); } $result = $this->ai_generate_meta_variants( $post ); if ( ! empty( $result['error'] ) ) { $err_data = array( 'msg' => $result['error'] ); if ( ! empty( $result['limit'] ) ) { $err_data['limit'] = true; $err_data['upgrade_url'] = $result['upgrade_url'] ?? 'https://wprankify.de/#pricing'; } wp_send_json_error( $err_data ); } $variants = $result['variants'] ?? array(); if ( empty( $variants ) ) { wp_send_json_error( array( 'msg' => 'Nicht genug Inhalt.' ) ); } $existing = get_post_meta( $post_id, '_yoast_wpseo_metadesc', true ); if ( empty( $existing ) ) { $existing = get_post_meta( $post_id, 'rank_math_description', true ); } if ( empty( $existing ) ) { $existing = get_post_meta( $post_id, '_aioseo_description', true ); } $seo = $this->get_active_seo_plugin(); $target = $seo === 'rankmath' ? 'rankmath' : ( $seo === 'aioseo' ? 'aioseo' : ( $seo === 'yoast' ? 'yoast' : 'none' ) ); wp_send_json_success( array( 'variants' => $variants, 'existing_meta' => $existing ?: '', 'target' => $target, 'post_id' => $post_id, 'quota' => $result['quota'] ?? array() ) ); } /** * Ersetzt die frühere rein PHP-template-basierte Generierung (immer Deutsch, * feste Textbausteine) durch einen KI-Aufruf am Proxy — analog zur * Themen-Analyse. Sprache folgt der Einstellung content_language: * 'auto' = Proxy erkennt Sprache am Artikelinhalt selbst; sonst wird die * gewählte Sprache erzwungen, damit KI-Ausgaben einheitlich bleiben. */ private function ai_generate_meta_variants( $post ) { $title = wp_strip_all_tags( get_the_title( $post ) ); $content = apply_filters( 'the_content', $post->post_content ); $content = preg_replace( '/]*>\s*(SEO\s*·|Lesedauer|Reading time)[^<]*<\/p>/iu', '', $content ); $content = wp_strip_all_tags( $content ); $content = preg_replace( '/\s+/', ' ', trim( $content ) ); if ( mb_strlen( $content ) < 100 ) { return array( 'variants' => array() ); } $o = $this->get_opts(); $language = $o['content_language'] ?? 'auto'; $payload = array( 'secret' => self::stats_secret(), 'site_key' => self::site_key(), 'site_url' => home_url(), 'title' => $title, 'content' => $content, 'action' => 'meta', 'language' => $language, ); $response = wp_remote_post( self::AI_PROXY_URL, array( 'timeout' => 35, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $payload ), 'sslverify' => true ) ); if ( is_wp_error( $response ) ) { return array( 'error' => 'Verbindungsfehler: ' . $response->get_error_message() ); } $http_code = wp_remote_retrieve_response_code( $response ); $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( empty( $body['success'] ) ) { $err = $body['error'] ?? 'Fehler'; if ( $http_code === 429 ) { return array( 'error' => $err, 'limit' => true, 'upgrade_url' => $body['upgrade_url'] ?? 'https://wprankify.de/#pricing' ); } return array( 'error' => $err ); } $variants = array(); foreach ( (array) ( $body['data']['variants'] ?? array() ) as $v ) { $text = trim( (string) ( $v['text'] ?? '' ) ); if ( $text === '' ) { continue; } $variants[] = array( 'strategy' => sanitize_text_field( (string) ( $v['strategy'] ?? '' ) ), 'text' => $text, 'chars' => mb_strlen( $text ), 'score' => max( 0, min( 100, (int) ( $v['score'] ?? 0 ) ) ), ); } return array( 'variants' => $variants, 'quota' => $body['quota'] ?? array() ); } public function aije_save_meta_handler() { // SICHERHEIT: Nonce fehlte komplett (CSRF) — das JS sendet sie bereits mit. check_ajax_referer( 'aije_meta_nonce', 'nonce' ); if ( ! current_user_can( 'edit_posts' ) ) { wp_send_json_error( array( 'msg' => 'Keine Berechtigung.' ) ); } $post_id = intval( $_POST['post_id'] ?? 0 ); // SICHERHEIT: Nur Beiträge, die der Nutzer selbst bearbeiten darf (vorher konnte // jeder Mitarbeiter die Meta-Description JEDES Beitrags überschreiben). if ( ! $post_id || ! get_post( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) { wp_send_json_error( array( 'msg' => 'Keine Berechtigung.' ) ); } $meta = sanitize_text_field( wp_unslash( $_POST['meta_text'] ?? '' ) ); $target = sanitize_key( wp_unslash( $_POST['target'] ?? 'none' ) ); if ( ! $post_id || empty( $meta ) ) { wp_send_json_error( array( 'msg' => 'Ungültige Parameter.' ) ); } switch ( $target ) { case 'rankmath': update_post_meta( $post_id, 'rank_math_description', $meta ); break; case 'aioseo': update_post_meta( $post_id, '_aioseo_description', $meta ); break; case 'none': update_post_meta( $post_id, '_aije_metadesc', $meta ); break; default: update_post_meta( $post_id, '_yoast_wpseo_metadesc', $meta ); break; } update_post_meta( $post_id, '_aije_metadesc', $meta ); clean_post_cache( $post_id ); wp_send_json_success( array( 'msg' => 'Gespeichert.', 'chars' => mb_strlen( $meta ), 'meta' => $meta ) ); } private function render_meta_generator_box( $post ) { $nonce = wp_create_nonce( 'aije_meta_nonce' ); $seo = $this->get_active_seo_plugin(); $target = array( 'rankmath' => 'rankmath', 'aioseo' => 'aioseo', 'yoast' => 'yoast' )[ $seo ] ?? 'none'; $tl = array( 'rankmath' => 'RankMath', 'aioseo' => 'AIOSEO', 'yoast' => 'Yoast', 'none' => 'WP Rankify' )[ $target ] ?? 'WP Rankify'; $ex = get_post_meta( $post->ID, '_yoast_wpseo_metadesc', true ) ?: get_post_meta( $post->ID, 'rank_math_description', true ) ?: get_post_meta( $post->ID, '_aioseo_description', true ) ?: get_post_meta( $post->ID, '_aije_metadesc', true ); $el = mb_strlen( $ex ); $ec = ( $el >= 140 && $el <= 160 ) ? '#22c55e' : ( $el > 0 ? '#f59e0b' : '#ef4444' ); echo '
'; echo '
Meta-Description Generator
'; if ( ! empty( $ex ) ) { echo '
Aktuelle Meta (' . esc_html( $tl ) . ')' . $el . ' Zeichen
' . esc_html( mb_substr( $ex, 0, 120 ) ) . ( $el > 120 ? '...' : '' ) . '
'; } else { echo '
Noch keine Meta-Description vorhanden.
'; } echo ''; echo ''; echo '
'; // JS aus separater Datei laden require plugin_dir_path( AIJE_PLUGIN_FILE ) . 'admin/metabox-meta-js.php'; } public function aije_topic_analysis_handler() { check_ajax_referer( 'aije_topic_nonce', 'nonce' ); if ( ! current_user_can( 'edit_posts' ) ) { wp_send_json_error( array( 'msg' => 'Keine Berechtigung.' ) ); } $post_id = intval( $_POST['post_id'] ?? 0 ); $post = get_post( $post_id ); if ( ! $post ) { wp_send_json_error( array( 'msg' => 'Beitrag nicht gefunden.' ) ); } if ( ! current_user_can( 'edit_post', $post_id ) ) { wp_send_json_error( array( 'msg' => 'Keine Berechtigung.' ) ); } if ( ! in_array( $post->post_status, array( 'publish','draft','pending','private','future' ), true ) ) { wp_send_json_error( array( 'msg' => 'Ungültiger Status.' ) ); } $lock_key = 'aije_analysis_lock_' . $post_id . '_' . get_current_user_id(); if ( get_transient( $lock_key ) ) { wp_send_json_error( array( 'msg' => 'Bitte kurz warten.' ) ); } set_transient( $lock_key, 1, 15 ); $title = get_the_title( $post ) ?: '(Kein Titel)'; $content = // Aufbereitung: HTML-Tags entfernen, Entities dekodieren, Whitespace normalisieren $plain = html_entity_decode( wp_strip_all_tags( $post->post_content ), ENT_QUOTES, 'UTF-8' ); $plain = preg_replace( '/[ \t]+/', ' ', $plain ); $plain = preg_replace( '/\n{3,}/', "\n\n", $plain ); $plain = trim( $plain ); // H2-Überschriften als Inhaltsübersicht extrahieren // Das Modell sieht damit die Struktur des gesamten Artikels – auch was nach dem Textlimit steht preg_match_all( '/]*>(.*?)<\/h2>/is', $post->post_content, $h2_matches ); $h2_list = array(); foreach ( $h2_matches[1] as $h2 ) { $h2_list[] = '- ' . trim( wp_strip_all_tags( $h2 ) ); } $structure_note = ! empty( $h2_list ) ? "Artikelstruktur (alle Abschnitte):\n" . implode( "\n", $h2_list ) . "\n\n" : ''; // Volltext auf 15.000 Zeichen begrenzen (reicht für ~3.000 Wörter) $char_limit = 15000; $is_truncated = mb_strlen( $plain ) > $char_limit; $content = $structure_note . ( $is_truncated ? mb_substr( $plain, 0, $char_limit ) : $plain ); $force = ! empty( $_POST['force'] ); if ( ! $force ) { $cached = get_transient( 'aije_topic_' . $post_id ); if ( $cached ) { wp_send_json_success( array( 'data' => $cached, 'quota' => array(), 'cached' => true ) ); } } $o = $this->get_opts(); $payload = array( 'secret' => self::stats_secret(), 'site_key' => self::site_key(), 'site_url' => home_url(), 'title' => $title, 'content' => $content, 'action' => 'analyze', 'language' => $o['content_language'] ?? 'auto' ); $response = wp_remote_post( self::AI_PROXY_URL, array( 'timeout' => 35, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $payload ), 'sslverify' => true ) ); if ( is_wp_error( $response ) ) { wp_send_json_error( array( 'msg' => 'Verbindungsfehler: ' . $response->get_error_message() ) ); } $http_code = wp_remote_retrieve_response_code( $response ); $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( empty( $body['success'] ) ) { $err = $body['error'] ?? 'Fehler'; if ( $http_code === 429 ) { wp_send_json_error( array( 'msg' => $err, 'limit' => true, 'upgrade_url' => $body['upgrade_url'] ?? 'https://wprankify.de/#pricing' ) ); } wp_send_json_error( array( 'msg' => $err ) ); } delete_transient( 'aije_topic_' . $post_id ); set_transient( 'aije_topic_' . $post_id, $body['data'], DAY_IN_SECONDS ); wp_send_json_success( array( 'data' => $body['data'], 'quota' => $body['quota'] ?? array() ) ); } private function render_topic_analysis_box( $post ) { $nonce = wp_create_nonce( 'aije_topic_nonce' ); $cached = get_transient( 'aije_topic_' . $post->ID ); $pro_status = $this->get_pro_status(); $tier = $pro_status['tier'] ?? 'free'; $remaining = $pro_status['remaining'] ?? 3; $is_pro = ! empty( $pro_status['is_pro'] ); $tc = array( 'free' => '#888', 'starter' => '#22c55e', 'pro' => '#f29400', 'agency' => '#3b82f6' )[ $tier ] ?? '#888'; echo '
'; echo '
🧠 KI-Themen-Analyse
'; echo '
' . esc_html( strtoupper( $tier ) ) . ''; if ( ! $is_pro ) { echo '' . (int) $remaining . ' verbleibend'; } echo '
'; echo ''; echo ''; echo ''; if ( $cached ) { echo '
💾 Letzte Analyse verfügbar.
'; } echo '
'; require plugin_dir_path( AIJE_PLUGIN_FILE ) . 'admin/metabox-topic-js.php'; } public function aije_insert_link_handler() { check_ajax_referer( 'aije_insert_link', 'nonce' ); if ( ! current_user_can( 'edit_posts' ) ) { wp_send_json_error( array( 'msg' => 'Keine Berechtigung.' ) ); } $post_id = intval( $_POST['post_id'] ?? 0 ); $target_id = intval( $_POST['target_id'] ?? 0 ); if ( ! $post_id || ! $target_id || $post_id === $target_id ) { wp_send_json_error( array( 'msg' => 'Ungültige Parameter.' ) ); } $post = get_post( $post_id ); $target = get_post( $target_id ); if ( ! $post || ! $target ) { wp_send_json_error( array( 'msg' => 'Beitrag nicht gefunden.' ) ); } // SICHERHEIT: Bearbeitungsrecht für den Quellbeitrag; Linkziel muss öffentlich sein. if ( ! current_user_can( 'edit_post', $post_id ) ) { wp_send_json_error( array( 'msg' => 'Keine Berechtigung.' ) ); } if ( 'publish' !== get_post_status( $target ) || ! is_post_type_viewable( $target->post_type ) ) { wp_send_json_error( array( 'msg' => 'Linkziel ist nicht öffentlich.' ) ); } $content = $post->post_content; $target_url = get_permalink( $target_id ); $target_title = wp_strip_all_tags( get_the_title( $target_id ) ); $method = ''; $anchor = ''; $ai_info = array(); // mode=backlink: Link wird aus einem ANDEREN Beitrag gesetzt ("Backlink // setzen") → ausschließlich über den KI-Dienst (Kontingent je Tarif). // Ohne mode (Vorschlags-Fenster, Link im eigenen Beitrag): wie bisher // inkl. Stichwort-Methode. $is_backlink = ( 'backlink' === sanitize_key( wp_unslash( $_POST['mode'] ?? '' ) ) ); // ── 1) KI-Verlinkung (Dienst über den Proxy, Kontingent pro Zielbeitrag) ── $ai = $this->ai_suggest_link( $post, $target, $content ); $ai_info = $ai['links'] ?? array(); if ( isset( $ai['paragraph_offset'], $ai['paragraph_length'], $ai['anchor'] ) ) { $para_html = substr( $content, $ai['paragraph_offset'], $ai['paragraph_length'] ); $new_para = self::link_phrase_in_html( $para_html, $ai['anchor'], $target_url, $target_title ); if ( $new_para !== null ) { $content = substr( $content, 0, $ai['paragraph_offset'] ) . $new_para . substr( $content, $ai['paragraph_offset'] + $ai['paragraph_length'] ); $method = 'ai'; $anchor = $ai['anchor']; } } if ( $is_backlink && $method === '' ) { $note = $ai['note'] ?? 'error'; $err = array( 'limit' => 'KI-Kontingent für diesen Beitrag aufgebraucht.', 'no_match' => 'Die KI hat in diesem Beitrag keine passende Stelle gefunden.', 'already_linked' => 'Dieser Beitrag verlinkt bereits hierher.', 'no_paragraphs' => 'Der Beitrag hat keine geeigneten Absätze.', ); wp_send_json_error( array( 'msg' => $err[ $note ] ?? 'KI-Dienst gerade nicht erreichbar. Bitte später erneut versuchen.', 'reason' => $note, 'limit' => ( 'limit' === $note ), 'ai_links' => $ai_info, 'upgrade_url' => 'https://wprankify.de/#pricing', ) ); } // ── 2) Stichwort-Methode (nur für Links im eigenen Beitrag) ── if ( $method === '' ) { foreach ( self::link_keyword_candidates( $target_title ) as $kw ) { $new = self::link_phrase_in_html( $content, $kw, $target_url, $target_title, true ); if ( $new !== null ) { $content = $new; $method = 'keyword'; $anchor = $kw; break; } } } // ── 3) Fallback: Lese-auch-Absatz am Ende ── if ( $method === '' ) { $content .= "\n\n" . '

📖 Wenn dich dieses Thema interessiert, lies auch unseren Artikel: ' . esc_html( $target_title ) . '

'; $method = 'paragraph'; } // Vorher-Stand als Revision sichern → Änderung über "Revisionen" rückgängig machbar. if ( function_exists( 'wp_save_post_revision' ) && post_type_supports( $post->post_type, 'revisions' ) ) { wp_save_post_revision( $post_id ); } global $wpdb; $updated = $wpdb->update( $wpdb->posts, array( 'post_content' => $content, 'post_modified' => current_time( 'mysql' ), 'post_modified_gmt' => current_time( 'mysql', 1 ) ), array( 'ID' => $post_id ), array( '%s', '%s', '%s' ), array( '%d' ) ); clean_post_cache( $post_id ); if ( $updated === false ) { wp_send_json_error( array( 'msg' => 'Datenbankfehler.' ) ); } if ( $method === 'ai' && ! empty( $ai_info ) ) { update_post_meta( $target_id, '_aije_ai_links_used', array( 'used' => (int) ( $ai_info['used'] ?? 0 ), 'limit' => (int) ( $ai_info['limit'] ?? 0 ) ) ); } update_option( 'aije_internal_link_uses', (int) get_option( 'aije_internal_link_uses', 0 ) + 1, false ); $msgs = array( 'ai' => '✓ KI-Link „' . $anchor . '" gesetzt.', 'keyword' => '✓ „' . $anchor . '" verlinkt.', 'paragraph' => '✓ Lese-auch-Absatz eingefügt.', ); wp_send_json_success( array( 'method' => $method, 'keyword' => $anchor, 'msg' => $msgs[ $method ], 'ai_links' => $ai_info, 'ai_note' => $ai['note'] ?? '', 'new_content' => $content, ) ); } /** * Fragt den Proxy (action "link"), welcher Absatz des Quellbeitrags am besten * auf den Zielbeitrag verlinkt und mit welchem Ankertext. Das Kontingent * (KI-Links pro Zielbeitrag je nach Tarif) verwaltet der Proxy. * Rückgabe: paragraph_offset/paragraph_length/anchor bei Erfolg, sonst nur * 'links' (Kontingent) und ggf. 'note' (Grund, warum keine KI genutzt wurde). */ private function ai_suggest_link( $post, $target, $content ) { // Absätze des Quellbeitrags sammeln (ohne den ersten/Intro-Absatz). $segments = self::link_text_segments( $content ); if ( empty( $segments ) ) { return array( 'note' => 'no_paragraphs' ); } $target_url = get_permalink( $target->ID ); $paras = array(); $map = array(); foreach ( $segments as $i => $pm ) { if ( $i === 0 ) { continue; } if ( stripos( $pm[0], $target_url ) !== false ) { return array( 'note' => 'already_linked' ); } $text = trim( preg_replace( '/\s+/', ' ', html_entity_decode( wp_strip_all_tags( $pm[0] ), ENT_QUOTES, 'UTF-8' ) ) ); if ( mb_strlen( $text ) < 40 ) { continue; } $idx = count( $paras ); $paras[] = array( 'i' => $idx, 'text' => mb_substr( $text, 0, 700 ) ); $map[ $idx ] = array( $pm[1], strlen( $pm[0] ) ); if ( count( $paras ) >= 40 ) { break; } } if ( empty( $paras ) ) { return array( 'note' => 'no_paragraphs' ); } $t_text = trim( preg_replace( '/\s+/', ' ', html_entity_decode( wp_strip_all_tags( $target->post_content ), ENT_QUOTES, 'UTF-8' ) ) ); $o = $this->get_opts(); $payload = array( 'secret' => self::stats_secret(), 'site_key' => self::site_key(), 'site_url' => home_url(), 'action' => 'link', 'language' => $o['content_language'] ?? 'auto', 'title' => wp_strip_all_tags( get_the_title( $target ) ), 'content' => mb_substr( $t_text, 0, 600 ), 'paragraphs' => $paras, 'target_post_id' => (int) $target->ID, 'source_post_id' => (int) $post->ID, 'plugin_version' => AIJE_PLUGIN_VERSION, ); $response = wp_remote_post( self::AI_PROXY_URL, array( 'timeout' => 35, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $payload ), 'sslverify' => true ) ); if ( is_wp_error( $response ) ) { return array( 'note' => 'connection' ); } $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! is_array( $body ) ) { return array( 'note' => 'invalid_response' ); } $links = isset( $body['links'] ) && is_array( $body['links'] ) ? $body['links'] : array(); if ( empty( $body['success'] ) ) { return array( 'note' => ! empty( $body['link_limit'] ) ? 'limit' : 'error', 'links' => $links ); } $pi = (int) ( $body['data']['paragraph'] ?? -1 ); $anchor = trim( (string) ( $body['data']['anchor'] ?? '' ) ); if ( $pi < 0 || $anchor === '' || ! isset( $map[ $pi ] ) ) { return array( 'note' => 'no_match', 'links' => $links ); } return array( 'paragraph_offset' => $map[ $pi ][0], 'paragraph_length' => $map[ $pi ][1], 'anchor' => $anchor, 'links' => $links ); } /** * Verlinkt das erste Vorkommen von $phrase in $html — aber NUR in sichtbarem * Text: nicht in Tags/Attributen, HTML-Kommentaren (Block-Editor), bestehenden * Links, Überschriften, Code/Script/Style. Achtet auf Wortgrenzen. * $skip_first_p: ersten Absatz (Einleitung) auslassen. * Gibt das geänderte HTML zurück oder null, wenn nichts gefunden wurde. */ private static function link_phrase_in_html( $html, $phrase, $url, $title, $skip_first_p = false ) { $phrase = trim( (string) $phrase ); if ( $phrase === '' ) { return null; } $parts = preg_split( '/(|<[^>]*>)/s', $html, -1, PREG_SPLIT_DELIM_CAPTURE ); if ( ! is_array( $parts ) ) { return null; } $words = preg_split( '/\s+/u', $phrase ); $pattern = '/(? $part ) { if ( $part === '' ) { continue; } if ( $part[0] === '<' ) { if ( strpos( $part, '