Changeset 3780762
- Timestamp:
- 11/02/2026 09:22:02 PM (15 hours ago)
- Location:
- anni-marketing
- Files:
-
- 26 added
- 18 edited
- 1 copied
-
tags/1.10.0 (copied) (copied from anni-marketing/trunk )
-
tags/1.10.0/CREDITS.md (modified) (1 diff)
-
tags/1.10.0/anni-consent.js (modified) (6 diffs)
-
tags/1.10.0/anni-marketing.php (modified) (14 diffs)
-
tags/1.10.0/includes/aktualisieren.php (added)
-
tags/1.10.0/includes/autorisieren.php (added)
-
tags/1.10.0/includes/bloecke.php (added)
-
tags/1.10.0/includes/consent-cmp.php (added)
-
tags/1.10.0/includes/consent-grund.php (added)
-
tags/1.10.0/includes/ga4-weitere.php (added)
-
tags/1.10.0/includes/ki-bots.php (added)
-
tags/1.10.0/includes/ki-links.php (added)
-
tags/1.10.0/includes/ki-seiten.php (added)
-
tags/1.10.0/includes/ki-technik.php (modified) (14 diffs)
-
tags/1.10.0/includes/kopffuss.php (added)
-
tags/1.10.0/includes/ledger.php (modified) (2 diffs)
-
tags/1.10.0/includes/rest-patch.php (modified) (2 diffs)
-
tags/1.10.0/includes/seo-felder.php (added)
-
tags/1.10.0/includes/sicherheit.php (added)
-
tags/1.10.0/includes/structure.php (modified) (1 diff)
-
tags/1.10.0/includes/weiterleitungen.php (added)
-
tags/1.10.0/readme.txt (modified) (5 diffs)
-
tags/1.10.0/uninstall.php (modified) (1 diff)
-
trunk/CREDITS.md (modified) (1 diff)
-
trunk/anni-consent.js (modified) (6 diffs)
-
trunk/anni-marketing.php (modified) (14 diffs)
-
trunk/includes/aktualisieren.php (added)
-
trunk/includes/autorisieren.php (added)
-
trunk/includes/bloecke.php (added)
-
trunk/includes/consent-cmp.php (added)
-
trunk/includes/consent-grund.php (added)
-
trunk/includes/ga4-weitere.php (added)
-
trunk/includes/ki-bots.php (added)
-
trunk/includes/ki-links.php (added)
-
trunk/includes/ki-seiten.php (added)
-
trunk/includes/ki-technik.php (modified) (14 diffs)
-
trunk/includes/kopffuss.php (added)
-
trunk/includes/ledger.php (modified) (2 diffs)
-
trunk/includes/rest-patch.php (modified) (2 diffs)
-
trunk/includes/seo-felder.php (added)
-
trunk/includes/sicherheit.php (added)
-
trunk/includes/structure.php (modified) (1 diff)
-
trunk/includes/weiterleitungen.php (added)
-
trunk/readme.txt (modified) (5 diffs)
-
trunk/uninstall.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
anni-marketing/tags/1.10.0/CREDITS.md
r3745272 r3780762 20 20 | `includes/rest-media.php` | – | REST-Routen `anni/v1/media/*` (eigener Code) | – | 21 21 | `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) | 22 23 | `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) | 24 33 | `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) | 25 36 | `includes/haertung.php` | – | Rate-Grenze (429) und Grössengrenze (413) der Schreib-Routen, Fähigkeiten in der Status-Route (eigener Code, Bauplan-Schritt 9) | – | 26 37 | `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) | – | 27 40 28 41 ## Bewusste Abweichungen von den Vorlagen -
anni-marketing/tags/1.10.0/anni-consent.js
r3756924 r3780762 57 57 if (mode === 'basic') { 58 58 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 59 60 var s = D.createElement('script'); s.async = true; s.src = 'https://www.googletagmanager.com/gtag/js?id=' + encodeURIComponent(C.m); 60 61 (D.head || D.documentElement).appendChild(s); … … 63 64 function apply(st) { 64 65 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 } 65 73 g('consent', 'update', { 66 74 analytics_storage: st.s ? 'granted' : 'denied', … … 72 80 if (st.s) loadGoogle(); 73 81 } 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 } 74 92 function zaehlen(st) { 75 93 if (!C.z) return; 94 if (automat()) return; 76 95 var e = (st.s && (!kat.marketing || st.m)) ? 'granted' : (!st.s && !st.m ? 'denied' : 'partial'); 77 96 try { … … 100 119 '.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}' + 101 120 '.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}' + 102 123 '.h{margin:0 0 6px;font-size:15px;font-weight:650;line-height:1.35}' + 103 124 '.p{margin:0;font-size:13.5px;line-height:1.55;color:' + c.muted + ';max-width:62ch}' + … … 147 168 var sh = root()._sh; 148 169 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>' : ''; 150 174 var link = C.u ? '<a href="' + esc(C.u) + '">' + esc(t.link) + '</a>' : ''; 151 175 var w = el('<div class="w" role="dialog" aria-modal="false" aria-label="' + esc(t.aria) + '"><div class="x">' + firm + … … 154 178 '<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>'); 155 179 sh.appendChild(w); 180 var li = w.querySelector('.lg img'); if (li) li.addEventListener('error', function () { li.parentNode.parentNode.removeChild(li.parentNode); }); 156 181 w.querySelector('[data-a="yes"]').addEventListener('click', function () { decide({ s: true, m: !!kat.marketing }, 'alle'); }); 157 182 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 4 4 * Plugin URI: https://anni-marketing.ch 5 5 * 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.36 * Version: 1.10.0 7 7 * Author: Anni Marketing 8 8 * Author URI: https://digitalerakete.ch … … 16 16 if (!defined('ABSPATH')) { exit; } 17 17 18 define('ANNI_MARKETING_VERSION', '1. 4.3');18 define('ANNI_MARKETING_VERSION', '1.10.0'); 19 19 20 20 // Seitenbearbeitung (Bauplan 07.10.2026, Schritte 0-2): Schema, Änderungsmotor, Verlauf, REST, Abilities. … … 32 32 require_once __DIR__ . '/includes/rest-media.php'; 33 33 require_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). 35 require_once __DIR__ . '/includes/seo-felder.php'; 34 36 // Ladezeit (Bauplan 07.10.2026): sichere Bild-Hebel, einzeln schaltbar, Messung auf dem Anni-Server. 35 37 require_once __DIR__ . '/includes/ladezeit.php'; 36 38 require_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. 40 require_once __DIR__ . '/includes/ki-seiten.php'; 41 require_once __DIR__ . '/includes/ki-bots.php'; 42 // 1.8.0 (S41 Interne Verlinkung): Hinweis-Links auf bestehenden Seiten (nur mit Freigabe, rücknehmbar). 43 require_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. 45 require_once __DIR__ . '/includes/bloecke.php'; 46 // 1.9.0 (W1.73): Kopf-/Fussbereich (Vorlagenteile, Widgets, Customizer-Texte) mit Bestätigung, Verlauf und Rücknahme. 47 require_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). 49 require_once __DIR__ . '/includes/weiterleitungen.php'; 50 // 1.7.0 (28.10.2026): Messung hinter dem vorhandenen Cookie-Tool der Website, dann kein Anni-Hinweis. 51 require_once __DIR__ . '/includes/consent-cmp.php'; 52 // 1.9.2 (Messkonzept L7a): weitere Mess-IDs entfernen mit Verlauf und Ruecknahme 53 require_once __DIR__ . '/includes/ga4-weitere.php'; 54 // 1.9.3 (B7): Consent-Grundeinstellung auch bei abgeschaltetem Hinweis nur fuer Annis Messung 55 require_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. 58 require_once __DIR__ . '/includes/aktualisieren.php'; 37 59 // Härtung (Bauplan-Schritt 9): Rate- und Grössengrenze der Schreib-Routen, Fähigkeiten im Status. 38 60 require_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). 62 require_once __DIR__ . '/includes/autorisieren.php'; 63 // K44 (1.6.0): Sicherheitsprüfung von innen, nur lesend (Kerndateien, Upload-Ordner, versteckte Erweiterungen). 64 require_once __DIR__ . '/includes/sicherheit.php'; 39 65 40 66 /** WPP/GWE (01.09.2026, Echtkundin-Befund): Security-Plugins schalten WordPress- … … 51 77 $name = sanitize_text_field(wp_unslash((string) $_REQUEST['app_name'])); 52 78 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; } 53 83 } 54 84 return true; … … 63 93 $name = isset($request['app_name']) ? (string) $request['app_name'] : ''; 64 94 if (strpos($name, 'Anni') !== 0) { return; } 95 if (!anni_marketing_ist_anni_ziel(isset($request['success_url']) ? (string) $request['success_url'] : '')) { return; } // K38 65 96 foreach ($error->get_error_codes() as $code) { $error->remove($code); } 66 97 }, PHP_INT_MAX, 3); … … 107 138 * Modus basic (Standard): Google-Skript wird erst nach Zustimmung vom Hinweis nachgeladen. 108 139 * 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. */ 142 function 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 109 154 function anni_marketing_consent_config() { 110 155 $mid = get_option('anni_ga4_id', ''); … … 113 158 $cfg = $raw ? json_decode($raw, true) : null; 114 159 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)); } 116 165 $priv = ''; 117 166 if (function_exists('get_privacy_policy_url')) { $priv = (string) get_privacy_policy_url(); } … … 137 186 if (empty($out['f'])) { $out['f'] = (string) get_bloginfo('name'); } 138 187 $out['m'] = $mid; 188 $out['w'] = anni_marketing_weitere($mid); 139 189 $out['enabled'] = true; 140 190 unset($out['_inline']); … … 153 203 . '"ad_user_data":"denied","ad_personalization":"denied","wait_for_update":500});'; 154 204 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; } 155 208 wp_register_script('anni-marketing-consent-default', false, array(), ANNI_MARKETING_VERSION, false); 156 209 wp_enqueue_script('anni-marketing-consent-default'); … … 158 211 return; 159 212 } 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 } 160 220 if ($cfg['mode'] === 'advanced') { 161 221 // Google sofort laden (mit "denied"), der Hinweis schaltet spaeter per consent update frei. 162 222 wp_enqueue_script('anni-marketing-gtag', 'https://www.googletagmanager.com/gtag/js?id=' . rawurlencode($mid), array(), ANNI_MARKETING_VERSION, false); 163 223 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'); 165 227 $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(); 166 231 } else { 167 232 wp_register_script('anni-marketing-consent-default', false, array(), ANNI_MARKETING_VERSION, false); … … 190 255 'ok' => true, 191 256 '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'), 192 261 'ga4_id' => (string) get_option('anni_ga4_id', ''), 193 262 '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'), 195 265 ); 196 266 }, … … 218 288 $out = array(); 219 289 $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; } 220 293 $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'; } 221 298 $out['gleichfarbig'] = !empty($cfg['gleichfarbig']); 222 299 $out['kat'] = array('statistik' => true, 'marketing' => !empty($cfg['kat']['marketing'])); 223 300 $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']); 224 305 $u = isset($cfg['u']) ? trim((string) $cfg['u']) : ''; 225 306 $out['u'] = ($u !== '' && preg_match('#^(?:/|https?://)[\w\-./:?=&%\#~]{1,300}$#', $u)) ? $u : ''; … … 239 320 } 240 321 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'])); 242 323 }, 243 324 )); … … 250 331 return new WP_Error('anni_bad_id', 'Ungueltige Mess-ID.', array('status' => 400)); 251 332 } 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 } 252 353 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); 254 355 }, 255 356 )); -
anni-marketing/tags/1.10.0/includes/ki-technik.php
r3757258 r3780762 14 14 * GET anni/v1/ki-technik Stand, Erkennung (robots.txt-Datei, blog_public, fremdes Schema), Verlauf 15 15 * 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)} 17 18 * Liegt eine echte robots.txt-Datei im Website-Ordner, schreibt das Plugin 18 19 * sie (WP_Filesystem, Vorher-Stand im Verlauf); sonst Filter robots_txt. … … 20 21 * POST anni/v1/ki-technik/nap {ersetzungen:[{von,nach,feld}], freigabe, dry_run?, auffangnetz?} 21 22 * 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) 22 26 * 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. 23 30 * 24 31 * Herkunft: eigener Code; Verlauf und Konfliktprüfung nach includes/ledger.php und includes/ladezeit.php. … … 69 76 'firma' => isset($roh['firma']) && is_array($roh['firma']) ? $roh['firma'] : null, 70 77 '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 72 79 'llms_beschreibung' => isset($roh['llms_beschreibung']) ? (string) $roh['llms_beschreibung'] : '', 80 'indexnow' => isset($roh['indexnow']) && anni_ki_indexnow_gueltig($roh['indexnow']) ? (string) $roh['indexnow'] : '', 73 81 'ersetzen' => $regeln, 74 82 ); 83 } 84 85 /** 1.7.0: IndexNow-Schlüssel nach indexnow.org (8–128 Zeichen: a-z, A-Z, 0-9, Bindestrich). */ 86 function anni_ki_indexnow_gueltig($k) { 87 return is_string($k) && preg_match('/^[A-Za-z0-9\-]{8,128}$/', $k) === 1; 75 88 } 76 89 … … 172 185 return array(function () use ($opt) { return get_option($opt); }, 173 186 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); }); 174 199 case 'ki_inhalt': 175 200 $pid = (int) $doc_id; … … 181 206 }); 182 207 } 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; 184 211 } 185 212 … … 203 230 204 231 function 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')))); 206 235 } 207 236 … … 250 279 $t = anni_ledger_tabelle(); 251 280 $a = anni_ki_arten(); 281 $platz = implode(', ', array_fill(0, count($a), '%s')); // 1.7.0: so viele Platzhalter wie Arten (vorher fest 6) 252 282 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- eigene Verlaufstabelle, Name aus $wpdb->prefix 253 283 $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(); 257 287 $out = array(); 258 288 foreach ($zeilen as $z) { … … 856 886 } 857 887 } 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'] !== '') { 859 895 $vorhanden = $vorhanden ?: anni_ki_html_schema($html); 860 896 $faq = $vorhanden['faq'] ? null : anni_ki_faq_schema($kontext['inhalt']); … … 875 911 if ($m !== 'GET' && $m !== 'HEAD') { return false; } 876 912 $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()); 878 914 } 879 915 … … 882 918 $startseite = is_front_page(); 883 919 $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) { 885 923 foreach (headers_list() as $kopf) { 886 924 if (stripos($kopf, 'content-type:') === 0 && stripos($kopf, 'text/html') === false) { return $html; } 887 925 } 888 926 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)); 890 928 }); 891 929 }, PHP_INT_MAX - 1); 892 930 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) ── */ 894 932 895 933 /* … … 966 1004 }, 0); 967 1005 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. */ 1011 function 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 1019 add_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 968 1030 /* ── Einstellungsseite: Zustimmung ───────────────────────────────────────────────────────── */ 969 1031 … … 983 1045 echo '<p><label><input type="checkbox" name="' . esc_attr(ANNI_KI_ERLAUBT) . '" value="1" ' . checked(anni_ki_erlaubt(), true, false) . '> ' 984 1046 . '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. ' 987 1050 . 'Ausgeschaltet gibt das Plugin nichts davon aus und ändert nichts mehr.</p>'; 988 1051 submit_button(); … … 1016 1079 ), 1017 1080 '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(), 1018 1085 )); 1019 1086 } … … 1050 1117 } 1051 1118 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 } 1052 1125 $auftrag = anni_rest_auftrag($req); 1053 1126 $antwort = array('ok' => true, 'change_id' => 0, 'robots_datei' => null); -
anni-marketing/tags/1.10.0/includes/ledger.php
r3745272 r3780762 152 152 */ 153 153 function 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')"; 155 156 } 156 157 … … 259 260 if ($nur_aktion !== '' && (string) $zeile['aktion'] !== (string) $nur_aktion) { 260 261 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); 261 266 } 262 267 // Bild-Optimierung (includes/media.php): Rücknahme = Original des Bilds wiederherstellen. -
anni-marketing/tags/1.10.0/includes/rest-patch.php
r3745272 r3780762 43 43 $out = array(); 44 44 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; } 45 47 if (function_exists('anni_kern_ist_aktion') && anni_kern_ist_aktion($z['aktion'])) { 46 48 $out[] = anni_ledger_fuer_ausgabe($z, (int) $z['ruecknahme_von'] === 0 && anni_kern_ruecknahme_pruefen($z) === true); … … 91 93 // «nur»: optional die Art der Änderung, die zurückgenommen werden darf (z. B. «kit»). 92 94 $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 = '-'; } 94 97 return anni_ledger_zuruecknehmen((string) $req['id'], anni_rest_auftrag($req), $bestaetigt, $nur); 95 98 }, -
anni-marketing/tags/1.10.0/includes/structure.php
r3745272 r3780762 397 397 } 398 398 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)); 400 401 } 401 402 if ($op === 'hide_on') { -
anni-marketing/tags/1.10.0/readme.txt
r3757258 r3780762 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1. 4.37 Stable tag: 1.10.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 15 15 Anni 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: 16 16 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). 18 18 * 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. 19 21 * 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. 20 22 … … 28 30 29 31 == Frequently Asked Questions == 32 33 = How is the plugin updated? = 34 Only through the normal WordPress update system, from WordPress.org. Anni can start an update that WordPress already 35 offers (like clicking "Update now"); your settings and the change history stay as they are. Automatic updates are only 36 enabled if you choose so, either on the Plugins screen or by agreeing in Anni. 30 37 31 38 = Does this plugin track my visitors? = … … 63 70 * `POST /wp-json/anni/v1/ga4` — stores your Google Analytics 4 measurement ID (requires `manage_options`). 64 71 * `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`). 65 76 66 77 Data involved: your GA4 measurement ID and your Search Console verification token. No visitor data, … … 76 87 77 88 == 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. 78 190 79 191 = 1.4.3 = -
anni-marketing/tags/1.10.0/uninstall.php
r3757258 r3780762 16 16 } 17 17 18 $anni_optionen = array('anni_ledger_schema', 'anni_struktur_nonces', 'anni_marketing_hinweis', 'anni_ga4_id', 'anni_g sc_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'); 19 19 delete_transient('anni_llms_cache'); 20 20 delete_transient('anni_llms_cache_v2'); 21 delete_transient('anni_akt_frisch'); 22 delete_transient('anni_akt_laeuft'); 21 23 foreach ($anni_optionen as $anni_o) { 22 24 delete_option($anni_o); -
anni-marketing/trunk/CREDITS.md
r3745272 r3780762 20 20 | `includes/rest-media.php` | – | REST-Routen `anni/v1/media/*` (eigener Code) | – | 21 21 | `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) | 22 23 | `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) | 24 33 | `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) | 25 36 | `includes/haertung.php` | – | Rate-Grenze (429) und Grössengrenze (413) der Schreib-Routen, Fähigkeiten in der Status-Route (eigener Code, Bauplan-Schritt 9) | – | 26 37 | `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) | – | 27 40 28 41 ## Bewusste Abweichungen von den Vorlagen -
anni-marketing/trunk/anni-consent.js
r3756924 r3780762 57 57 if (mode === 'basic') { 58 58 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 59 60 var s = D.createElement('script'); s.async = true; s.src = 'https://www.googletagmanager.com/gtag/js?id=' + encodeURIComponent(C.m); 60 61 (D.head || D.documentElement).appendChild(s); … … 63 64 function apply(st) { 64 65 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 } 65 73 g('consent', 'update', { 66 74 analytics_storage: st.s ? 'granted' : 'denied', … … 72 80 if (st.s) loadGoogle(); 73 81 } 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 } 74 92 function zaehlen(st) { 75 93 if (!C.z) return; 94 if (automat()) return; 76 95 var e = (st.s && (!kat.marketing || st.m)) ? 'granted' : (!st.s && !st.m ? 'denied' : 'partial'); 77 96 try { … … 100 119 '.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}' + 101 120 '.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}' + 102 123 '.h{margin:0 0 6px;font-size:15px;font-weight:650;line-height:1.35}' + 103 124 '.p{margin:0;font-size:13.5px;line-height:1.55;color:' + c.muted + ';max-width:62ch}' + … … 147 168 var sh = root()._sh; 148 169 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>' : ''; 150 174 var link = C.u ? '<a href="' + esc(C.u) + '">' + esc(t.link) + '</a>' : ''; 151 175 var w = el('<div class="w" role="dialog" aria-modal="false" aria-label="' + esc(t.aria) + '"><div class="x">' + firm + … … 154 178 '<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>'); 155 179 sh.appendChild(w); 180 var li = w.querySelector('.lg img'); if (li) li.addEventListener('error', function () { li.parentNode.parentNode.removeChild(li.parentNode); }); 156 181 w.querySelector('[data-a="yes"]').addEventListener('click', function () { decide({ s: true, m: !!kat.marketing }, 'alle'); }); 157 182 w.querySelector('[data-a="no"]').addEventListener('click', function () { decide({ s: false, m: false }, 'notwendig'); }); -
anni-marketing/trunk/anni-marketing.php
r3757258 r3780762 4 4 * Plugin URI: https://anni-marketing.ch 5 5 * 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.36 * Version: 1.10.0 7 7 * Author: Anni Marketing 8 8 * Author URI: https://digitalerakete.ch … … 16 16 if (!defined('ABSPATH')) { exit; } 17 17 18 define('ANNI_MARKETING_VERSION', '1. 4.3');18 define('ANNI_MARKETING_VERSION', '1.10.0'); 19 19 20 20 // Seitenbearbeitung (Bauplan 07.10.2026, Schritte 0-2): Schema, Änderungsmotor, Verlauf, REST, Abilities. … … 32 32 require_once __DIR__ . '/includes/rest-media.php'; 33 33 require_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). 35 require_once __DIR__ . '/includes/seo-felder.php'; 34 36 // Ladezeit (Bauplan 07.10.2026): sichere Bild-Hebel, einzeln schaltbar, Messung auf dem Anni-Server. 35 37 require_once __DIR__ . '/includes/ladezeit.php'; 36 38 require_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. 40 require_once __DIR__ . '/includes/ki-seiten.php'; 41 require_once __DIR__ . '/includes/ki-bots.php'; 42 // 1.8.0 (S41 Interne Verlinkung): Hinweis-Links auf bestehenden Seiten (nur mit Freigabe, rücknehmbar). 43 require_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. 45 require_once __DIR__ . '/includes/bloecke.php'; 46 // 1.9.0 (W1.73): Kopf-/Fussbereich (Vorlagenteile, Widgets, Customizer-Texte) mit Bestätigung, Verlauf und Rücknahme. 47 require_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). 49 require_once __DIR__ . '/includes/weiterleitungen.php'; 50 // 1.7.0 (28.10.2026): Messung hinter dem vorhandenen Cookie-Tool der Website, dann kein Anni-Hinweis. 51 require_once __DIR__ . '/includes/consent-cmp.php'; 52 // 1.9.2 (Messkonzept L7a): weitere Mess-IDs entfernen mit Verlauf und Ruecknahme 53 require_once __DIR__ . '/includes/ga4-weitere.php'; 54 // 1.9.3 (B7): Consent-Grundeinstellung auch bei abgeschaltetem Hinweis nur fuer Annis Messung 55 require_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. 58 require_once __DIR__ . '/includes/aktualisieren.php'; 37 59 // Härtung (Bauplan-Schritt 9): Rate- und Grössengrenze der Schreib-Routen, Fähigkeiten im Status. 38 60 require_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). 62 require_once __DIR__ . '/includes/autorisieren.php'; 63 // K44 (1.6.0): Sicherheitsprüfung von innen, nur lesend (Kerndateien, Upload-Ordner, versteckte Erweiterungen). 64 require_once __DIR__ . '/includes/sicherheit.php'; 39 65 40 66 /** WPP/GWE (01.09.2026, Echtkundin-Befund): Security-Plugins schalten WordPress- … … 51 77 $name = sanitize_text_field(wp_unslash((string) $_REQUEST['app_name'])); 52 78 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; } 53 83 } 54 84 return true; … … 63 93 $name = isset($request['app_name']) ? (string) $request['app_name'] : ''; 64 94 if (strpos($name, 'Anni') !== 0) { return; } 95 if (!anni_marketing_ist_anni_ziel(isset($request['success_url']) ? (string) $request['success_url'] : '')) { return; } // K38 65 96 foreach ($error->get_error_codes() as $code) { $error->remove($code); } 66 97 }, PHP_INT_MAX, 3); … … 107 138 * Modus basic (Standard): Google-Skript wird erst nach Zustimmung vom Hinweis nachgeladen. 108 139 * 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. */ 142 function 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 109 154 function anni_marketing_consent_config() { 110 155 $mid = get_option('anni_ga4_id', ''); … … 113 158 $cfg = $raw ? json_decode($raw, true) : null; 114 159 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)); } 116 165 $priv = ''; 117 166 if (function_exists('get_privacy_policy_url')) { $priv = (string) get_privacy_policy_url(); } … … 137 186 if (empty($out['f'])) { $out['f'] = (string) get_bloginfo('name'); } 138 187 $out['m'] = $mid; 188 $out['w'] = anni_marketing_weitere($mid); 139 189 $out['enabled'] = true; 140 190 unset($out['_inline']); … … 153 203 . '"ad_user_data":"denied","ad_personalization":"denied","wait_for_update":500});'; 154 204 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; } 155 208 wp_register_script('anni-marketing-consent-default', false, array(), ANNI_MARKETING_VERSION, false); 156 209 wp_enqueue_script('anni-marketing-consent-default'); … … 158 211 return; 159 212 } 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 } 160 220 if ($cfg['mode'] === 'advanced') { 161 221 // Google sofort laden (mit "denied"), der Hinweis schaltet spaeter per consent update frei. 162 222 wp_enqueue_script('anni-marketing-gtag', 'https://www.googletagmanager.com/gtag/js?id=' . rawurlencode($mid), array(), ANNI_MARKETING_VERSION, false); 163 223 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'); 165 227 $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(); 166 231 } else { 167 232 wp_register_script('anni-marketing-consent-default', false, array(), ANNI_MARKETING_VERSION, false); … … 190 255 'ok' => true, 191 256 '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'), 192 261 'ga4_id' => (string) get_option('anni_ga4_id', ''), 193 262 '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'), 195 265 ); 196 266 }, … … 218 288 $out = array(); 219 289 $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; } 220 293 $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'; } 221 298 $out['gleichfarbig'] = !empty($cfg['gleichfarbig']); 222 299 $out['kat'] = array('statistik' => true, 'marketing' => !empty($cfg['kat']['marketing'])); 223 300 $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']); 224 305 $u = isset($cfg['u']) ? trim((string) $cfg['u']) : ''; 225 306 $out['u'] = ($u !== '' && preg_match('#^(?:/|https?://)[\w\-./:?=&%\#~]{1,300}$#', $u)) ? $u : ''; … … 239 320 } 240 321 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'])); 242 323 }, 243 324 )); … … 250 331 return new WP_Error('anni_bad_id', 'Ungueltige Mess-ID.', array('status' => 400)); 251 332 } 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 } 252 353 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); 254 355 }, 255 356 )); -
anni-marketing/trunk/includes/ki-technik.php
r3757258 r3780762 14 14 * GET anni/v1/ki-technik Stand, Erkennung (robots.txt-Datei, blog_public, fremdes Schema), Verlauf 15 15 * 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)} 17 18 * Liegt eine echte robots.txt-Datei im Website-Ordner, schreibt das Plugin 18 19 * sie (WP_Filesystem, Vorher-Stand im Verlauf); sonst Filter robots_txt. … … 20 21 * POST anni/v1/ki-technik/nap {ersetzungen:[{von,nach,feld}], freigabe, dry_run?, auffangnetz?} 21 22 * 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) 22 26 * 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. 23 30 * 24 31 * Herkunft: eigener Code; Verlauf und Konfliktprüfung nach includes/ledger.php und includes/ladezeit.php. … … 69 76 'firma' => isset($roh['firma']) && is_array($roh['firma']) ? $roh['firma'] : null, 70 77 '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 72 79 'llms_beschreibung' => isset($roh['llms_beschreibung']) ? (string) $roh['llms_beschreibung'] : '', 80 'indexnow' => isset($roh['indexnow']) && anni_ki_indexnow_gueltig($roh['indexnow']) ? (string) $roh['indexnow'] : '', 73 81 'ersetzen' => $regeln, 74 82 ); 83 } 84 85 /** 1.7.0: IndexNow-Schlüssel nach indexnow.org (8–128 Zeichen: a-z, A-Z, 0-9, Bindestrich). */ 86 function anni_ki_indexnow_gueltig($k) { 87 return is_string($k) && preg_match('/^[A-Za-z0-9\-]{8,128}$/', $k) === 1; 75 88 } 76 89 … … 172 185 return array(function () use ($opt) { return get_option($opt); }, 173 186 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); }); 174 199 case 'ki_inhalt': 175 200 $pid = (int) $doc_id; … … 181 206 }); 182 207 } 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; 184 211 } 185 212 … … 203 230 204 231 function 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')))); 206 235 } 207 236 … … 250 279 $t = anni_ledger_tabelle(); 251 280 $a = anni_ki_arten(); 281 $platz = implode(', ', array_fill(0, count($a), '%s')); // 1.7.0: so viele Platzhalter wie Arten (vorher fest 6) 252 282 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- eigene Verlaufstabelle, Name aus $wpdb->prefix 253 283 $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(); 257 287 $out = array(); 258 288 foreach ($zeilen as $z) { … … 856 886 } 857 887 } 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'] !== '') { 859 895 $vorhanden = $vorhanden ?: anni_ki_html_schema($html); 860 896 $faq = $vorhanden['faq'] ? null : anni_ki_faq_schema($kontext['inhalt']); … … 875 911 if ($m !== 'GET' && $m !== 'HEAD') { return false; } 876 912 $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()); 878 914 } 879 915 … … 882 918 $startseite = is_front_page(); 883 919 $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) { 885 923 foreach (headers_list() as $kopf) { 886 924 if (stripos($kopf, 'content-type:') === 0 && stripos($kopf, 'text/html') === false) { return $html; } 887 925 } 888 926 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)); 890 928 }); 891 929 }, PHP_INT_MAX - 1); 892 930 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) ── */ 894 932 895 933 /* … … 966 1004 }, 0); 967 1005 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. */ 1011 function 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 1019 add_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 968 1030 /* ── Einstellungsseite: Zustimmung ───────────────────────────────────────────────────────── */ 969 1031 … … 983 1045 echo '<p><label><input type="checkbox" name="' . esc_attr(ANNI_KI_ERLAUBT) . '" value="1" ' . checked(anni_ki_erlaubt(), true, false) . '> ' 984 1046 . '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. ' 987 1050 . 'Ausgeschaltet gibt das Plugin nichts davon aus und ändert nichts mehr.</p>'; 988 1051 submit_button(); … … 1016 1079 ), 1017 1080 '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(), 1018 1085 )); 1019 1086 } … … 1050 1117 } 1051 1118 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 } 1052 1125 $auftrag = anni_rest_auftrag($req); 1053 1126 $antwort = array('ok' => true, 'change_id' => 0, 'robots_datei' => null); -
anni-marketing/trunk/includes/ledger.php
r3745272 r3780762 152 152 */ 153 153 function 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')"; 155 156 } 156 157 … … 259 260 if ($nur_aktion !== '' && (string) $zeile['aktion'] !== (string) $nur_aktion) { 260 261 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); 261 266 } 262 267 // Bild-Optimierung (includes/media.php): Rücknahme = Original des Bilds wiederherstellen. -
anni-marketing/trunk/includes/rest-patch.php
r3745272 r3780762 43 43 $out = array(); 44 44 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; } 45 47 if (function_exists('anni_kern_ist_aktion') && anni_kern_ist_aktion($z['aktion'])) { 46 48 $out[] = anni_ledger_fuer_ausgabe($z, (int) $z['ruecknahme_von'] === 0 && anni_kern_ruecknahme_pruefen($z) === true); … … 91 93 // «nur»: optional die Art der Änderung, die zurückgenommen werden darf (z. B. «kit»). 92 94 $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 = '-'; } 94 97 return anni_ledger_zuruecknehmen((string) $req['id'], anni_rest_auftrag($req), $bestaetigt, $nur); 95 98 }, -
anni-marketing/trunk/includes/structure.php
r3745272 r3780762 397 397 } 398 398 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)); 400 401 } 401 402 if ($op === 'hide_on') { -
anni-marketing/trunk/readme.txt
r3757258 r3780762 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1. 4.37 Stable tag: 1.10.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 15 15 Anni 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: 16 16 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). 18 18 * 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. 19 21 * 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. 20 22 … … 28 30 29 31 == Frequently Asked Questions == 32 33 = How is the plugin updated? = 34 Only through the normal WordPress update system, from WordPress.org. Anni can start an update that WordPress already 35 offers (like clicking "Update now"); your settings and the change history stay as they are. Automatic updates are only 36 enabled if you choose so, either on the Plugins screen or by agreeing in Anni. 30 37 31 38 = Does this plugin track my visitors? = … … 63 70 * `POST /wp-json/anni/v1/ga4` — stores your Google Analytics 4 measurement ID (requires `manage_options`). 64 71 * `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`). 65 76 66 77 Data involved: your GA4 measurement ID and your Search Console verification token. No visitor data, … … 76 87 77 88 == 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. 78 190 79 191 = 1.4.3 = -
anni-marketing/trunk/uninstall.php
r3757258 r3780762 16 16 } 17 17 18 $anni_optionen = array('anni_ledger_schema', 'anni_struktur_nonces', 'anni_marketing_hinweis', 'anni_ga4_id', 'anni_g sc_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'); 19 19 delete_transient('anni_llms_cache'); 20 20 delete_transient('anni_llms_cache_v2'); 21 delete_transient('anni_akt_frisch'); 22 delete_transient('anni_akt_laeuft'); 21 23 foreach ($anni_optionen as $anni_o) { 22 24 delete_option($anni_o);
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)