Plugin Directory

Changeset 3780762


Ignore:
Timestamp:
11/02/2026 09:22:02 PM (15 hours ago)
Author:
annimarketing
Message:

Release 1.10.0

Location:
anni-marketing
Files:
26 added
18 edited
1 copied

Legend:

Unmodified
Added
Removed
  • anni-marketing/tags/1.10.0/CREDITS.md

    r3745272 r3780762  
    2020| `includes/rest-media.php` | – | REST-Routen `anni/v1/media/*` (eigener Code) | – |
    2121| `includes/seo.php` | WPPilot 1.17.2 (Commit `7285270`), `includes/kits/seo-basics/src/rank-math.php`, `yoast.php`, `common.php`; `includes/kits/a11y-audit/src/media.php` | Meta-Schlüssel von Rank Math und Yoast, Erkennung der SEO-Plugins über ihre Konstanten, Säubern mit `wp_kses` ohne Tags (Rank-Math-Variablen wie `%sep%` bleiben erhalten), Alt-Text am Medium (`_wp_attachment_image_alt`) mit Vorher-Bild und Prüfung nach dem Schreiben. Freigabe der Felder für die Kern-REST (`register_post_meta`), die Verlauf-Brücke für Kern-REST-Aufrufe und die Rücknahme sind eigener Code. | GPL-2.0-or-later, © 2026 WPPilot |
     22| `includes/seo-felder.php` | eigener Code; Speicherorte aus dem Quelltext von All in One SEO 5.0.4, SEOPress 10.3, The SEO Framework 5.2.0, Slim SEO 4.11.4 (wordpress.org); Verlauf und Rücknahme über `includes/ki-technik.php` (Art `seo_felder`) | SEO-Titel/-Beschreibung je Plugin lesen und schreiben (AIOSEO über sein Modell `Models\Post::savePost`, TSF über `update_single_meta_item`, sonst Post-Meta), nur ins aktive führende Plugin | – (eigener Code) |
    2223| `includes/ladezeit.php` | eigener Code; Verlauf nach dem Muster von `includes/ledger.php` (siehe oben: WPPilot, EMCP) | Sichere Bild-Hebel (fetchpriority/kein lazy fürs LCP-Bild, loading=lazy darunter, decoding=async) über `WP_HTML_Tag_Processor` (WordPress-Kern), Rücknahme über den bestehenden Verlauf | – (eigener Code) |
    23 | `includes/ki-technik.php` | eigener Code; Verlauf nach dem Muster von `includes/ledger.php` und `includes/ladezeit.php` | robots.txt für KI-Suchdienste (Filter `robots_txt`, echte Datei über WP_Filesystem), Firmen-Schema (ergänzt Rank Math/Yoast über deren Schema-Filter), FAQPage, llms.txt, Korrektur der Firmenangaben (Elementor über die Patch-Engine, Block-/Classic-Inhalte, Widgets, Customizer, Ausgabe-Regel), Freischaltung für Suchmaschinen; Rücknahme über den bestehenden Verlauf | – (eigener Code) |
     24| `includes/ki-technik.php` | eigener Code; Verlauf nach dem Muster von `includes/ledger.php` und `includes/ladezeit.php` | robots.txt für KI-Suchdienste (Filter `robots_txt`, echte Datei über WP_Filesystem), Firmen-Schema (ergänzt Rank Math/Yoast über deren Schema-Filter), FAQPage, llms.txt (seit 1.7.0 standardmässig aus), Korrektur der Firmenangaben (Elementor über die Patch-Engine, Block-/Classic-Inhalte, Widgets, Customizer, Ausgabe-Regel), Freischaltung für Suchmaschinen; Rücknahme über den bestehenden Verlauf | – (eigener Code) |
     25| `includes/ki-seiten.php` | eigener Code; Verlauf und Rücknahme über `includes/ki-technik.php` (Art `ki_seiten`) | Kurzfassung, Fragen (FAQPage) und Inhaltsverzeichnis aus H2/H3 auf einer Seite, nur mit Freigabe-ID; Ausgabe über `the_content`, gespeichert je Seite in einer Option | – (eigener Code) |
     26| `includes/ki-links.php` | eigener Code; Verlauf und Rücknahme über `includes/ki-technik.php` (Art `ki_links`) | Hinweis-Link-Absatz («Passend dazu: …») auf einer bestehenden Seite, nur mit Freigabe-ID, Ziele nur veröffentlichte Seiten derselben Website; Ausgabe über `the_content`, gespeichert je Seite in einer Option | – (eigener Code) |
     27| `includes/weiterleitungen.php` | eigener Code; Verlauf und Rücknahme über `includes/ki-technik.php` (Art `weiterleitung`) | 301-Weiterleitung einer alten Adresse auf eine veröffentlichte Seite derselben Website, nur mit Freigabe-ID, nur bei 404, ohne Kreise/Ketten, keine Regel neben einer Weiterleitungs-Erweiterung; gespeichert in einer Option | – (eigener Code) |
     28| `includes/ki-bots.php` | eigener Code | Abrufe durch KI-Dienste je Tag zählen (nur Tag, Name, Anzahl; keine IP, keine Seite), Lesen über `anni/v1/ki-bots` | – (eigener Code) |
     29| `includes/consent-cmp.php` | eigener Code; gleiches Verhalten wie `core/consent.py` (build_cmp_gated_tag) im Anni-Server | Messung hinter dem vorhandenen Cookie-Tool der Website: kein Anni-Hinweis, Google erst nach dessen Statistik-Zustimmung | – (eigener Code) |
     30| `includes/aktualisieren.php` | eigener Code; Ablauf wie `wp_ajax_update_plugin()` und `wp_ajax_toggle_auto_updates()` im WordPress-Kern (GPLv2+) | Aktualisieren ohne Löschen über die WordPress-Aktualisierung (Quelle wordpress.org, kein eigener Update-Server), automatische Aktualisierung nur auf Auftrag | – (eigener Code) |
     31| `includes/consent-grund.php` | eigener Code; gleiche Regel wie `nur_eigen` in `core/consent_push.py` (Messkonzept L7b) | Bei abgeschaltetem Anni-Hinweis und eigenem Google-Code der Website ohne Cookie-Tool keine seitenweite Consent-Grundeinstellung | – (eigener Code) |
     32| `includes/ga4-weitere.php` | eigener Code; Verlauf und Rücknahme über `includes/ki-technik.php` (Art `ga4_weitere`) | Weitere Mess-IDs (Option `anni_ga4_weitere`) entfernen, wenn die Website eine ID selbst lädt (keine Doppelzählung, Messkonzept L7a), Rücknahme über `anni/v1/rollback` | – (eigener Code) |
    2433| `core/wp_ladezeit.py` (Anni-Server, nicht Teil der Erweiterung) | EMCP Tools 3.18.x von Mian Shahzad Raza (msrbuilds), https://github.com/msrbuilds/elementor-mcp (Commit `ebf7a71`), `includes/performance/class-performance-page-audit.php` (`analyze()`, `asset_findings()`) | Server- und HTML-Prüfungen nach Python portiert: Antwortzeit über 800 ms, Komprimierung, Cache-Header, mehr als 5 blockierende Dateien im `<head>`, HTML über 500 KB, fremde Domains. Gemessen wird mit Lighthouse (Apache 2.0, nur wenn auf dem Server installiert) bzw. headless Chromium | GPL-2.0-or-later, © Mian Shahzad Raza |
     34| `includes/bloecke.php` | eigener Code; Verlauf und Konfliktprüfung nach `includes/ledger.php` und `includes/ki-technik.php` | Inhalt von Gutenberg-/klassischen Seiten mit Prüfsumme schreiben (Stand beim Lesen = Stand beim Schreiben), Zurücklesen auf Byte-Gleichheit, Verlauf-Art `bloecke` mit Rücknahme; die Block-Zerlegung (Semantik von `WP_Block_Parser`) läuft im Anni-Server (`core/wp_bloecke.py`) | – (eigener Code) |
     35| `includes/kopffuss.php` | eigener Code; Verlauf und Rücknahme über `includes/bloecke.php` (Art `kopffuss`) | Kopf-/Fussbereich: Vorlagenteile über die WordPress-eigene REST-Route `/wp/v2/template-parts` (wie der Website-Editor; Rücknahme löscht eine neu angelegte Anpassung), Widgets und Customizer-Texte, Prüfsumme und Zurücklesen | – (eigener Code) |
    2536| `includes/haertung.php` | – | Rate-Grenze (429) und Grössengrenze (413) der Schreib-Routen, Fähigkeiten in der Status-Route (eigener Code, Bauplan-Schritt 9) | – |
    2637| `includes/abilities.php` | WordPress Abilities API (Kern, ab 6.9), Muster aus dem Praxistest wp-k3 | Registrierung als Abilities nur wenn vorhanden | – (eigener Code) |
     38| `includes/autorisieren.php` | – | Seite «Anwendung autorisieren» für die Anni-Freigabe: fremde Hinweise nur dort ausblenden, Anleitung über dem Knopf, Anzeige «Anni» (eigener Code, K38) | – |
     39| `includes/sicherheit.php` | – | Sicherheitsprüfung von innen, nur lesend: Kern-Prüfsummen (`get_core_checksums`), PHP-Dateien im Upload-Ordner, Pflicht-Erweiterungen und Drop-ins mit Musterprüfung (eigener Code, K44) | – |
    2740
    2841## Bewusste Abweichungen von den Vorlagen
  • anni-marketing/tags/1.10.0/anni-consent.js

    r3756924 r3780762  
    5757    if (mode === 'basic') {
    5858      gt()('js', new Date()); gt()('config', C.m);
     59      (C.w || []).forEach(function (w) { if (/^G-[A-Z0-9]{4,20}$/.test(w)) gt()('config', w); });   // 1.5.0: weitere Mess-IDs zählen mit
    5960      var s = D.createElement('script'); s.async = true; s.src = 'https://www.googletagmanager.com/gtag/js?id=' + encodeURIComponent(C.m);
    6061      (D.head || D.documentElement).appendChild(s);
    … …  
    6364  function apply(st) {
    6465    var g = gt();
     66    // Messkonzept L7b (30.10.2026): C.nur_eigen = die Website hat eigenen Google-Code ohne Cookie-Tool. Kein seitenweites
     67    // consent update (es veränderte deren Zahlen); Annis Messung lädt ohnehin erst nach Zustimmung (basic).
     68    if (C.nur_eigen) {
     69      try { W.dataLayer.push({ event: 'anni_consent_update', consent_statistik: !!st.s, consent_marketing: !!st.m }); } catch (e) {}
     70      if (st.s) loadGoogle();
     71      return;
     72    }
    6573    g('consent', 'update', {
    6674      analytics_storage: st.s ? 'granted' : 'denied',
    … …  
    7280    if (st.s) loadGoogle();
    7381  }
     82  // 28.10.2026 (Befund: ein Headless-Screenshot klickte «Alle akzeptieren» und wurde dem Kunden gezählt):
     83  // automatisierte Browser und bekannte Bots zählen nie mit. Der Server filtert den User-Agent zusätzlich.
     84  var BOT_UA = /Headless|PhantomJS|Puppeteer|Playwright|Selenium|webdriver|Lighthouse|PageSpeed|GTmetrix|Pingdom|UptimeRobot|bot[\/_\-]|\bbot\b|crawler|spider|slurp|facebookexternalhit|curl\/|wget\/|python-requests|HttpClient/i;
     85  function automat() {
     86    try {
     87      var n = W.navigator || {};
     88      if (n.webdriver === true) return true;
     89      return BOT_UA.test(String(n.userAgent || ''));
     90    } catch (e) { return false; }
     91  }
    7492  function zaehlen(st) {
    7593    if (!C.z) return;
     94    if (automat()) return;
    7695    var e = (st.s && (!kat.marketing || st.m)) ? 'granted' : (!st.s && !st.m ? 'denied' : 'partial');
    7796    try {
    … …  
    100119    '.w{position:fixed;left:0;right:0;bottom:0;z-index:2147483646;margin:0 auto;max-width:64rem;background:' + c.bg + ';color:' + c.fg + ';border:1px solid ' + c.border + ';border-bottom:none;border-radius:12px 12px 0 0;box-shadow:0 -2px 24px rgba(0,0,0,.13);padding:16px 20px 14px;display:flex;flex-wrap:wrap;gap:14px;align-items:flex-start;justify-content:space-between}' +
    101120    '.f{margin:0 0 4px;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:' + c.muted + '}' +
     121    '.lg{display:inline-flex;align-items:center;vertical-align:middle;margin:0 8px 2px 0}.lg img{display:block;max-height:26px;max-width:120px}' +
     122    '.lg.dk{background:#2a211c;padding:4px 8px;border-radius:6px}' +
    102123    '.h{margin:0 0 6px;font-size:15px;font-weight:650;line-height:1.35}' +
    103124    '.p{margin:0;font-size:13.5px;line-height:1.55;color:' + c.muted + ';max-width:62ch}' +
    … …  
    147168    var sh = root()._sh;
    148169    if (sh.querySelector('.w')) return;
    149     var firm = C.f ? '<p class="f">' + esc(C.f) + '</p>' : '';
     170    // Punkt 17: Logo der Website vor dem Namen; helles Logo (lgd) auf dunklem Grund. Nur https der Kundenseite oder ein
     171    // eingebettetes Bild (data-URI) — nie eine Adresse von Anni vor der Einwilligung (Plugin prüft es auch).
     172    var lg = (C.lg && /^(https:\/\/|data:image\/(png|webp);base64,)/.test(C.lg)) ? '<span class="lg' + (C.lgd ? ' dk' : '') + '"><img src="' + esc(C.lg) + '" alt=""></span>' : '';
     173    var firm = (C.f || lg) ? '<p class="f">' + lg + esc(C.f || '') + '</p>' : '';
    150174    var link = C.u ? '<a href="' + esc(C.u) + '">' + esc(t.link) + '</a>' : '';
    151175    var w = el('<div class="w" role="dialog" aria-modal="false" aria-label="' + esc(t.aria) + '"><div class="x">' + firm +
    … …  
    154178      '<div class="b"><button type="button" data-a="no">' + esc(t.reject) + '</button><button type="button" class="ok" data-a="yes">' + esc(t.accept) + '</button></div></div>');
    155179    sh.appendChild(w);
     180    var li = w.querySelector('.lg img'); if (li) li.addEventListener('error', function () { li.parentNode.parentNode.removeChild(li.parentNode); });
    156181    w.querySelector('[data-a="yes"]').addEventListener('click', function () { decide({ s: true, m: !!kat.marketing }, 'alle'); });
    157182    w.querySelector('[data-a="no"]').addEventListener('click', function () { decide({ s: false, m: false }, 'notwendig'); });
  • anni-marketing/tags/1.10.0/anni-marketing.php

    r3757258 r3780762  
    44 * Plugin URI: https://anni-marketing.ch
    55 * Description: Verbindet deine Website mit Anni (anni-marketing.ch): Statistik-Messcode mit Einwilligungs-Hinweis und Status-Kanal. Keine Daten an Dritte, kein Tracking durch das Plugin selbst.
    6  * Version: 1.4.3
     6 * Version: 1.10.0
    77 * Author: Anni Marketing
    88 * Author URI: https://digitalerakete.ch
    … …  
    1616if (!defined('ABSPATH')) { exit; }
    1717
    18 define('ANNI_MARKETING_VERSION', '1.4.3');
     18define('ANNI_MARKETING_VERSION', '1.10.0');
    1919
    2020// Seitenbearbeitung (Bauplan 07.10.2026, Schritte 0-2): Schema, Änderungsmotor, Verlauf, REST, Abilities.
    … …  
    3232require_once __DIR__ . '/includes/rest-media.php';
    3333require_once __DIR__ . '/includes/seo.php';
     34// 1.9.0 (W1.39): SEO-Titel/-Beschreibung auch für All in One SEO, SEOPress, The SEO Framework und Slim SEO (rücknehmbar).
     35require_once __DIR__ . '/includes/seo-felder.php';
    3436// Ladezeit (Bauplan 07.10.2026): sichere Bild-Hebel, einzeln schaltbar, Messung auf dem Anni-Server.
    3537require_once __DIR__ . '/includes/ladezeit.php';
    3638require_once __DIR__ . '/includes/ki-technik.php';
     39// 1.7.0 (G13/G20 KI-Suche): Kurzfassung/Fragen/Inhaltsverzeichnis auf Seiten (nur mit Freigabe), Abrufe durch KI-Dienste zählen.
     40require_once __DIR__ . '/includes/ki-seiten.php';
     41require_once __DIR__ . '/includes/ki-bots.php';
     42// 1.8.0 (S41 Interne Verlinkung): Hinweis-Links auf bestehenden Seiten (nur mit Freigabe, rücknehmbar).
     43require_once __DIR__ . '/includes/ki-links.php';
     44// 1.9.0 (W1.36/W1.31 Gutenberg/klassisch): Bild tauschen und Abschnitte umstellen, mit Prüfsumme, Verlauf und Rücknahme.
     45require_once __DIR__ . '/includes/bloecke.php';
     46// 1.9.0 (W1.73): Kopf-/Fussbereich (Vorlagenteile, Widgets, Customizer-Texte) mit Bestätigung, Verlauf und Rücknahme.
     47require_once __DIR__ . '/includes/kopffuss.php';
     48// 1.9.0 (W1.72): 301-Weiterleitungen ohne Fremd-Plugin (nur mit Freigabe, rücknehmbar, keine zweite Regel neben einer Weiterleitungs-Erweiterung).
     49require_once __DIR__ . '/includes/weiterleitungen.php';
     50// 1.7.0 (28.10.2026): Messung hinter dem vorhandenen Cookie-Tool der Website, dann kein Anni-Hinweis.
     51require_once __DIR__ . '/includes/consent-cmp.php';
     52// 1.9.2 (Messkonzept L7a): weitere Mess-IDs entfernen mit Verlauf und Ruecknahme
     53require_once __DIR__ . '/includes/ga4-weitere.php';
     54// 1.9.3 (B7): Consent-Grundeinstellung auch bei abgeschaltetem Hinweis nur fuer Annis Messung
     55require_once __DIR__ . '/includes/consent-grund.php';
     56// 1.10.0 (Befund Kalex 02.11.2026): aktualisieren wie «Jetzt aktualisieren» in wp-admin — ohne Löschen, Verlauf und
     57// Einstellungen bleiben. Quelle bleibt die WordPress-Aktualisierung (wordpress.org), kein eigener Update-Server.
     58require_once __DIR__ . '/includes/aktualisieren.php';
    3759// Härtung (Bauplan-Schritt 9): Rate- und Grössengrenze der Schreib-Routen, Fähigkeiten im Status.
    3860require_once __DIR__ . '/includes/haertung.php';
     61// K38: Seite «Anwendung autorisieren» bei der Anni-Verbindung aufgeräumt (nur dort, nur für Anni-Rückkehr-Adressen).
     62require_once __DIR__ . '/includes/autorisieren.php';
     63// K44 (1.6.0): Sicherheitsprüfung von innen, nur lesend (Kerndateien, Upload-Ordner, versteckte Erweiterungen).
     64require_once __DIR__ . '/includes/sicherheit.php';
    3965
    4066/** WPP/GWE (01.09.2026, Echtkundin-Befund): Security-Plugins schalten WordPress-
    … …  
    5177        $name = sanitize_text_field(wp_unslash((string) $_REQUEST['app_name']));
    5278        if (strpos($name, 'Anni') !== 0) { return $verfuegbar; }
     79        // K38 (Leitstelle 23.10.2026): der App-Name allein ist frei wählbar — nur Annis eigener Rückweg setzt die
     80        // Entscheidung des Security-Plugins ausser Kraft, sonst bekäme ein fremder Link «Anni …» das Kennwort.
     81        $ziel = isset($_REQUEST['success_url']) ? wp_unslash((string) $_REQUEST['success_url']) : '';
     82        if (!anni_marketing_ist_anni_ziel($ziel)) { return $verfuegbar; }
    5383    }
    5484    return true;
    … …  
    6393    $name = isset($request['app_name']) ? (string) $request['app_name'] : '';
    6494    if (strpos($name, 'Anni') !== 0) { return; }
     95    if (!anni_marketing_ist_anni_ziel(isset($request['success_url']) ? (string) $request['success_url'] : '')) { return; }   // K38
    6596    foreach ($error->get_error_codes() as $code) { $error->remove($code); }
    6697}, PHP_INT_MAX, 3);
    … …  
    107138 * Modus basic (Standard): Google-Skript wird erst nach Zustimmung vom Hinweis nachgeladen.
    108139 * Modus advanced: Google-Skript laedt sofort mit "denied" (cookielose Pings, Google modelliert). */
     140/** 1.5.0 (23.10.2026): weitere Mess-IDs neben Annis eigener (z. B. der bisherige Code der Kundin oder einer Agentur).
     141 * Anni ersetzt nie einen fremden Code: er bleibt aktiv, Annis ID zaehlt zusaetzlich. Hoechstens 3, nur G-IDs. */
     142function anni_marketing_weitere($ohne = '') {
     143    $raw = get_option('anni_ga4_weitere', '');
     144    $liste = $raw ? json_decode($raw, true) : array();
     145    if (!is_array($liste)) { $liste = array(); }
     146    $out = array();
     147    foreach ($liste as $id) {
     148        $id = (string) $id;
     149        if ($id !== $ohne && preg_match('/^G-[A-Z0-9]{4,20}$/', $id) && !in_array($id, $out, true)) { $out[] = $id; }
     150    }
     151    return array_slice($out, 0, 3);
     152}
     153
    109154function anni_marketing_consent_config() {
    110155    $mid = get_option('anni_ga4_id', '');
    … …  
    113158    $cfg = $raw ? json_decode($raw, true) : null;
    114159    if (!is_array($cfg)) { $cfg = array(); }
    115     if (isset($cfg['enabled']) && $cfg['enabled'] === false) { return array('enabled' => false, 'm' => $mid); }
     160    // 1.9.3 (B7): auch bei abgeschaltetem Hinweis die Angabe «nur_eigen» behalten (includes/consent-grund.php)
     161    if (isset($cfg['enabled']) && $cfg['enabled'] === false) { return anni_consent_aus($cfg, $mid); }
     162    // 1.7.0: gekoppelt mit dem Cookie-Tool der Website -> kein Anni-Hinweis, Google nur nach dessen Zustimmung.
     163    $cmp = isset($cfg['cmp']) ? anni_marketing_cmp_pruefen($cfg['cmp']) : null;
     164    if ($cmp) { return array('enabled' => true, 'cmp' => $cmp, 'm' => $mid, 'w' => anni_marketing_weitere($mid)); }
    116165    $priv = '';
    117166    if (function_exists('get_privacy_policy_url')) { $priv = (string) get_privacy_policy_url(); }
    … …  
    137186    if (empty($out['f'])) { $out['f'] = (string) get_bloginfo('name'); }
    138187    $out['m'] = $mid;
     188    $out['w'] = anni_marketing_weitere($mid);
    139189    $out['enabled'] = true;
    140190    unset($out['_inline']);
    … …  
    153203        . '"ad_user_data":"denied","ad_personalization":"denied","wait_for_update":500});';
    154204    if (empty($cfg['enabled'])) {
     205        // 1.9.3 (B7): eigener Google-Code der Website ohne Cookie-Tool → keine seitenweite «denied»-Zeile; Anni misst
     206        // ohne Hinweis ohnehin nicht (kein Weg zur Zustimmung), die Zeile wuerde nur die Zahlen der Kundin senken.
     207        if (!anni_consent_grundzeile_noetig($cfg)) { return; }
    155208        wp_register_script('anni-marketing-consent-default', false, array(), ANNI_MARKETING_VERSION, false);
    156209        wp_enqueue_script('anni-marketing-consent-default');
    … …  
    158211        return;
    159212    }
     213    if (!empty($cfg['cmp'])) {
     214        // Kein zweiter Hinweis: das Cookie-Tool der Website fragt, Google laedt erst nach dessen Zustimmung.
     215        wp_register_script('anni-marketing-cmp', false, array(), ANNI_MARKETING_VERSION, false);
     216        wp_enqueue_script('anni-marketing-cmp');
     217        wp_add_inline_script('anni-marketing-cmp', anni_marketing_cmp_skript($cfg['cmp'], $mid, $cfg['w']));
     218        return;
     219    }
    160220    if ($cfg['mode'] === 'advanced') {
    161221        // Google sofort laden (mit "denied"), der Hinweis schaltet spaeter per consent update frei.
    162222        wp_enqueue_script('anni-marketing-gtag', 'https://www.googletagmanager.com/gtag/js?id=' . rawurlencode($mid), array(), ANNI_MARKETING_VERSION, false);
    163223        wp_script_add_data('anni-marketing-gtag', 'strategy', 'async');
    164         wp_add_inline_script('anni-marketing-gtag', $default . 'gtag("config","' . esc_js($mid) . '");', 'before');
     224        $configs = 'gtag("config","' . esc_js($mid) . '");';
     225        foreach ((array) $cfg['w'] as $wid) { $configs .= 'gtag("config","' . esc_js($wid) . '");'; }
     226        wp_add_inline_script('anni-marketing-gtag', $default . $configs, 'before');
    165227        $dep = array('anni-marketing-gtag');
     228    } elseif (!empty($cfg['nur_eigen'])) {
     229        // L7b: kein seitenweiter Consent-Default — die Google-Tags der Website laufen wie ohne Anni
     230        $dep = array();
    166231    } else {
    167232        wp_register_script('anni-marketing-consent-default', false, array(), ANNI_MARKETING_VERSION, false);
    … …  
    190255                'ok'      => true,
    191256                'version' => ANNI_MARKETING_VERSION,
     257                // 1.4.4: PHP- und WordPress-Version, damit Anni veraltete bzw. verwundbare Versionen erkennt
     258                // (nur fuer Administratoren, wie der ganze Endpunkt).
     259                'php'     => PHP_VERSION,
     260                'wp'      => (string) get_bloginfo('version'),
    192261                'ga4_id'  => (string) get_option('anni_ga4_id', ''),
    193262                'consent' => (bool) get_option('anni_consent', ''),
    194                 'faehigkeiten' => array('patch', 'verlauf'),
     263                'ga4_weitere' => anni_marketing_weitere((string) get_option('anni_ga4_id', '')),
     264                'faehigkeiten' => array('patch', 'verlauf', 'ga4_weitere', 'cmp', 'ga4_weitere_entfernen', 'nur_eigen', 'ga4_weitere_rueckweg', 'aktualisieren'),
    195265            );
    196266        },
    … …  
    218288            $out = array();
    219289            $out['enabled'] = !(isset($cfg['enabled']) && $cfg['enabled'] === false);
     290            // 1.7.0: Kopplung an das Cookie-Tool der Website (fehlt/ungueltig -> Anni-Hinweis wie bisher)
     291            $cmp = isset($cfg['cmp']) ? anni_marketing_cmp_pruefen($cfg['cmp']) : null;
     292            if ($cmp) { $out['cmp'] = $cmp; }
    220293            $out['mode'] = (isset($cfg['mode']) && $cfg['mode'] === 'advanced') ? 'advanced' : 'basic';
     294            // Messkonzept L7b (30.10.2026): laeuft eigener GA-Code der Website ohne Cookie-Tool, gilt Annis Grundeinstellung
     295            // nur fuer Annis Messung: keine seitenweite «denied»-Zeile, kein consent update, Google erst nach Zustimmung.
     296            $out['nur_eigen'] = !empty($cfg['nur_eigen']);
     297            if ($out['nur_eigen']) { $out['mode'] = 'basic'; }
    221298            $out['gleichfarbig'] = !empty($cfg['gleichfarbig']);
    222299            $out['kat'] = array('statistik' => true, 'marketing' => !empty($cfg['kat']['marketing']));
    223300            $out['f'] = isset($cfg['f']) ? sanitize_text_field(mb_substr((string) $cfg['f'], 0, 80)) : '';
     301            $lg = isset($cfg['lg']) ? trim((string) $cfg['lg']) : '';   // Logo im Hinweis (nur https, keine Anführungszeichen)
     302            $out['lg'] = ($lg !== '' && (preg_match('#^https://[\w\-.]+(?::\d+)?/[\w\-./%~+]{1,280}$#', $lg)
     303                || preg_match('#^data:image/(?:png|webp);base64,[A-Za-z0-9+/]{1,41000}={0,2}$#', $lg))) ? $lg : '';
     304            $out['lgd'] = !empty($cfg['lgd']);
    224305            $u = isset($cfg['u']) ? trim((string) $cfg['u']) : '';
    225306            $out['u'] = ($u !== '' && preg_match('#^(?:/|https?://)[\w\-./:?=&%\#~]{1,300}$#', $u)) ? $u : '';
    … …  
    239320            }
    240321            update_option('anni_consent', wp_json_encode($out, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), true);
    241             return array('ok' => true, 'consent' => true, 'mode' => $out['mode']);
     322            return array('ok' => true, 'consent' => true, 'mode' => $out['mode'], 'cmp' => !empty($out['cmp']));
    242323        },
    243324    ));
    … …  
    250331                return new WP_Error('anni_bad_id', 'Ungueltige Mess-ID.', array('status' => 400));
    251332            }
     333            // 1.5.0: beibehalten=true → eine bisher hinterlegte, andere Mess-ID bleibt als weitere aktiv (nie ersetzen)
     334            $alt = (string) get_option('anni_ga4_id', '');
     335            if ($req->get_param('beibehalten') && $alt !== '' && $alt !== $mid && preg_match('/^G-[A-Z0-9]{4,20}$/', $alt)) {
     336                $w = anni_marketing_weitere($mid);
     337                if (!in_array($alt, $w, true)) {
     338                    // voll: lieber nichts aendern als einen fremden Code still fallen zu lassen
     339                    if (count($w) >= 3) { return new WP_Error('anni_weitere_voll', 'Zu viele weitere Mess-IDs.', array('status' => 409)); }
     340                    $w[] = $alt;
     341                }
     342                update_option('anni_ga4_weitere', wp_json_encode($w), true);
     343            }
     344            // Messkonzept L7a (30.10.2026, 1.9.2): weitere ID entfernen, wenn die Website sie inzwischen selbst laedt (sonst
     345            // doppelt). Mit Eintrag im Verlauf (Art «ga4_weitere») — Ruecknahme ueber anni/v1/rollback/{change_id}.
     346            $weg = $req->get_param('entfernen');
     347            $change_id = 0;
     348            if (is_array($weg) && $weg) {
     349                $r = anni_ga4_weitere_ohne(anni_marketing_weitere($mid), $weg);
     350                if (is_wp_error($r)) { return $r; }
     351                $change_id = (int) $r;
     352            }
    252353            update_option('anni_ga4_id', $mid, true);
    253             return array('ok' => true, 'ga4_id' => $mid);
     354            return array('ok' => true, 'ga4_id' => $mid, 'ga4_weitere' => anni_marketing_weitere($mid), 'change_id' => $change_id);
    254355        },
    255356    ));
  • anni-marketing/tags/1.10.0/includes/ki-technik.php

    r3757258 r3780762  
    1414 *   GET  anni/v1/ki-technik             Stand, Erkennung (robots.txt-Datei, blog_public, fremdes Schema), Verlauf
    1515 *   POST anni/v1/ki-technik             {robots?:{suchbots:true, training:"erlauben|sperren|unveraendert"},
    16  *                                        firma?:{JSON-LD}|null, faq?:bool, llms?:bool, llms_beschreibung?:string}
     16 *                                        firma?:{JSON-LD}|null, faq?:bool, llms?:bool, llms_beschreibung?:string,
     17 *                                        indexnow?:string (1.7.0: IndexNow-Schlüssel, 8–128 Zeichen a-f 0-9 -; "" = aus)}
    1718 *                                       Liegt eine echte robots.txt-Datei im Website-Ordner, schreibt das Plugin
    1819 *                                       sie (WP_Filesystem, Vorher-Stand im Verlauf); sonst Filter robots_txt.
    … …  
    2021 *   POST anni/v1/ki-technik/nap         {ersetzungen:[{von,nach,feld}], freigabe, dry_run?, auffangnetz?}
    2122 *                                       oder {art:"block_ergaenzen", seite, inhalt, freigabe}
     23 *   POST anni/v1/ki-technik/seiten      Kurzfassung, Fragen und Inhaltsverzeichnis auf einer Seite (1.7.0, includes/ki-seiten.php)
     24 *   POST anni/v1/ki-technik/links       Hinweis-Links auf einer bestehenden Seite (1.8.0, includes/ki-links.php)
     25 *   GET  anni/v1/ki-bots                Abrufe durch KI-Dienste je Tag (1.7.0, includes/ki-bots.php)
    2226 *   Rücknahme über anni/v1/rollback/{change_id}.
     27 *
     28 * llms.txt (1.7.0): standardmässig aus und kein beworbener Teil mehr (diva-e/Adobe 10/2026: hat sich nicht durchgesetzt,
     29 * Standard bleibt robots.txt). Wer sie schon eingeschaltet hat, behält sie; Anni schaltet sie nicht mehr ein.
    2330 *
    2431 * Herkunft: eigener Code; Verlauf und Konfliktprüfung nach includes/ledger.php und includes/ladezeit.php.
    … …  
    6976        'firma' => isset($roh['firma']) && is_array($roh['firma']) ? $roh['firma'] : null,
    7077        'faq' => !empty($roh['faq']),
    71         'llms' => !empty($roh['llms']),
     78        'llms' => isset($roh['llms']) && $roh['llms'] === true,   // 1.7.0: ohne ausdrückliches «an» bleibt llms.txt aus
    7279        'llms_beschreibung' => isset($roh['llms_beschreibung']) ? (string) $roh['llms_beschreibung'] : '',
     80        'indexnow' => isset($roh['indexnow']) && anni_ki_indexnow_gueltig($roh['indexnow']) ? (string) $roh['indexnow'] : '',
    7381        'ersetzen' => $regeln,
    7482    );
     83}
     84
     85/** 1.7.0: IndexNow-Schlüssel nach indexnow.org (8–128 Zeichen: a-z, A-Z, 0-9, Bindestrich). */
     86function anni_ki_indexnow_gueltig($k) {
     87    return is_string($k) && preg_match('/^[A-Za-z0-9\-]{8,128}$/', $k) === 1;
    7588}
    7689
    … …  
    172185            return array(function () use ($opt) { return get_option($opt); },
    173186                function ($w) use ($opt) { update_option($opt, $w); return true; });
     187        case 'ki_seiten':
     188            // 1.7.0: Bausteine EINER Seite (Kurzfassung, Fragen, Inhaltsverzeichnis) — Rücknahme betrifft nur diese Seite.
     189            $pid = (int) $doc_id;
     190            if ($pid <= 0 || !function_exists('anni_ki_seite_lesen')) { return null; }
     191            return array(function () use ($pid) { return anni_ki_seite_lesen($pid); },
     192                function ($w) use ($pid) { return anni_ki_seite_setzen($pid, $w); });
     193        case 'ki_links':
     194            // 1.8.0 (S41): Hinweis-Links EINER Seite — Rücknahme betrifft nur diese Seite, post_content bleibt unberührt.
     195            $pid = (int) $doc_id;
     196            if ($pid <= 0 || !function_exists('anni_ki_links_lesen')) { return null; }
     197            return array(function () use ($pid) { return anni_ki_links_lesen($pid); },
     198                function ($w) use ($pid) { return anni_ki_links_setzen($pid, $w); });
    174199        case 'ki_inhalt':
    175200            $pid = (int) $doc_id;
    … …  
    181206                });
    182207    }
    183     return null;
     208    // 1.9.0: Arten aus eigenen Dateien (Filter anni_ki_arten_weitere) — array(lesen, setzen) oder null
     209    $weitere = function_exists('apply_filters') ? apply_filters('anni_ki_zugriff_weitere', null, $art, $doc_id, $ziel) : null;
     210    return is_array($weitere) && count($weitere) === 2 && is_callable($weitere[0]) && is_callable($weitere[1]) ? $weitere : null;
    184211}
    185212
    … …  
    203230
    204231function anni_ki_arten() {
    205     return array('ki_technik', 'ki_sichtbar', 'ki_robots_datei', 'ki_widget', 'ki_thememod', 'ki_inhalt');
     232    $a = array('ki_technik', 'ki_sichtbar', 'ki_robots_datei', 'ki_widget', 'ki_thememod', 'ki_inhalt', 'ki_seiten', 'ki_links');
     233    // 1.9.0: weitere Arten aus eigenen Dateien (weiterleitungen.php, seo-felder.php) — sie bringen Leser/Schreiber selbst mit
     234    return array_values(array_unique(array_merge($a, array_filter((array) (function_exists('apply_filters') ? apply_filters('anni_ki_arten_weitere', array()) : array()), 'is_string'))));
    206235}
    207236
    … …  
    250279    $t = anni_ledger_tabelle();
    251280    $a = anni_ki_arten();
     281    $platz = implode(', ', array_fill(0, count($a), '%s'));   // 1.7.0: so viele Platzhalter wie Arten (vorher fest 6)
    252282    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- eigene Verlaufstabelle, Name aus $wpdb->prefix
    253283    $zeilen = $wpdb->get_results($wpdb->prepare(
    254         // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Tabellenname aus anni_ledger_tabelle()
    255         "SELECT id, doc_id, element_ids, aktion, auftrag, ruecknahme_von, zurueckgenommen_durch, erstellt, nachher FROM {$t} WHERE element_ids IN (%s, %s, %s, %s, %s, %s) ORDER BY id DESC LIMIT %d",
    256         $a[0], $a[1], $a[2], $a[3], $a[4], $a[5], max(1, min(50, (int) $limit))), ARRAY_A) ?: array();
     284        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Tabellenname aus anni_ledger_tabelle(), Platzhalter gezählt
     285        "SELECT id, doc_id, element_ids, aktion, auftrag, ruecknahme_von, zurueckgenommen_durch, erstellt, nachher FROM {$t} WHERE element_ids IN ({$platz}) ORDER BY id DESC LIMIT %d",
     286        array_merge($a, array(max(1, min(50, (int) $limit))))), ARRAY_A) ?: array();
    257287    $out = array();
    258288    foreach ($zeilen as $z) {
    … …  
    856886        }
    857887    }
    858     if ($cfg['faq'] && !empty($kontext['beitrag']) && (string) $kontext['inhalt'] !== '') {
     888    // 1.7.0: freigegebene Fragen einer Seite (includes/ki-seiten.php) → FAQPage aus genau diesen Fragen. Dann kein zweites
     889    // FAQPage aus den Überschriften; hat die Seite schon ein fremdes FAQ-Schema, bleibt es beim sichtbaren Block.
     890    $seiten_faq = !empty($kontext['seite_faq']) && function_exists('anni_ki_seite_faq_schema') ? anni_ki_seite_faq_schema($kontext['seite_faq']) : null;
     891    if ($seiten_faq) {
     892        $vorhanden = $vorhanden ?: anni_ki_html_schema($html);
     893        if (!$vorhanden['faq']) { $zusatz .= anni_ki_ld_tag($seiten_faq, 'anni-seiten-faq'); }
     894    } elseif ($cfg['faq'] && !empty($kontext['beitrag']) && (string) $kontext['inhalt'] !== '') {
    859895        $vorhanden = $vorhanden ?: anni_ki_html_schema($html);
    860896        $faq = $vorhanden['faq'] ? null : anni_ki_faq_schema($kontext['inhalt']);
    … …  
    875911    if ($m !== 'GET' && $m !== 'HEAD') { return false; }
    876912    $c = anni_ki_holen();
    877     return $c['firma'] || $c['faq'] || $c['ersetzen'];
     913    return $c['firma'] || $c['faq'] || $c['ersetzen'] || (function_exists('anni_ki_seiten_alle') && anni_ki_seiten_alle());
    878914}
    879915
    … …  
    882918    $startseite = is_front_page();
    883919    $beitrag = is_singular() && !$startseite;
    884     ob_start(function ($html) use ($startseite, $beitrag) {
     920    $seite = is_singular() && function_exists('anni_ki_seite_lesen') ? anni_ki_seite_lesen((int) get_queried_object_id()) : null;
     921    $seite_faq = is_array($seite) && !empty($seite['faq']) ? $seite['faq'] : array();
     922    ob_start(function ($html) use ($startseite, $beitrag, $seite_faq) {
    885923        foreach (headers_list() as $kopf) {
    886924            if (stripos($kopf, 'content-type:') === 0 && stripos($kopf, 'text/html') === false) { return $html; }
    887925        }
    888926        return anni_ki_html_anwenden($html, anni_ki_holen(), array('startseite' => $startseite, 'beitrag' => $beitrag,
    889             'inhalt' => $GLOBALS['anni_ki_inhalt'], 'seo_weg' => $GLOBALS['anni_ki_schema_weg']));
     927            'inhalt' => $GLOBALS['anni_ki_inhalt'], 'seo_weg' => $GLOBALS['anni_ki_schema_weg'], 'seite_faq' => $seite_faq));
    890928    });
    891929}, PHP_INT_MAX - 1);
    892930
    893 /* ── llms.txt (nicht beworben: kein Link, kein Eintrag in robots/sitemap) ────────────────── */
     931/* ── llms.txt (nicht beworben: kein Link, kein Eintrag in robots/sitemap; seit 1.7.0 standardmässig aus) ── */
    894932
    895933/*
    … …  
    9661004}, 0);
    9671005
     1006/* ── IndexNow-Schlüsseldatei (1.7.0) ─────────────────────────────────────────────────────────
     1007 * Bing bestätigt einen IndexNow-Ping nur, wenn https://<website>/<schlüssel>.txt genau den Schlüssel liefert. Anni setzt
     1008 * den Schlüssel über anni/v1/ki-technik; die Datei wird wie /llms.txt ohne Rewrite-Regel beim Start beantwortet. Kein
     1009 * Eintrag in robots.txt oder Sitemap, keine Daten nach aussen. */
     1010/** Schlüssel, wenn $pfad genau die Schlüsseldatei ist (Schalter an, Schlüssel gesetzt), sonst null. */
     1011function anni_ki_indexnow_antwort($pfad) {
     1012    if (!anni_ki_erlaubt()) { return null; }
     1013    $cfg = anni_ki_holen();
     1014    if ($cfg['indexnow'] === '') { return null; }
     1015    $soll = trailingslashit((string) wp_parse_url(home_url('/'), PHP_URL_PATH)) . $cfg['indexnow'] . '.txt';
     1016    return (string) $pfad === $soll ? $cfg['indexnow'] : null;
     1017}
     1018
     1019add_action('parse_request', function () {
     1020    $pfad = isset($_SERVER['REQUEST_URI']) ? (string) wp_parse_url(wp_unslash($_SERVER['REQUEST_URI']), PHP_URL_PATH) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
     1021    $key = anni_ki_indexnow_antwort($pfad);
     1022    if ($key === null) { return; }
     1023    status_header(200);
     1024    header('Content-Type: text/plain; charset=utf-8');
     1025    header('Cache-Control: public, max-age=3600');
     1026    echo $key; // phpcs:ignore WordPress.Security.EscapeOutput -- geprüfter Schlüssel (a-z, 0-9, -)
     1027    exit;
     1028}, 0);
     1029
    9681030/* ── Einstellungsseite: Zustimmung ───────────────────────────────────────────────────────── */
    9691031
    … …  
    9831045        echo '<p><label><input type="checkbox" name="' . esc_attr(ANNI_KI_ERLAUBT) . '" value="1" ' . checked(anni_ki_erlaubt(), true, false) . '> '
    9841046            . 'Anni darf Website-Technik anpassen</label></p>';
    985         echo '<p class="description">Firmen-Angaben für Suchmaschinen (Schema), Fragen-Schema in Beiträgen, Zugang für KI-Suchdienste in der robots.txt, '
    986             . 'llms.txt und Korrekturen deiner Firmenangaben. Jede Änderung steht im Anni-Verlauf und lässt sich rückgängig machen. '
     1047        echo '<p class="description">Firmen-Angaben für Suchmaschinen (Schema), Fragen-Schema in Beiträgen, Zugang für KI-Suchdienste in der robots.txt '
     1048            . 'und Korrekturen deiner Firmenangaben. Sichtbare Ergänzungen auf deinen Seiten (Kurzfassung, Fragen, Inhaltsverzeichnis) '
     1049            . 'nur nach deiner Freigabe in Anni. Jede Änderung steht im Anni-Verlauf und lässt sich rückgängig machen. '
    9871050            . 'Ausgeschaltet gibt das Plugin nichts davon aus und ändert nichts mehr.</p>';
    9881051        submit_button();
    … …  
    10161079        ),
    10171080        'verlauf' => anni_ki_verlauf(20),
     1081        // 1.7.0: welche Seiten Anni-Bausteine tragen (je Seite: Kurzfassung ja/nein, Anzahl Fragen, Inhaltsverzeichnis)
     1082        'seiten' => function_exists('anni_ki_seiten_uebersicht') ? anni_ki_seiten_uebersicht() : array(),
     1083        // 1.8.0 (S41): welche Seiten Hinweis-Links von Anni tragen (je Seite die Linkziele)
     1084        'links' => function_exists('anni_ki_links_uebersicht') ? anni_ki_links_uebersicht() : array(),
    10181085    ));
    10191086}
    … …  
    10501117            }
    10511118            if (array_key_exists('llms_beschreibung', $j)) { $neu['llms_beschreibung'] = mb_substr(sanitize_text_field((string) $j['llms_beschreibung']), 0, 300); }
     1119            if (array_key_exists('indexnow', $j)) {
     1120                if (!($j['indexnow'] === '' || anni_ki_indexnow_gueltig($j['indexnow']))) {
     1121                    return new WP_Error('anni_ki_abgelehnt', 'Abgelehnt: indexnow braucht 8 bis 128 Zeichen (Buchstaben, Ziffern, Bindestrich) oder "".', array('status' => 400));
     1122                }
     1123                $neu['indexnow'] = (string) $j['indexnow'];
     1124            }
    10521125            $auftrag = anni_rest_auftrag($req);
    10531126            $antwort = array('ok' => true, 'change_id' => 0, 'robots_datei' => null);
  • anni-marketing/tags/1.10.0/includes/ledger.php

    r3745272 r3780762  
    152152 */
    153153function anni_ledger_ohne_kern_sql() {
    154     return "AND aktion NOT IN ('seo','alt','status','titelbild','kern')";
     154    // 1.9.0: Block-Inhalte (includes/bloecke.php) haben eine eigene Konfliktprüfung über post_content.
     155    return "AND aktion NOT IN ('seo','alt','status','titelbild','kern') AND element_ids NOT IN ('bloecke','kopffuss')";
    155156}
    156157
    … …  
    259260    if ($nur_aktion !== '' && (string) $zeile['aktion'] !== (string) $nur_aktion) {
    260261        return new WP_Error('anni_change_falsche_art', 'Diese Änderung gehört nicht hierher. Ich nehme nichts zurück.', array('status' => 409, 'aktion' => (string) $zeile['aktion']));
     262    }
     263    // 1.9.0 (W1.36/W1.31): Gutenberg-/klassische Inhalte (includes/bloecke.php) — eigene Prüfsumme über post_content.
     264    if (function_exists('anni_bloecke_ist_eintrag') && anni_bloecke_ist_eintrag($zeile)) {
     265        return anni_bloecke_zuruecknehmen($zeile, $auftrag);
    261266    }
    262267    // Bild-Optimierung (includes/media.php): Rücknahme = Original des Bilds wiederherstellen.
  • anni-marketing/tags/1.10.0/includes/rest-patch.php

    r3745272 r3780762  
    4343    $out = array();
    4444    foreach ($zeilen as $z) {
     45        // 1.9.0 (W1.36/W1.31): Block-Inhalte mit eigener Rücknahme-Prüfung (includes/bloecke.php).
     46        if (function_exists('anni_bloecke_ausgabe') && ($eigen = anni_bloecke_ausgabe($z)) !== null) { $out[] = $eigen; continue; }
    4547        if (function_exists('anni_kern_ist_aktion') && anni_kern_ist_aktion($z['aktion'])) {
    4648            $out[] = anni_ledger_fuer_ausgabe($z, (int) $z['ruecknahme_von'] === 0 && anni_kern_ruecknahme_pruefen($z) === true);
    … …  
    9193            // «nur»: optional die Art der Änderung, die zurückgenommen werden darf (z. B. «kit»).
    9294            $nur = (string) $req->get_param('nur');
    93             if ($nur !== '' && !preg_match('/^[a-z_]{1,20}$/', $nur)) { $nur = '-'; }
     95            // 1.9.3 (A4): Ziffern erlaubt (Art «ga4_weitere»); ungueltig → «-» (nimmt nichts zurueck, nie still alles)
     96            if ($nur !== '' && !preg_match('/^[a-z0-9_]{1,20}$/', $nur)) { $nur = '-'; }
    9497            return anni_ledger_zuruecknehmen((string) $req['id'], anni_rest_auftrag($req), $bestaetigt, $nur);
    9598        },
  • anni-marketing/tags/1.10.0/includes/structure.php

    r3745272 r3780762  
    397397    }
    398398    if (!preg_match('/^[A-Za-z0-9]{1,20}$/', $eid)) {
    399         return new WP_Error('anni_struktur_eingabe', 'Bitte gib die Element-ID an.', array('status' => 400));
     399        // Eigener Code (Leitstelle 22.10.2026): die App übersetzt ihn in Kundensprache; der Text ist nur Rückfall.
     400        return new WP_Error('anni_element_id_fehlt', 'Diesen Abschnitt finde ich auf der Seite nicht mehr. Lade die Seite bitte neu.', array('status' => 400));
    400401    }
    401402    if ($op === 'hide_on') {
  • anni-marketing/tags/1.10.0/readme.txt

    r3757258 r3780762  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.4.3
     7Stable tag: 1.10.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    1515Anni is a marketing assistant for small businesses in Switzerland (anni-marketing.ch). This companion plugin lets Anni finish your Google setup automatically after you connect your WordPress site:
    1616
    17 * Outputs the **Google Analytics 4** tag (gtag.js) in your site's head — only if you (or Anni, with your permission) configure a measurement ID.
     17* Outputs the **Google Analytics 4** tag (gtag.js) in your site's head — only if you (or Anni, with your permission) configure a measurement ID. An analytics ID that was already configured stays active alongside Anni's (it is never replaced).
    1818* Outputs the **Google Search Console** site-verification meta tag — only if a verification token is configured.
     19* Counts visits of known AI crawlers per day (only the date, the crawler name and the count — no IP address, no visitor
     20  data). The numbers stay on your site until Anni reads them through the authenticated endpoint.
    1921* Provides a small authenticated REST endpoint (`anni/v1`) so the Anni app can read plugin status and set the two values above. All endpoints require the `manage_options` capability (administrators). Requests are authenticated with WordPress Application Passwords.
    2022
    … …  
    2830
    2931== Frequently Asked Questions ==
     32
     33= How is the plugin updated? =
     34Only through the normal WordPress update system, from WordPress.org. Anni can start an update that WordPress already
     35offers (like clicking "Update now"); your settings and the change history stay as they are. Automatic updates are only
     36enabled if you choose so, either on the Plugins screen or by agreeing in Anni.
    3037
    3138= Does this plugin track my visitors? =
    … …  
    6370* `POST /wp-json/anni/v1/ga4` — stores your Google Analytics 4 measurement ID (requires `manage_options`).
    6471* `POST /wp-json/anni/v1/gsc` — stores your Google Search Console verification token (requires `manage_options`).
     72* `GET /wp-json/anni/v1/ki-bots` — reads the daily counts of AI crawler visits (service name and number only;
     73  requires `manage_options`).
     74* `POST /wp-json/anni/v1/ki-technik/seiten` — stores a summary, questions and a table of contents for one page that
     75  you approved in Anni (requires `manage_options`).
    6576
    6677Data involved: your GA4 measurement ID and your Search Console verification token. No visitor data,
    … …  
    7687
    7788== Changelog ==
     89
     90= 1.10.0 =
     91* Updates without data loss: new authenticated endpoint `anni/v1/aktualisieren` lets Anni start the update that
     92  WordPress itself offers for this plugin (same as "Update now" on the Plugins screen). The plugin stays active; its
     93  settings and the change history (`anni_changes`) are kept. The new version always comes from WordPress.org through
     94  the regular WordPress update system; the plugin contains no update server of its own.
     95* Automatic updates for this plugin are only switched on when you ask for it (same setting as "Enable auto-updates" on
     96  the Plugins screen). If your site has disabled automatic plugin updates, the plugin leaves that setting untouched.
     97* Site owners can turn the endpoint off with `add_filter( 'anni_marketing_selbst_aktualisieren', '__return_false' );`.
     98
     99= 1.9.3 =
     100* Consent: with Anni's notice switched off and your own Google tag running without a cookie tool, the plugin no longer
     101  prints a site-wide consent default either (Anni does not measure without its notice).
     102* Undo: the type filter of `anni/v1/rollback` (`nur`) now accepts digits (e.g. `ga4_weitere`); an invalid value still
     103  undoes nothing.
     104
     105= 1.9.2 =
     106* Google Analytics: if your site already loads a measurement ID itself (theme or another plugin), Anni no longer loads
     107  it a second time as an additional ID, so visits are not counted twice. Anni can remove such an additional ID again
     108  (`anni/v1/ga4` with `entfernen`).
     109* Consent: if your site runs its own Google tag without a cookie tool, Anni's notice no longer sets a site-wide consent
     110  default or update (`nur_eigen`); it only applies to Anni's own measurement, which still loads after consent.
     111
     112= 1.9.1 =
     113* All in One SEO: on an existing entry Anni now changes only the title and description columns directly; all other
     114  columns (e.g. schema) stay byte-identical, also after undo. A new entry is still removed completely on undo. After
     115  each change the post cache and the All in One SEO caches are cleared, so the page title shows the new text at once.
     116
     117= 1.9.0 =
     118* SEO title and description for All in One SEO, SEOPress, The SEO Framework and Slim SEO: Anni writes them only into
     119  the SEO plugin that outputs your page title (never into two plugins), with the storage each plugin uses itself
     120  (AIOSEO table via its own model, post meta for the others). Every change is in the Anni history and can be undone
     121  (`anni/v1/rollback`).
     122* Block and classic pages (not Elementor): replace an image (image, cover, media & text blocks and images in classic
     123  content) with a new image and alt text, when you confirm it in Anni. Only that spot changes; everything else in the
     124  page content stays byte for byte the same (checked after saving, otherwise the old content is restored). The plugin
     125  only writes when the page is still exactly as Anni read it. Every change is in the Anni history and can be undone
     126  (`anni/v1/bloecke/{id}`, `anni/v1/rollback`).
     127* Block and classic pages (not Elementor): duplicate, move and delete sections (top-level blocks; on classic pages the
     128  parts between blank lines, only where the structure is unambiguous) and hide blocks per device using WordPress' own
     129  block visibility (WordPress 7.0 or newer). Deleting needs a confirmation in Anni. Same checks, history and undo as above.
     130* Header and footer: change a text there (e.g. address, opening hours, phone) after you confirm it in Anni. Block themes:
     131  the header/footer template parts (saved like the Site Editor does; a theme part becomes a customized copy, undo removes
     132  that copy again). Classic themes: widgets in the sidebars and short contact/footer texts in the Customizer. Texts
     133  written into the theme's code cannot be changed here. History and undo as above (`anni/v1/kopffuss`).
     134* Customizer text settings never receive HTML: writing text containing "<" into a theme setting is refused.
     135* 301 redirects without an extra plugin: after you approve it in your Anni account, an old address of your site (e.g.
     136  a renamed or deleted page) leads permanently to a published page of the same site. Only used when WordPress itself
     137  finds nothing at the old address (404); never the home page, never system paths, never another domain, no loops or
     138  chains. Stored in the option `anni_weiterleitungen` (at most 200 rules); every rule is in the Anni history and can be
     139  undone (`anni/v1/rollback`). If a redirect plugin is active (Redirection, Rank Math redirections, Yoast Premium,
     140  SEOPress PRO, AIOSEO Redirects, Slim SEO, Safe Redirect Manager, Simple 301 Redirects), Anni adds no rule of its own. Redirects keep working when "Anni may adjust site technology" is off.
     141
     142= 1.8.0 =
     143* Links between your own pages: after you approve a suggestion in your Anni account, the plugin shows one short
     144  paragraph with a link to a matching page of your site ("Passend dazu: ...") below the content of an existing page,
     145  e.g. for a page no other page links to. Stored per page in the option `anni_ki_links`, never written into your page
     146  content; link targets must be published pages of the same site. No extra block on posts when YARPP, Jetpack Related
     147  Posts or Contextual Related Posts already show related posts. Every change is in the Anni history and can be undone
     148  (`anni/v1/rollback`). Nothing is shown without your approval.
     149
     150= 1.7.0 =
     151* Short summary, questions and table of contents on existing pages: after you approve a draft in your Anni account,
     152  the plugin shows 2-3 sentences "Kurz gesagt" at the top of that page, a table of contents from its H2/H3 headings
     153  (long pages only) and visible answers to common questions with FAQPage schema. Stored per page in the option
     154  `anni_ki_seiten`, never written into your page content; works with Elementor pages. Every change is in the Anni
     155  history and can be undone (`anni/v1/rollback`). Nothing is shown without your approval.
     156* Counts how often AI services read your site (GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-SearchBot,
     157  PerplexityBot, CCBot and others): only the day, the service name and the count are stored (option `anni_ki_bots`,
     158  at most 90 days) — no IP address, no page, no visitor data. Anni reads the numbers via the authenticated
     159  `anni/v1/ki-bots` endpoint. Requests answered by a page cache or CDN are not counted.
     160* Existing cookie tool: if your Anni account is linked to the cookie tool already running on your site, the plugin
     161  shows no Anni cookie notice of its own. Google Analytics then loads only after visitors agree to statistics in your
     162  cookie tool (no consent, unreadable or declined: nothing is loaded). Without such a link nothing changes.
     163* Cookie notice: decisions made by automated browsers (navigator.webdriver, headless browsers) and known bots or
     164  crawlers are no longer counted in the anonymous consent statistics.
     165* /llms.txt is off by default and no longer switched on by Anni (it has not become an established standard; robots.txt
     166  stays the standard). Sites that already use it keep it unchanged.
     167* IndexNow key file: when Anni stores an IndexNow key through `anni/v1/ki-technik` (field `indexnow`, 8 to 128 letters,
     168  digits or hyphens; an empty value turns it off), the site answers `/<key>.txt` with that key as plain text. Search
     169  engines that take part in IndexNow (for example Bing) use this file to confirm that a reported page belongs to the
     170  site. Nothing is added to robots.txt or the sitemap, and the plugin sends nothing anywhere itself.
     171
     172= 1.6.0 =
     173* Read-only security check for administrators (`anni/v1/sicherheit`): compares WordPress core files with the official
     174  checksums, lists PHP files inside the uploads folder and lists must-use plugins and drop-ins (flagging typical
     175  malicious patterns). Only file names and counts are reported, never file contents. Nothing on the site is changed.
     176  Anni uses this when you connect your site and pauses all changes if the site shows signs of a compromise.
     177* An analytics measurement ID that was already configured is kept active next to Anni's own (option
     178  `anni_ga4_weitere`, at most three IDs) instead of being replaced. Both respect the consent banner.
     179* The WordPress "Authorize Application" screen for the Anni connection is easier to follow: only when the
     180  request returns to Anni's own connection callback (app.anni-marketing.ch), other admin notices are hidden on that one screen, a one-line instruction
     181  points to the approve button, and the application is shown as "Anni" (the stored password name is unchanged).
     182* Security: the plugin only overrides a security plugin's application-password block for requests that return to
     183  Anni's own connection callback; a request that merely uses the name "Anni" is treated like any other application.
     184* The consent banner can show the site's own logo: an https address or a small embedded PNG/WebP image (data URI),
     185  nothing else. Light logos get a dark background so they stay visible. No image is loaded from Anni's server.
     186
     187= 1.4.4 =
     188* The authenticated status endpoint (`anni/v1/status`, administrators only) also reports the PHP and WordPress
     189  version, so Anni can tell you when your site runs outdated or insecure software.
    78190
    79191= 1.4.3 =
  • anni-marketing/tags/1.10.0/uninstall.php

    r3757258 r3780762  
    1616}
    1717
    18 $anni_optionen = array('anni_ledger_schema', 'anni_struktur_nonces', 'anni_marketing_hinweis', 'anni_ga4_id', 'anni_gsc_verification', 'anni_consent', 'anni_ladezeit', 'anni_ki_technik', 'anni_ki_erlaubt', 'anni_ki_beobachtet');
     18$anni_optionen = array('anni_ledger_schema', 'anni_struktur_nonces', 'anni_marketing_hinweis', 'anni_ga4_id', 'anni_ga4_weitere', 'anni_gsc_verification', 'anni_consent', 'anni_ladezeit', 'anni_ki_technik', 'anni_ki_erlaubt', 'anni_ki_beobachtet', 'anni_ki_seiten', 'anni_ki_bots', 'anni_ki_links', 'anni_weiterleitungen', 'anni_aktualisierung', 'anni_auto_update_auftrag');
    1919delete_transient('anni_llms_cache');
    2020delete_transient('anni_llms_cache_v2');
     21delete_transient('anni_akt_frisch');
     22delete_transient('anni_akt_laeuft');
    2123foreach ($anni_optionen as $anni_o) {
    2224    delete_option($anni_o);
  • anni-marketing/trunk/CREDITS.md

    r3745272 r3780762  
    2020| `includes/rest-media.php` | – | REST-Routen `anni/v1/media/*` (eigener Code) | – |
    2121| `includes/seo.php` | WPPilot 1.17.2 (Commit `7285270`), `includes/kits/seo-basics/src/rank-math.php`, `yoast.php`, `common.php`; `includes/kits/a11y-audit/src/media.php` | Meta-Schlüssel von Rank Math und Yoast, Erkennung der SEO-Plugins über ihre Konstanten, Säubern mit `wp_kses` ohne Tags (Rank-Math-Variablen wie `%sep%` bleiben erhalten), Alt-Text am Medium (`_wp_attachment_image_alt`) mit Vorher-Bild und Prüfung nach dem Schreiben. Freigabe der Felder für die Kern-REST (`register_post_meta`), die Verlauf-Brücke für Kern-REST-Aufrufe und die Rücknahme sind eigener Code. | GPL-2.0-or-later, © 2026 WPPilot |
     22| `includes/seo-felder.php` | eigener Code; Speicherorte aus dem Quelltext von All in One SEO 5.0.4, SEOPress 10.3, The SEO Framework 5.2.0, Slim SEO 4.11.4 (wordpress.org); Verlauf und Rücknahme über `includes/ki-technik.php` (Art `seo_felder`) | SEO-Titel/-Beschreibung je Plugin lesen und schreiben (AIOSEO über sein Modell `Models\Post::savePost`, TSF über `update_single_meta_item`, sonst Post-Meta), nur ins aktive führende Plugin | – (eigener Code) |
    2223| `includes/ladezeit.php` | eigener Code; Verlauf nach dem Muster von `includes/ledger.php` (siehe oben: WPPilot, EMCP) | Sichere Bild-Hebel (fetchpriority/kein lazy fürs LCP-Bild, loading=lazy darunter, decoding=async) über `WP_HTML_Tag_Processor` (WordPress-Kern), Rücknahme über den bestehenden Verlauf | – (eigener Code) |
    23 | `includes/ki-technik.php` | eigener Code; Verlauf nach dem Muster von `includes/ledger.php` und `includes/ladezeit.php` | robots.txt für KI-Suchdienste (Filter `robots_txt`, echte Datei über WP_Filesystem), Firmen-Schema (ergänzt Rank Math/Yoast über deren Schema-Filter), FAQPage, llms.txt, Korrektur der Firmenangaben (Elementor über die Patch-Engine, Block-/Classic-Inhalte, Widgets, Customizer, Ausgabe-Regel), Freischaltung für Suchmaschinen; Rücknahme über den bestehenden Verlauf | – (eigener Code) |
     24| `includes/ki-technik.php` | eigener Code; Verlauf nach dem Muster von `includes/ledger.php` und `includes/ladezeit.php` | robots.txt für KI-Suchdienste (Filter `robots_txt`, echte Datei über WP_Filesystem), Firmen-Schema (ergänzt Rank Math/Yoast über deren Schema-Filter), FAQPage, llms.txt (seit 1.7.0 standardmässig aus), Korrektur der Firmenangaben (Elementor über die Patch-Engine, Block-/Classic-Inhalte, Widgets, Customizer, Ausgabe-Regel), Freischaltung für Suchmaschinen; Rücknahme über den bestehenden Verlauf | – (eigener Code) |
     25| `includes/ki-seiten.php` | eigener Code; Verlauf und Rücknahme über `includes/ki-technik.php` (Art `ki_seiten`) | Kurzfassung, Fragen (FAQPage) und Inhaltsverzeichnis aus H2/H3 auf einer Seite, nur mit Freigabe-ID; Ausgabe über `the_content`, gespeichert je Seite in einer Option | – (eigener Code) |
     26| `includes/ki-links.php` | eigener Code; Verlauf und Rücknahme über `includes/ki-technik.php` (Art `ki_links`) | Hinweis-Link-Absatz («Passend dazu: …») auf einer bestehenden Seite, nur mit Freigabe-ID, Ziele nur veröffentlichte Seiten derselben Website; Ausgabe über `the_content`, gespeichert je Seite in einer Option | – (eigener Code) |
     27| `includes/weiterleitungen.php` | eigener Code; Verlauf und Rücknahme über `includes/ki-technik.php` (Art `weiterleitung`) | 301-Weiterleitung einer alten Adresse auf eine veröffentlichte Seite derselben Website, nur mit Freigabe-ID, nur bei 404, ohne Kreise/Ketten, keine Regel neben einer Weiterleitungs-Erweiterung; gespeichert in einer Option | – (eigener Code) |
     28| `includes/ki-bots.php` | eigener Code | Abrufe durch KI-Dienste je Tag zählen (nur Tag, Name, Anzahl; keine IP, keine Seite), Lesen über `anni/v1/ki-bots` | – (eigener Code) |
     29| `includes/consent-cmp.php` | eigener Code; gleiches Verhalten wie `core/consent.py` (build_cmp_gated_tag) im Anni-Server | Messung hinter dem vorhandenen Cookie-Tool der Website: kein Anni-Hinweis, Google erst nach dessen Statistik-Zustimmung | – (eigener Code) |
     30| `includes/aktualisieren.php` | eigener Code; Ablauf wie `wp_ajax_update_plugin()` und `wp_ajax_toggle_auto_updates()` im WordPress-Kern (GPLv2+) | Aktualisieren ohne Löschen über die WordPress-Aktualisierung (Quelle wordpress.org, kein eigener Update-Server), automatische Aktualisierung nur auf Auftrag | – (eigener Code) |
     31| `includes/consent-grund.php` | eigener Code; gleiche Regel wie `nur_eigen` in `core/consent_push.py` (Messkonzept L7b) | Bei abgeschaltetem Anni-Hinweis und eigenem Google-Code der Website ohne Cookie-Tool keine seitenweite Consent-Grundeinstellung | – (eigener Code) |
     32| `includes/ga4-weitere.php` | eigener Code; Verlauf und Rücknahme über `includes/ki-technik.php` (Art `ga4_weitere`) | Weitere Mess-IDs (Option `anni_ga4_weitere`) entfernen, wenn die Website eine ID selbst lädt (keine Doppelzählung, Messkonzept L7a), Rücknahme über `anni/v1/rollback` | – (eigener Code) |
    2433| `core/wp_ladezeit.py` (Anni-Server, nicht Teil der Erweiterung) | EMCP Tools 3.18.x von Mian Shahzad Raza (msrbuilds), https://github.com/msrbuilds/elementor-mcp (Commit `ebf7a71`), `includes/performance/class-performance-page-audit.php` (`analyze()`, `asset_findings()`) | Server- und HTML-Prüfungen nach Python portiert: Antwortzeit über 800 ms, Komprimierung, Cache-Header, mehr als 5 blockierende Dateien im `<head>`, HTML über 500 KB, fremde Domains. Gemessen wird mit Lighthouse (Apache 2.0, nur wenn auf dem Server installiert) bzw. headless Chromium | GPL-2.0-or-later, © Mian Shahzad Raza |
     34| `includes/bloecke.php` | eigener Code; Verlauf und Konfliktprüfung nach `includes/ledger.php` und `includes/ki-technik.php` | Inhalt von Gutenberg-/klassischen Seiten mit Prüfsumme schreiben (Stand beim Lesen = Stand beim Schreiben), Zurücklesen auf Byte-Gleichheit, Verlauf-Art `bloecke` mit Rücknahme; die Block-Zerlegung (Semantik von `WP_Block_Parser`) läuft im Anni-Server (`core/wp_bloecke.py`) | – (eigener Code) |
     35| `includes/kopffuss.php` | eigener Code; Verlauf und Rücknahme über `includes/bloecke.php` (Art `kopffuss`) | Kopf-/Fussbereich: Vorlagenteile über die WordPress-eigene REST-Route `/wp/v2/template-parts` (wie der Website-Editor; Rücknahme löscht eine neu angelegte Anpassung), Widgets und Customizer-Texte, Prüfsumme und Zurücklesen | – (eigener Code) |
    2536| `includes/haertung.php` | – | Rate-Grenze (429) und Grössengrenze (413) der Schreib-Routen, Fähigkeiten in der Status-Route (eigener Code, Bauplan-Schritt 9) | – |
    2637| `includes/abilities.php` | WordPress Abilities API (Kern, ab 6.9), Muster aus dem Praxistest wp-k3 | Registrierung als Abilities nur wenn vorhanden | – (eigener Code) |
     38| `includes/autorisieren.php` | – | Seite «Anwendung autorisieren» für die Anni-Freigabe: fremde Hinweise nur dort ausblenden, Anleitung über dem Knopf, Anzeige «Anni» (eigener Code, K38) | – |
     39| `includes/sicherheit.php` | – | Sicherheitsprüfung von innen, nur lesend: Kern-Prüfsummen (`get_core_checksums`), PHP-Dateien im Upload-Ordner, Pflicht-Erweiterungen und Drop-ins mit Musterprüfung (eigener Code, K44) | – |
    2740
    2841## Bewusste Abweichungen von den Vorlagen
  • anni-marketing/trunk/anni-consent.js

    r3756924 r3780762  
    5757    if (mode === 'basic') {
    5858      gt()('js', new Date()); gt()('config', C.m);
     59      (C.w || []).forEach(function (w) { if (/^G-[A-Z0-9]{4,20}$/.test(w)) gt()('config', w); });   // 1.5.0: weitere Mess-IDs zählen mit
    5960      var s = D.createElement('script'); s.async = true; s.src = 'https://www.googletagmanager.com/gtag/js?id=' + encodeURIComponent(C.m);
    6061      (D.head || D.documentElement).appendChild(s);
    … …  
    6364  function apply(st) {
    6465    var g = gt();
     66    // Messkonzept L7b (30.10.2026): C.nur_eigen = die Website hat eigenen Google-Code ohne Cookie-Tool. Kein seitenweites
     67    // consent update (es veränderte deren Zahlen); Annis Messung lädt ohnehin erst nach Zustimmung (basic).
     68    if (C.nur_eigen) {
     69      try { W.dataLayer.push({ event: 'anni_consent_update', consent_statistik: !!st.s, consent_marketing: !!st.m }); } catch (e) {}
     70      if (st.s) loadGoogle();
     71      return;
     72    }
    6573    g('consent', 'update', {
    6674      analytics_storage: st.s ? 'granted' : 'denied',
    … …  
    7280    if (st.s) loadGoogle();
    7381  }
     82  // 28.10.2026 (Befund: ein Headless-Screenshot klickte «Alle akzeptieren» und wurde dem Kunden gezählt):
     83  // automatisierte Browser und bekannte Bots zählen nie mit. Der Server filtert den User-Agent zusätzlich.
     84  var BOT_UA = /Headless|PhantomJS|Puppeteer|Playwright|Selenium|webdriver|Lighthouse|PageSpeed|GTmetrix|Pingdom|UptimeRobot|bot[\/_\-]|\bbot\b|crawler|spider|slurp|facebookexternalhit|curl\/|wget\/|python-requests|HttpClient/i;
     85  function automat() {
     86    try {
     87      var n = W.navigator || {};
     88      if (n.webdriver === true) return true;
     89      return BOT_UA.test(String(n.userAgent || ''));
     90    } catch (e) { return false; }
     91  }
    7492  function zaehlen(st) {
    7593    if (!C.z) return;
     94    if (automat()) return;
    7695    var e = (st.s && (!kat.marketing || st.m)) ? 'granted' : (!st.s && !st.m ? 'denied' : 'partial');
    7796    try {
    … …  
    100119    '.w{position:fixed;left:0;right:0;bottom:0;z-index:2147483646;margin:0 auto;max-width:64rem;background:' + c.bg + ';color:' + c.fg + ';border:1px solid ' + c.border + ';border-bottom:none;border-radius:12px 12px 0 0;box-shadow:0 -2px 24px rgba(0,0,0,.13);padding:16px 20px 14px;display:flex;flex-wrap:wrap;gap:14px;align-items:flex-start;justify-content:space-between}' +
    101120    '.f{margin:0 0 4px;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:' + c.muted + '}' +
     121    '.lg{display:inline-flex;align-items:center;vertical-align:middle;margin:0 8px 2px 0}.lg img{display:block;max-height:26px;max-width:120px}' +
     122    '.lg.dk{background:#2a211c;padding:4px 8px;border-radius:6px}' +
    102123    '.h{margin:0 0 6px;font-size:15px;font-weight:650;line-height:1.35}' +
    103124    '.p{margin:0;font-size:13.5px;line-height:1.55;color:' + c.muted + ';max-width:62ch}' +
    … …  
    147168    var sh = root()._sh;
    148169    if (sh.querySelector('.w')) return;
    149     var firm = C.f ? '<p class="f">' + esc(C.f) + '</p>' : '';
     170    // Punkt 17: Logo der Website vor dem Namen; helles Logo (lgd) auf dunklem Grund. Nur https der Kundenseite oder ein
     171    // eingebettetes Bild (data-URI) — nie eine Adresse von Anni vor der Einwilligung (Plugin prüft es auch).
     172    var lg = (C.lg && /^(https:\/\/|data:image\/(png|webp);base64,)/.test(C.lg)) ? '<span class="lg' + (C.lgd ? ' dk' : '') + '"><img src="' + esc(C.lg) + '" alt=""></span>' : '';
     173    var firm = (C.f || lg) ? '<p class="f">' + lg + esc(C.f || '') + '</p>' : '';
    150174    var link = C.u ? '<a href="' + esc(C.u) + '">' + esc(t.link) + '</a>' : '';
    151175    var w = el('<div class="w" role="dialog" aria-modal="false" aria-label="' + esc(t.aria) + '"><div class="x">' + firm +
    … …  
    154178      '<div class="b"><button type="button" data-a="no">' + esc(t.reject) + '</button><button type="button" class="ok" data-a="yes">' + esc(t.accept) + '</button></div></div>');
    155179    sh.appendChild(w);
     180    var li = w.querySelector('.lg img'); if (li) li.addEventListener('error', function () { li.parentNode.parentNode.removeChild(li.parentNode); });
    156181    w.querySelector('[data-a="yes"]').addEventListener('click', function () { decide({ s: true, m: !!kat.marketing }, 'alle'); });
    157182    w.querySelector('[data-a="no"]').addEventListener('click', function () { decide({ s: false, m: false }, 'notwendig'); });
  • anni-marketing/trunk/anni-marketing.php

    r3757258 r3780762  
    44 * Plugin URI: https://anni-marketing.ch
    55 * Description: Verbindet deine Website mit Anni (anni-marketing.ch): Statistik-Messcode mit Einwilligungs-Hinweis und Status-Kanal. Keine Daten an Dritte, kein Tracking durch das Plugin selbst.
    6  * Version: 1.4.3
     6 * Version: 1.10.0
    77 * Author: Anni Marketing
    88 * Author URI: https://digitalerakete.ch
    … …  
    1616if (!defined('ABSPATH')) { exit; }
    1717
    18 define('ANNI_MARKETING_VERSION', '1.4.3');
     18define('ANNI_MARKETING_VERSION', '1.10.0');
    1919
    2020// Seitenbearbeitung (Bauplan 07.10.2026, Schritte 0-2): Schema, Änderungsmotor, Verlauf, REST, Abilities.
    … …  
    3232require_once __DIR__ . '/includes/rest-media.php';
    3333require_once __DIR__ . '/includes/seo.php';
     34// 1.9.0 (W1.39): SEO-Titel/-Beschreibung auch für All in One SEO, SEOPress, The SEO Framework und Slim SEO (rücknehmbar).
     35require_once __DIR__ . '/includes/seo-felder.php';
    3436// Ladezeit (Bauplan 07.10.2026): sichere Bild-Hebel, einzeln schaltbar, Messung auf dem Anni-Server.
    3537require_once __DIR__ . '/includes/ladezeit.php';
    3638require_once __DIR__ . '/includes/ki-technik.php';
     39// 1.7.0 (G13/G20 KI-Suche): Kurzfassung/Fragen/Inhaltsverzeichnis auf Seiten (nur mit Freigabe), Abrufe durch KI-Dienste zählen.
     40require_once __DIR__ . '/includes/ki-seiten.php';
     41require_once __DIR__ . '/includes/ki-bots.php';
     42// 1.8.0 (S41 Interne Verlinkung): Hinweis-Links auf bestehenden Seiten (nur mit Freigabe, rücknehmbar).
     43require_once __DIR__ . '/includes/ki-links.php';
     44// 1.9.0 (W1.36/W1.31 Gutenberg/klassisch): Bild tauschen und Abschnitte umstellen, mit Prüfsumme, Verlauf und Rücknahme.
     45require_once __DIR__ . '/includes/bloecke.php';
     46// 1.9.0 (W1.73): Kopf-/Fussbereich (Vorlagenteile, Widgets, Customizer-Texte) mit Bestätigung, Verlauf und Rücknahme.
     47require_once __DIR__ . '/includes/kopffuss.php';
     48// 1.9.0 (W1.72): 301-Weiterleitungen ohne Fremd-Plugin (nur mit Freigabe, rücknehmbar, keine zweite Regel neben einer Weiterleitungs-Erweiterung).
     49require_once __DIR__ . '/includes/weiterleitungen.php';
     50// 1.7.0 (28.10.2026): Messung hinter dem vorhandenen Cookie-Tool der Website, dann kein Anni-Hinweis.
     51require_once __DIR__ . '/includes/consent-cmp.php';
     52// 1.9.2 (Messkonzept L7a): weitere Mess-IDs entfernen mit Verlauf und Ruecknahme
     53require_once __DIR__ . '/includes/ga4-weitere.php';
     54// 1.9.3 (B7): Consent-Grundeinstellung auch bei abgeschaltetem Hinweis nur fuer Annis Messung
     55require_once __DIR__ . '/includes/consent-grund.php';
     56// 1.10.0 (Befund Kalex 02.11.2026): aktualisieren wie «Jetzt aktualisieren» in wp-admin — ohne Löschen, Verlauf und
     57// Einstellungen bleiben. Quelle bleibt die WordPress-Aktualisierung (wordpress.org), kein eigener Update-Server.
     58require_once __DIR__ . '/includes/aktualisieren.php';
    3759// Härtung (Bauplan-Schritt 9): Rate- und Grössengrenze der Schreib-Routen, Fähigkeiten im Status.
    3860require_once __DIR__ . '/includes/haertung.php';
     61// K38: Seite «Anwendung autorisieren» bei der Anni-Verbindung aufgeräumt (nur dort, nur für Anni-Rückkehr-Adressen).
     62require_once __DIR__ . '/includes/autorisieren.php';
     63// K44 (1.6.0): Sicherheitsprüfung von innen, nur lesend (Kerndateien, Upload-Ordner, versteckte Erweiterungen).
     64require_once __DIR__ . '/includes/sicherheit.php';
    3965
    4066/** WPP/GWE (01.09.2026, Echtkundin-Befund): Security-Plugins schalten WordPress-
    … …  
    5177        $name = sanitize_text_field(wp_unslash((string) $_REQUEST['app_name']));
    5278        if (strpos($name, 'Anni') !== 0) { return $verfuegbar; }
     79        // K38 (Leitstelle 23.10.2026): der App-Name allein ist frei wählbar — nur Annis eigener Rückweg setzt die
     80        // Entscheidung des Security-Plugins ausser Kraft, sonst bekäme ein fremder Link «Anni …» das Kennwort.
     81        $ziel = isset($_REQUEST['success_url']) ? wp_unslash((string) $_REQUEST['success_url']) : '';
     82        if (!anni_marketing_ist_anni_ziel($ziel)) { return $verfuegbar; }
    5383    }
    5484    return true;
    … …  
    6393    $name = isset($request['app_name']) ? (string) $request['app_name'] : '';
    6494    if (strpos($name, 'Anni') !== 0) { return; }
     95    if (!anni_marketing_ist_anni_ziel(isset($request['success_url']) ? (string) $request['success_url'] : '')) { return; }   // K38
    6596    foreach ($error->get_error_codes() as $code) { $error->remove($code); }
    6697}, PHP_INT_MAX, 3);
    … …  
    107138 * Modus basic (Standard): Google-Skript wird erst nach Zustimmung vom Hinweis nachgeladen.
    108139 * Modus advanced: Google-Skript laedt sofort mit "denied" (cookielose Pings, Google modelliert). */
     140/** 1.5.0 (23.10.2026): weitere Mess-IDs neben Annis eigener (z. B. der bisherige Code der Kundin oder einer Agentur).
     141 * Anni ersetzt nie einen fremden Code: er bleibt aktiv, Annis ID zaehlt zusaetzlich. Hoechstens 3, nur G-IDs. */
     142function anni_marketing_weitere($ohne = '') {
     143    $raw = get_option('anni_ga4_weitere', '');
     144    $liste = $raw ? json_decode($raw, true) : array();
     145    if (!is_array($liste)) { $liste = array(); }
     146    $out = array();
     147    foreach ($liste as $id) {
     148        $id = (string) $id;
     149        if ($id !== $ohne && preg_match('/^G-[A-Z0-9]{4,20}$/', $id) && !in_array($id, $out, true)) { $out[] = $id; }
     150    }
     151    return array_slice($out, 0, 3);
     152}
     153
    109154function anni_marketing_consent_config() {
    110155    $mid = get_option('anni_ga4_id', '');
    … …  
    113158    $cfg = $raw ? json_decode($raw, true) : null;
    114159    if (!is_array($cfg)) { $cfg = array(); }
    115     if (isset($cfg['enabled']) && $cfg['enabled'] === false) { return array('enabled' => false, 'm' => $mid); }
     160    // 1.9.3 (B7): auch bei abgeschaltetem Hinweis die Angabe «nur_eigen» behalten (includes/consent-grund.php)
     161    if (isset($cfg['enabled']) && $cfg['enabled'] === false) { return anni_consent_aus($cfg, $mid); }
     162    // 1.7.0: gekoppelt mit dem Cookie-Tool der Website -> kein Anni-Hinweis, Google nur nach dessen Zustimmung.
     163    $cmp = isset($cfg['cmp']) ? anni_marketing_cmp_pruefen($cfg['cmp']) : null;
     164    if ($cmp) { return array('enabled' => true, 'cmp' => $cmp, 'm' => $mid, 'w' => anni_marketing_weitere($mid)); }
    116165    $priv = '';
    117166    if (function_exists('get_privacy_policy_url')) { $priv = (string) get_privacy_policy_url(); }
    … …  
    137186    if (empty($out['f'])) { $out['f'] = (string) get_bloginfo('name'); }
    138187    $out['m'] = $mid;
     188    $out['w'] = anni_marketing_weitere($mid);
    139189    $out['enabled'] = true;
    140190    unset($out['_inline']);
    … …  
    153203        . '"ad_user_data":"denied","ad_personalization":"denied","wait_for_update":500});';
    154204    if (empty($cfg['enabled'])) {
     205        // 1.9.3 (B7): eigener Google-Code der Website ohne Cookie-Tool → keine seitenweite «denied»-Zeile; Anni misst
     206        // ohne Hinweis ohnehin nicht (kein Weg zur Zustimmung), die Zeile wuerde nur die Zahlen der Kundin senken.
     207        if (!anni_consent_grundzeile_noetig($cfg)) { return; }
    155208        wp_register_script('anni-marketing-consent-default', false, array(), ANNI_MARKETING_VERSION, false);
    156209        wp_enqueue_script('anni-marketing-consent-default');
    … …  
    158211        return;
    159212    }
     213    if (!empty($cfg['cmp'])) {
     214        // Kein zweiter Hinweis: das Cookie-Tool der Website fragt, Google laedt erst nach dessen Zustimmung.
     215        wp_register_script('anni-marketing-cmp', false, array(), ANNI_MARKETING_VERSION, false);
     216        wp_enqueue_script('anni-marketing-cmp');
     217        wp_add_inline_script('anni-marketing-cmp', anni_marketing_cmp_skript($cfg['cmp'], $mid, $cfg['w']));
     218        return;
     219    }
    160220    if ($cfg['mode'] === 'advanced') {
    161221        // Google sofort laden (mit "denied"), der Hinweis schaltet spaeter per consent update frei.
    162222        wp_enqueue_script('anni-marketing-gtag', 'https://www.googletagmanager.com/gtag/js?id=' . rawurlencode($mid), array(), ANNI_MARKETING_VERSION, false);
    163223        wp_script_add_data('anni-marketing-gtag', 'strategy', 'async');
    164         wp_add_inline_script('anni-marketing-gtag', $default . 'gtag("config","' . esc_js($mid) . '");', 'before');
     224        $configs = 'gtag("config","' . esc_js($mid) . '");';
     225        foreach ((array) $cfg['w'] as $wid) { $configs .= 'gtag("config","' . esc_js($wid) . '");'; }
     226        wp_add_inline_script('anni-marketing-gtag', $default . $configs, 'before');
    165227        $dep = array('anni-marketing-gtag');
     228    } elseif (!empty($cfg['nur_eigen'])) {
     229        // L7b: kein seitenweiter Consent-Default — die Google-Tags der Website laufen wie ohne Anni
     230        $dep = array();
    166231    } else {
    167232        wp_register_script('anni-marketing-consent-default', false, array(), ANNI_MARKETING_VERSION, false);
    … …  
    190255                'ok'      => true,
    191256                'version' => ANNI_MARKETING_VERSION,
     257                // 1.4.4: PHP- und WordPress-Version, damit Anni veraltete bzw. verwundbare Versionen erkennt
     258                // (nur fuer Administratoren, wie der ganze Endpunkt).
     259                'php'     => PHP_VERSION,
     260                'wp'      => (string) get_bloginfo('version'),
    192261                'ga4_id'  => (string) get_option('anni_ga4_id', ''),
    193262                'consent' => (bool) get_option('anni_consent', ''),
    194                 'faehigkeiten' => array('patch', 'verlauf'),
     263                'ga4_weitere' => anni_marketing_weitere((string) get_option('anni_ga4_id', '')),
     264                'faehigkeiten' => array('patch', 'verlauf', 'ga4_weitere', 'cmp', 'ga4_weitere_entfernen', 'nur_eigen', 'ga4_weitere_rueckweg', 'aktualisieren'),
    195265            );
    196266        },
    … …  
    218288            $out = array();
    219289            $out['enabled'] = !(isset($cfg['enabled']) && $cfg['enabled'] === false);
     290            // 1.7.0: Kopplung an das Cookie-Tool der Website (fehlt/ungueltig -> Anni-Hinweis wie bisher)
     291            $cmp = isset($cfg['cmp']) ? anni_marketing_cmp_pruefen($cfg['cmp']) : null;
     292            if ($cmp) { $out['cmp'] = $cmp; }
    220293            $out['mode'] = (isset($cfg['mode']) && $cfg['mode'] === 'advanced') ? 'advanced' : 'basic';
     294            // Messkonzept L7b (30.10.2026): laeuft eigener GA-Code der Website ohne Cookie-Tool, gilt Annis Grundeinstellung
     295            // nur fuer Annis Messung: keine seitenweite «denied»-Zeile, kein consent update, Google erst nach Zustimmung.
     296            $out['nur_eigen'] = !empty($cfg['nur_eigen']);
     297            if ($out['nur_eigen']) { $out['mode'] = 'basic'; }
    221298            $out['gleichfarbig'] = !empty($cfg['gleichfarbig']);
    222299            $out['kat'] = array('statistik' => true, 'marketing' => !empty($cfg['kat']['marketing']));
    223300            $out['f'] = isset($cfg['f']) ? sanitize_text_field(mb_substr((string) $cfg['f'], 0, 80)) : '';
     301            $lg = isset($cfg['lg']) ? trim((string) $cfg['lg']) : '';   // Logo im Hinweis (nur https, keine Anführungszeichen)
     302            $out['lg'] = ($lg !== '' && (preg_match('#^https://[\w\-.]+(?::\d+)?/[\w\-./%~+]{1,280}$#', $lg)
     303                || preg_match('#^data:image/(?:png|webp);base64,[A-Za-z0-9+/]{1,41000}={0,2}$#', $lg))) ? $lg : '';
     304            $out['lgd'] = !empty($cfg['lgd']);
    224305            $u = isset($cfg['u']) ? trim((string) $cfg['u']) : '';
    225306            $out['u'] = ($u !== '' && preg_match('#^(?:/|https?://)[\w\-./:?=&%\#~]{1,300}$#', $u)) ? $u : '';
    … …  
    239320            }
    240321            update_option('anni_consent', wp_json_encode($out, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), true);
    241             return array('ok' => true, 'consent' => true, 'mode' => $out['mode']);
     322            return array('ok' => true, 'consent' => true, 'mode' => $out['mode'], 'cmp' => !empty($out['cmp']));
    242323        },
    243324    ));
    … …  
    250331                return new WP_Error('anni_bad_id', 'Ungueltige Mess-ID.', array('status' => 400));
    251332            }
     333            // 1.5.0: beibehalten=true → eine bisher hinterlegte, andere Mess-ID bleibt als weitere aktiv (nie ersetzen)
     334            $alt = (string) get_option('anni_ga4_id', '');
     335            if ($req->get_param('beibehalten') && $alt !== '' && $alt !== $mid && preg_match('/^G-[A-Z0-9]{4,20}$/', $alt)) {
     336                $w = anni_marketing_weitere($mid);
     337                if (!in_array($alt, $w, true)) {
     338                    // voll: lieber nichts aendern als einen fremden Code still fallen zu lassen
     339                    if (count($w) >= 3) { return new WP_Error('anni_weitere_voll', 'Zu viele weitere Mess-IDs.', array('status' => 409)); }
     340                    $w[] = $alt;
     341                }
     342                update_option('anni_ga4_weitere', wp_json_encode($w), true);
     343            }
     344            // Messkonzept L7a (30.10.2026, 1.9.2): weitere ID entfernen, wenn die Website sie inzwischen selbst laedt (sonst
     345            // doppelt). Mit Eintrag im Verlauf (Art «ga4_weitere») — Ruecknahme ueber anni/v1/rollback/{change_id}.
     346            $weg = $req->get_param('entfernen');
     347            $change_id = 0;
     348            if (is_array($weg) && $weg) {
     349                $r = anni_ga4_weitere_ohne(anni_marketing_weitere($mid), $weg);
     350                if (is_wp_error($r)) { return $r; }
     351                $change_id = (int) $r;
     352            }
    252353            update_option('anni_ga4_id', $mid, true);
    253             return array('ok' => true, 'ga4_id' => $mid);
     354            return array('ok' => true, 'ga4_id' => $mid, 'ga4_weitere' => anni_marketing_weitere($mid), 'change_id' => $change_id);
    254355        },
    255356    ));
  • anni-marketing/trunk/includes/ki-technik.php

    r3757258 r3780762  
    1414 *   GET  anni/v1/ki-technik             Stand, Erkennung (robots.txt-Datei, blog_public, fremdes Schema), Verlauf
    1515 *   POST anni/v1/ki-technik             {robots?:{suchbots:true, training:"erlauben|sperren|unveraendert"},
    16  *                                        firma?:{JSON-LD}|null, faq?:bool, llms?:bool, llms_beschreibung?:string}
     16 *                                        firma?:{JSON-LD}|null, faq?:bool, llms?:bool, llms_beschreibung?:string,
     17 *                                        indexnow?:string (1.7.0: IndexNow-Schlüssel, 8–128 Zeichen a-f 0-9 -; "" = aus)}
    1718 *                                       Liegt eine echte robots.txt-Datei im Website-Ordner, schreibt das Plugin
    1819 *                                       sie (WP_Filesystem, Vorher-Stand im Verlauf); sonst Filter robots_txt.
    … …  
    2021 *   POST anni/v1/ki-technik/nap         {ersetzungen:[{von,nach,feld}], freigabe, dry_run?, auffangnetz?}
    2122 *                                       oder {art:"block_ergaenzen", seite, inhalt, freigabe}
     23 *   POST anni/v1/ki-technik/seiten      Kurzfassung, Fragen und Inhaltsverzeichnis auf einer Seite (1.7.0, includes/ki-seiten.php)
     24 *   POST anni/v1/ki-technik/links       Hinweis-Links auf einer bestehenden Seite (1.8.0, includes/ki-links.php)
     25 *   GET  anni/v1/ki-bots                Abrufe durch KI-Dienste je Tag (1.7.0, includes/ki-bots.php)
    2226 *   Rücknahme über anni/v1/rollback/{change_id}.
     27 *
     28 * llms.txt (1.7.0): standardmässig aus und kein beworbener Teil mehr (diva-e/Adobe 10/2026: hat sich nicht durchgesetzt,
     29 * Standard bleibt robots.txt). Wer sie schon eingeschaltet hat, behält sie; Anni schaltet sie nicht mehr ein.
    2330 *
    2431 * Herkunft: eigener Code; Verlauf und Konfliktprüfung nach includes/ledger.php und includes/ladezeit.php.
    … …  
    6976        'firma' => isset($roh['firma']) && is_array($roh['firma']) ? $roh['firma'] : null,
    7077        'faq' => !empty($roh['faq']),
    71         'llms' => !empty($roh['llms']),
     78        'llms' => isset($roh['llms']) && $roh['llms'] === true,   // 1.7.0: ohne ausdrückliches «an» bleibt llms.txt aus
    7279        'llms_beschreibung' => isset($roh['llms_beschreibung']) ? (string) $roh['llms_beschreibung'] : '',
     80        'indexnow' => isset($roh['indexnow']) && anni_ki_indexnow_gueltig($roh['indexnow']) ? (string) $roh['indexnow'] : '',
    7381        'ersetzen' => $regeln,
    7482    );
     83}
     84
     85/** 1.7.0: IndexNow-Schlüssel nach indexnow.org (8–128 Zeichen: a-z, A-Z, 0-9, Bindestrich). */
     86function anni_ki_indexnow_gueltig($k) {
     87    return is_string($k) && preg_match('/^[A-Za-z0-9\-]{8,128}$/', $k) === 1;
    7588}
    7689
    … …  
    172185            return array(function () use ($opt) { return get_option($opt); },
    173186                function ($w) use ($opt) { update_option($opt, $w); return true; });
     187        case 'ki_seiten':
     188            // 1.7.0: Bausteine EINER Seite (Kurzfassung, Fragen, Inhaltsverzeichnis) — Rücknahme betrifft nur diese Seite.
     189            $pid = (int) $doc_id;
     190            if ($pid <= 0 || !function_exists('anni_ki_seite_lesen')) { return null; }
     191            return array(function () use ($pid) { return anni_ki_seite_lesen($pid); },
     192                function ($w) use ($pid) { return anni_ki_seite_setzen($pid, $w); });
     193        case 'ki_links':
     194            // 1.8.0 (S41): Hinweis-Links EINER Seite — Rücknahme betrifft nur diese Seite, post_content bleibt unberührt.
     195            $pid = (int) $doc_id;
     196            if ($pid <= 0 || !function_exists('anni_ki_links_lesen')) { return null; }
     197            return array(function () use ($pid) { return anni_ki_links_lesen($pid); },
     198                function ($w) use ($pid) { return anni_ki_links_setzen($pid, $w); });
    174199        case 'ki_inhalt':
    175200            $pid = (int) $doc_id;
    … …  
    181206                });
    182207    }
    183     return null;
     208    // 1.9.0: Arten aus eigenen Dateien (Filter anni_ki_arten_weitere) — array(lesen, setzen) oder null
     209    $weitere = function_exists('apply_filters') ? apply_filters('anni_ki_zugriff_weitere', null, $art, $doc_id, $ziel) : null;
     210    return is_array($weitere) && count($weitere) === 2 && is_callable($weitere[0]) && is_callable($weitere[1]) ? $weitere : null;
    184211}
    185212
    … …  
    203230
    204231function anni_ki_arten() {
    205     return array('ki_technik', 'ki_sichtbar', 'ki_robots_datei', 'ki_widget', 'ki_thememod', 'ki_inhalt');
     232    $a = array('ki_technik', 'ki_sichtbar', 'ki_robots_datei', 'ki_widget', 'ki_thememod', 'ki_inhalt', 'ki_seiten', 'ki_links');
     233    // 1.9.0: weitere Arten aus eigenen Dateien (weiterleitungen.php, seo-felder.php) — sie bringen Leser/Schreiber selbst mit
     234    return array_values(array_unique(array_merge($a, array_filter((array) (function_exists('apply_filters') ? apply_filters('anni_ki_arten_weitere', array()) : array()), 'is_string'))));
    206235}
    207236
    … …  
    250279    $t = anni_ledger_tabelle();
    251280    $a = anni_ki_arten();
     281    $platz = implode(', ', array_fill(0, count($a), '%s'));   // 1.7.0: so viele Platzhalter wie Arten (vorher fest 6)
    252282    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- eigene Verlaufstabelle, Name aus $wpdb->prefix
    253283    $zeilen = $wpdb->get_results($wpdb->prepare(
    254         // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Tabellenname aus anni_ledger_tabelle()
    255         "SELECT id, doc_id, element_ids, aktion, auftrag, ruecknahme_von, zurueckgenommen_durch, erstellt, nachher FROM {$t} WHERE element_ids IN (%s, %s, %s, %s, %s, %s) ORDER BY id DESC LIMIT %d",
    256         $a[0], $a[1], $a[2], $a[3], $a[4], $a[5], max(1, min(50, (int) $limit))), ARRAY_A) ?: array();
     284        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Tabellenname aus anni_ledger_tabelle(), Platzhalter gezählt
     285        "SELECT id, doc_id, element_ids, aktion, auftrag, ruecknahme_von, zurueckgenommen_durch, erstellt, nachher FROM {$t} WHERE element_ids IN ({$platz}) ORDER BY id DESC LIMIT %d",
     286        array_merge($a, array(max(1, min(50, (int) $limit))))), ARRAY_A) ?: array();
    257287    $out = array();
    258288    foreach ($zeilen as $z) {
    … …  
    856886        }
    857887    }
    858     if ($cfg['faq'] && !empty($kontext['beitrag']) && (string) $kontext['inhalt'] !== '') {
     888    // 1.7.0: freigegebene Fragen einer Seite (includes/ki-seiten.php) → FAQPage aus genau diesen Fragen. Dann kein zweites
     889    // FAQPage aus den Überschriften; hat die Seite schon ein fremdes FAQ-Schema, bleibt es beim sichtbaren Block.
     890    $seiten_faq = !empty($kontext['seite_faq']) && function_exists('anni_ki_seite_faq_schema') ? anni_ki_seite_faq_schema($kontext['seite_faq']) : null;
     891    if ($seiten_faq) {
     892        $vorhanden = $vorhanden ?: anni_ki_html_schema($html);
     893        if (!$vorhanden['faq']) { $zusatz .= anni_ki_ld_tag($seiten_faq, 'anni-seiten-faq'); }
     894    } elseif ($cfg['faq'] && !empty($kontext['beitrag']) && (string) $kontext['inhalt'] !== '') {
    859895        $vorhanden = $vorhanden ?: anni_ki_html_schema($html);
    860896        $faq = $vorhanden['faq'] ? null : anni_ki_faq_schema($kontext['inhalt']);
    … …  
    875911    if ($m !== 'GET' && $m !== 'HEAD') { return false; }
    876912    $c = anni_ki_holen();
    877     return $c['firma'] || $c['faq'] || $c['ersetzen'];
     913    return $c['firma'] || $c['faq'] || $c['ersetzen'] || (function_exists('anni_ki_seiten_alle') && anni_ki_seiten_alle());
    878914}
    879915
    … …  
    882918    $startseite = is_front_page();
    883919    $beitrag = is_singular() && !$startseite;
    884     ob_start(function ($html) use ($startseite, $beitrag) {
     920    $seite = is_singular() && function_exists('anni_ki_seite_lesen') ? anni_ki_seite_lesen((int) get_queried_object_id()) : null;
     921    $seite_faq = is_array($seite) && !empty($seite['faq']) ? $seite['faq'] : array();
     922    ob_start(function ($html) use ($startseite, $beitrag, $seite_faq) {
    885923        foreach (headers_list() as $kopf) {
    886924            if (stripos($kopf, 'content-type:') === 0 && stripos($kopf, 'text/html') === false) { return $html; }
    887925        }
    888926        return anni_ki_html_anwenden($html, anni_ki_holen(), array('startseite' => $startseite, 'beitrag' => $beitrag,
    889             'inhalt' => $GLOBALS['anni_ki_inhalt'], 'seo_weg' => $GLOBALS['anni_ki_schema_weg']));
     927            'inhalt' => $GLOBALS['anni_ki_inhalt'], 'seo_weg' => $GLOBALS['anni_ki_schema_weg'], 'seite_faq' => $seite_faq));
    890928    });
    891929}, PHP_INT_MAX - 1);
    892930
    893 /* ── llms.txt (nicht beworben: kein Link, kein Eintrag in robots/sitemap) ────────────────── */
     931/* ── llms.txt (nicht beworben: kein Link, kein Eintrag in robots/sitemap; seit 1.7.0 standardmässig aus) ── */
    894932
    895933/*
    … …  
    9661004}, 0);
    9671005
     1006/* ── IndexNow-Schlüsseldatei (1.7.0) ─────────────────────────────────────────────────────────
     1007 * Bing bestätigt einen IndexNow-Ping nur, wenn https://<website>/<schlüssel>.txt genau den Schlüssel liefert. Anni setzt
     1008 * den Schlüssel über anni/v1/ki-technik; die Datei wird wie /llms.txt ohne Rewrite-Regel beim Start beantwortet. Kein
     1009 * Eintrag in robots.txt oder Sitemap, keine Daten nach aussen. */
     1010/** Schlüssel, wenn $pfad genau die Schlüsseldatei ist (Schalter an, Schlüssel gesetzt), sonst null. */
     1011function anni_ki_indexnow_antwort($pfad) {
     1012    if (!anni_ki_erlaubt()) { return null; }
     1013    $cfg = anni_ki_holen();
     1014    if ($cfg['indexnow'] === '') { return null; }
     1015    $soll = trailingslashit((string) wp_parse_url(home_url('/'), PHP_URL_PATH)) . $cfg['indexnow'] . '.txt';
     1016    return (string) $pfad === $soll ? $cfg['indexnow'] : null;
     1017}
     1018
     1019add_action('parse_request', function () {
     1020    $pfad = isset($_SERVER['REQUEST_URI']) ? (string) wp_parse_url(wp_unslash($_SERVER['REQUEST_URI']), PHP_URL_PATH) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
     1021    $key = anni_ki_indexnow_antwort($pfad);
     1022    if ($key === null) { return; }
     1023    status_header(200);
     1024    header('Content-Type: text/plain; charset=utf-8');
     1025    header('Cache-Control: public, max-age=3600');
     1026    echo $key; // phpcs:ignore WordPress.Security.EscapeOutput -- geprüfter Schlüssel (a-z, 0-9, -)
     1027    exit;
     1028}, 0);
     1029
    9681030/* ── Einstellungsseite: Zustimmung ───────────────────────────────────────────────────────── */
    9691031
    … …  
    9831045        echo '<p><label><input type="checkbox" name="' . esc_attr(ANNI_KI_ERLAUBT) . '" value="1" ' . checked(anni_ki_erlaubt(), true, false) . '> '
    9841046            . 'Anni darf Website-Technik anpassen</label></p>';
    985         echo '<p class="description">Firmen-Angaben für Suchmaschinen (Schema), Fragen-Schema in Beiträgen, Zugang für KI-Suchdienste in der robots.txt, '
    986             . 'llms.txt und Korrekturen deiner Firmenangaben. Jede Änderung steht im Anni-Verlauf und lässt sich rückgängig machen. '
     1047        echo '<p class="description">Firmen-Angaben für Suchmaschinen (Schema), Fragen-Schema in Beiträgen, Zugang für KI-Suchdienste in der robots.txt '
     1048            . 'und Korrekturen deiner Firmenangaben. Sichtbare Ergänzungen auf deinen Seiten (Kurzfassung, Fragen, Inhaltsverzeichnis) '
     1049            . 'nur nach deiner Freigabe in Anni. Jede Änderung steht im Anni-Verlauf und lässt sich rückgängig machen. '
    9871050            . 'Ausgeschaltet gibt das Plugin nichts davon aus und ändert nichts mehr.</p>';
    9881051        submit_button();
    … …  
    10161079        ),
    10171080        'verlauf' => anni_ki_verlauf(20),
     1081        // 1.7.0: welche Seiten Anni-Bausteine tragen (je Seite: Kurzfassung ja/nein, Anzahl Fragen, Inhaltsverzeichnis)
     1082        'seiten' => function_exists('anni_ki_seiten_uebersicht') ? anni_ki_seiten_uebersicht() : array(),
     1083        // 1.8.0 (S41): welche Seiten Hinweis-Links von Anni tragen (je Seite die Linkziele)
     1084        'links' => function_exists('anni_ki_links_uebersicht') ? anni_ki_links_uebersicht() : array(),
    10181085    ));
    10191086}
    … …  
    10501117            }
    10511118            if (array_key_exists('llms_beschreibung', $j)) { $neu['llms_beschreibung'] = mb_substr(sanitize_text_field((string) $j['llms_beschreibung']), 0, 300); }
     1119            if (array_key_exists('indexnow', $j)) {
     1120                if (!($j['indexnow'] === '' || anni_ki_indexnow_gueltig($j['indexnow']))) {
     1121                    return new WP_Error('anni_ki_abgelehnt', 'Abgelehnt: indexnow braucht 8 bis 128 Zeichen (Buchstaben, Ziffern, Bindestrich) oder "".', array('status' => 400));
     1122                }
     1123                $neu['indexnow'] = (string) $j['indexnow'];
     1124            }
    10521125            $auftrag = anni_rest_auftrag($req);
    10531126            $antwort = array('ok' => true, 'change_id' => 0, 'robots_datei' => null);
  • anni-marketing/trunk/includes/ledger.php

    r3745272 r3780762  
    152152 */
    153153function anni_ledger_ohne_kern_sql() {
    154     return "AND aktion NOT IN ('seo','alt','status','titelbild','kern')";
     154    // 1.9.0: Block-Inhalte (includes/bloecke.php) haben eine eigene Konfliktprüfung über post_content.
     155    return "AND aktion NOT IN ('seo','alt','status','titelbild','kern') AND element_ids NOT IN ('bloecke','kopffuss')";
    155156}
    156157
    … …  
    259260    if ($nur_aktion !== '' && (string) $zeile['aktion'] !== (string) $nur_aktion) {
    260261        return new WP_Error('anni_change_falsche_art', 'Diese Änderung gehört nicht hierher. Ich nehme nichts zurück.', array('status' => 409, 'aktion' => (string) $zeile['aktion']));
     262    }
     263    // 1.9.0 (W1.36/W1.31): Gutenberg-/klassische Inhalte (includes/bloecke.php) — eigene Prüfsumme über post_content.
     264    if (function_exists('anni_bloecke_ist_eintrag') && anni_bloecke_ist_eintrag($zeile)) {
     265        return anni_bloecke_zuruecknehmen($zeile, $auftrag);
    261266    }
    262267    // Bild-Optimierung (includes/media.php): Rücknahme = Original des Bilds wiederherstellen.
  • anni-marketing/trunk/includes/rest-patch.php

    r3745272 r3780762  
    4343    $out = array();
    4444    foreach ($zeilen as $z) {
     45        // 1.9.0 (W1.36/W1.31): Block-Inhalte mit eigener Rücknahme-Prüfung (includes/bloecke.php).
     46        if (function_exists('anni_bloecke_ausgabe') && ($eigen = anni_bloecke_ausgabe($z)) !== null) { $out[] = $eigen; continue; }
    4547        if (function_exists('anni_kern_ist_aktion') && anni_kern_ist_aktion($z['aktion'])) {
    4648            $out[] = anni_ledger_fuer_ausgabe($z, (int) $z['ruecknahme_von'] === 0 && anni_kern_ruecknahme_pruefen($z) === true);
    … …  
    9193            // «nur»: optional die Art der Änderung, die zurückgenommen werden darf (z. B. «kit»).
    9294            $nur = (string) $req->get_param('nur');
    93             if ($nur !== '' && !preg_match('/^[a-z_]{1,20}$/', $nur)) { $nur = '-'; }
     95            // 1.9.3 (A4): Ziffern erlaubt (Art «ga4_weitere»); ungueltig → «-» (nimmt nichts zurueck, nie still alles)
     96            if ($nur !== '' && !preg_match('/^[a-z0-9_]{1,20}$/', $nur)) { $nur = '-'; }
    9497            return anni_ledger_zuruecknehmen((string) $req['id'], anni_rest_auftrag($req), $bestaetigt, $nur);
    9598        },
  • anni-marketing/trunk/includes/structure.php

    r3745272 r3780762  
    397397    }
    398398    if (!preg_match('/^[A-Za-z0-9]{1,20}$/', $eid)) {
    399         return new WP_Error('anni_struktur_eingabe', 'Bitte gib die Element-ID an.', array('status' => 400));
     399        // Eigener Code (Leitstelle 22.10.2026): die App übersetzt ihn in Kundensprache; der Text ist nur Rückfall.
     400        return new WP_Error('anni_element_id_fehlt', 'Diesen Abschnitt finde ich auf der Seite nicht mehr. Lade die Seite bitte neu.', array('status' => 400));
    400401    }
    401402    if ($op === 'hide_on') {
  • anni-marketing/trunk/readme.txt

    r3757258 r3780762  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.4.3
     7Stable tag: 1.10.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    1515Anni is a marketing assistant for small businesses in Switzerland (anni-marketing.ch). This companion plugin lets Anni finish your Google setup automatically after you connect your WordPress site:
    1616
    17 * Outputs the **Google Analytics 4** tag (gtag.js) in your site's head — only if you (or Anni, with your permission) configure a measurement ID.
     17* Outputs the **Google Analytics 4** tag (gtag.js) in your site's head — only if you (or Anni, with your permission) configure a measurement ID. An analytics ID that was already configured stays active alongside Anni's (it is never replaced).
    1818* Outputs the **Google Search Console** site-verification meta tag — only if a verification token is configured.
     19* Counts visits of known AI crawlers per day (only the date, the crawler name and the count — no IP address, no visitor
     20  data). The numbers stay on your site until Anni reads them through the authenticated endpoint.
    1921* Provides a small authenticated REST endpoint (`anni/v1`) so the Anni app can read plugin status and set the two values above. All endpoints require the `manage_options` capability (administrators). Requests are authenticated with WordPress Application Passwords.
    2022
    … …  
    2830
    2931== Frequently Asked Questions ==
     32
     33= How is the plugin updated? =
     34Only through the normal WordPress update system, from WordPress.org. Anni can start an update that WordPress already
     35offers (like clicking "Update now"); your settings and the change history stay as they are. Automatic updates are only
     36enabled if you choose so, either on the Plugins screen or by agreeing in Anni.
    3037
    3138= Does this plugin track my visitors? =
    … …  
    6370* `POST /wp-json/anni/v1/ga4` — stores your Google Analytics 4 measurement ID (requires `manage_options`).
    6471* `POST /wp-json/anni/v1/gsc` — stores your Google Search Console verification token (requires `manage_options`).
     72* `GET /wp-json/anni/v1/ki-bots` — reads the daily counts of AI crawler visits (service name and number only;
     73  requires `manage_options`).
     74* `POST /wp-json/anni/v1/ki-technik/seiten` — stores a summary, questions and a table of contents for one page that
     75  you approved in Anni (requires `manage_options`).
    6576
    6677Data involved: your GA4 measurement ID and your Search Console verification token. No visitor data,
    … …  
    7687
    7788== Changelog ==
     89
     90= 1.10.0 =
     91* Updates without data loss: new authenticated endpoint `anni/v1/aktualisieren` lets Anni start the update that
     92  WordPress itself offers for this plugin (same as "Update now" on the Plugins screen). The plugin stays active; its
     93  settings and the change history (`anni_changes`) are kept. The new version always comes from WordPress.org through
     94  the regular WordPress update system; the plugin contains no update server of its own.
     95* Automatic updates for this plugin are only switched on when you ask for it (same setting as "Enable auto-updates" on
     96  the Plugins screen). If your site has disabled automatic plugin updates, the plugin leaves that setting untouched.
     97* Site owners can turn the endpoint off with `add_filter( 'anni_marketing_selbst_aktualisieren', '__return_false' );`.
     98
     99= 1.9.3 =
     100* Consent: with Anni's notice switched off and your own Google tag running without a cookie tool, the plugin no longer
     101  prints a site-wide consent default either (Anni does not measure without its notice).
     102* Undo: the type filter of `anni/v1/rollback` (`nur`) now accepts digits (e.g. `ga4_weitere`); an invalid value still
     103  undoes nothing.
     104
     105= 1.9.2 =
     106* Google Analytics: if your site already loads a measurement ID itself (theme or another plugin), Anni no longer loads
     107  it a second time as an additional ID, so visits are not counted twice. Anni can remove such an additional ID again
     108  (`anni/v1/ga4` with `entfernen`).
     109* Consent: if your site runs its own Google tag without a cookie tool, Anni's notice no longer sets a site-wide consent
     110  default or update (`nur_eigen`); it only applies to Anni's own measurement, which still loads after consent.
     111
     112= 1.9.1 =
     113* All in One SEO: on an existing entry Anni now changes only the title and description columns directly; all other
     114  columns (e.g. schema) stay byte-identical, also after undo. A new entry is still removed completely on undo. After
     115  each change the post cache and the All in One SEO caches are cleared, so the page title shows the new text at once.
     116
     117= 1.9.0 =
     118* SEO title and description for All in One SEO, SEOPress, The SEO Framework and Slim SEO: Anni writes them only into
     119  the SEO plugin that outputs your page title (never into two plugins), with the storage each plugin uses itself
     120  (AIOSEO table via its own model, post meta for the others). Every change is in the Anni history and can be undone
     121  (`anni/v1/rollback`).
     122* Block and classic pages (not Elementor): replace an image (image, cover, media & text blocks and images in classic
     123  content) with a new image and alt text, when you confirm it in Anni. Only that spot changes; everything else in the
     124  page content stays byte for byte the same (checked after saving, otherwise the old content is restored). The plugin
     125  only writes when the page is still exactly as Anni read it. Every change is in the Anni history and can be undone
     126  (`anni/v1/bloecke/{id}`, `anni/v1/rollback`).
     127* Block and classic pages (not Elementor): duplicate, move and delete sections (top-level blocks; on classic pages the
     128  parts between blank lines, only where the structure is unambiguous) and hide blocks per device using WordPress' own
     129  block visibility (WordPress 7.0 or newer). Deleting needs a confirmation in Anni. Same checks, history and undo as above.
     130* Header and footer: change a text there (e.g. address, opening hours, phone) after you confirm it in Anni. Block themes:
     131  the header/footer template parts (saved like the Site Editor does; a theme part becomes a customized copy, undo removes
     132  that copy again). Classic themes: widgets in the sidebars and short contact/footer texts in the Customizer. Texts
     133  written into the theme's code cannot be changed here. History and undo as above (`anni/v1/kopffuss`).
     134* Customizer text settings never receive HTML: writing text containing "<" into a theme setting is refused.
     135* 301 redirects without an extra plugin: after you approve it in your Anni account, an old address of your site (e.g.
     136  a renamed or deleted page) leads permanently to a published page of the same site. Only used when WordPress itself
     137  finds nothing at the old address (404); never the home page, never system paths, never another domain, no loops or
     138  chains. Stored in the option `anni_weiterleitungen` (at most 200 rules); every rule is in the Anni history and can be
     139  undone (`anni/v1/rollback`). If a redirect plugin is active (Redirection, Rank Math redirections, Yoast Premium,
     140  SEOPress PRO, AIOSEO Redirects, Slim SEO, Safe Redirect Manager, Simple 301 Redirects), Anni adds no rule of its own. Redirects keep working when "Anni may adjust site technology" is off.
     141
     142= 1.8.0 =
     143* Links between your own pages: after you approve a suggestion in your Anni account, the plugin shows one short
     144  paragraph with a link to a matching page of your site ("Passend dazu: ...") below the content of an existing page,
     145  e.g. for a page no other page links to. Stored per page in the option `anni_ki_links`, never written into your page
     146  content; link targets must be published pages of the same site. No extra block on posts when YARPP, Jetpack Related
     147  Posts or Contextual Related Posts already show related posts. Every change is in the Anni history and can be undone
     148  (`anni/v1/rollback`). Nothing is shown without your approval.
     149
     150= 1.7.0 =
     151* Short summary, questions and table of contents on existing pages: after you approve a draft in your Anni account,
     152  the plugin shows 2-3 sentences "Kurz gesagt" at the top of that page, a table of contents from its H2/H3 headings
     153  (long pages only) and visible answers to common questions with FAQPage schema. Stored per page in the option
     154  `anni_ki_seiten`, never written into your page content; works with Elementor pages. Every change is in the Anni
     155  history and can be undone (`anni/v1/rollback`). Nothing is shown without your approval.
     156* Counts how often AI services read your site (GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-SearchBot,
     157  PerplexityBot, CCBot and others): only the day, the service name and the count are stored (option `anni_ki_bots`,
     158  at most 90 days) — no IP address, no page, no visitor data. Anni reads the numbers via the authenticated
     159  `anni/v1/ki-bots` endpoint. Requests answered by a page cache or CDN are not counted.
     160* Existing cookie tool: if your Anni account is linked to the cookie tool already running on your site, the plugin
     161  shows no Anni cookie notice of its own. Google Analytics then loads only after visitors agree to statistics in your
     162  cookie tool (no consent, unreadable or declined: nothing is loaded). Without such a link nothing changes.
     163* Cookie notice: decisions made by automated browsers (navigator.webdriver, headless browsers) and known bots or
     164  crawlers are no longer counted in the anonymous consent statistics.
     165* /llms.txt is off by default and no longer switched on by Anni (it has not become an established standard; robots.txt
     166  stays the standard). Sites that already use it keep it unchanged.
     167* IndexNow key file: when Anni stores an IndexNow key through `anni/v1/ki-technik` (field `indexnow`, 8 to 128 letters,
     168  digits or hyphens; an empty value turns it off), the site answers `/<key>.txt` with that key as plain text. Search
     169  engines that take part in IndexNow (for example Bing) use this file to confirm that a reported page belongs to the
     170  site. Nothing is added to robots.txt or the sitemap, and the plugin sends nothing anywhere itself.
     171
     172= 1.6.0 =
     173* Read-only security check for administrators (`anni/v1/sicherheit`): compares WordPress core files with the official
     174  checksums, lists PHP files inside the uploads folder and lists must-use plugins and drop-ins (flagging typical
     175  malicious patterns). Only file names and counts are reported, never file contents. Nothing on the site is changed.
     176  Anni uses this when you connect your site and pauses all changes if the site shows signs of a compromise.
     177* An analytics measurement ID that was already configured is kept active next to Anni's own (option
     178  `anni_ga4_weitere`, at most three IDs) instead of being replaced. Both respect the consent banner.
     179* The WordPress "Authorize Application" screen for the Anni connection is easier to follow: only when the
     180  request returns to Anni's own connection callback (app.anni-marketing.ch), other admin notices are hidden on that one screen, a one-line instruction
     181  points to the approve button, and the application is shown as "Anni" (the stored password name is unchanged).
     182* Security: the plugin only overrides a security plugin's application-password block for requests that return to
     183  Anni's own connection callback; a request that merely uses the name "Anni" is treated like any other application.
     184* The consent banner can show the site's own logo: an https address or a small embedded PNG/WebP image (data URI),
     185  nothing else. Light logos get a dark background so they stay visible. No image is loaded from Anni's server.
     186
     187= 1.4.4 =
     188* The authenticated status endpoint (`anni/v1/status`, administrators only) also reports the PHP and WordPress
     189  version, so Anni can tell you when your site runs outdated or insecure software.
    78190
    79191= 1.4.3 =
  • anni-marketing/trunk/uninstall.php

    r3757258 r3780762  
    1616}
    1717
    18 $anni_optionen = array('anni_ledger_schema', 'anni_struktur_nonces', 'anni_marketing_hinweis', 'anni_ga4_id', 'anni_gsc_verification', 'anni_consent', 'anni_ladezeit', 'anni_ki_technik', 'anni_ki_erlaubt', 'anni_ki_beobachtet');
     18$anni_optionen = array('anni_ledger_schema', 'anni_struktur_nonces', 'anni_marketing_hinweis', 'anni_ga4_id', 'anni_ga4_weitere', 'anni_gsc_verification', 'anni_consent', 'anni_ladezeit', 'anni_ki_technik', 'anni_ki_erlaubt', 'anni_ki_beobachtet', 'anni_ki_seiten', 'anni_ki_bots', 'anni_ki_links', 'anni_weiterleitungen', 'anni_aktualisierung', 'anni_auto_update_auftrag');
    1919delete_transient('anni_llms_cache');
    2020delete_transient('anni_llms_cache_v2');
     21delete_transient('anni_akt_frisch');
     22delete_transient('anni_akt_laeuft');
    2123foreach ($anni_optionen as $anni_o) {
    2224    delete_option($anni_o);
Note: See TracChangeset for help on using the changeset viewer.