Files
WP-LIT-MCP/docs/superpowers/notes/2026-10-19-etappe-4d-divi-recherche.md
T

185 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Etappe 4d – Divi 5.15.1: Theme-Optionen und Schriften (Code-Recherche)
Stand: 2026-10-19. Grundlage der Rulings W1–W14 (siehe `2026-09-28-etappe-4-entscheidungen-entwurf.md`, Abschnitt „Etappe 4d“). Die Befunde in Abschnitt 0 sind in den Formate-Nachtrag „Etappe 4d“ übernommen.
Quelle: installiertes Divi 5.15.1 (`style.css:4`), WordPress 7.1.3. Alle Pfade relativ zu `wp-content/themes/Divi`, sofern nicht anders angegeben. Nur aus Code abgeleitet; die Zeilen der Formate-Notiz §4 (5.2.1) sind durchweg um ca. +80 verschoben.
## 0. Überraschungen gegenüber Spec §8 (Kurzfassung)
1. **`FontsUtility::font_add()` ist in-process aufrufbar.** `wp_handle_upload()` nimmt `$overrides['action']` als Action (`wp-admin/includes/file.php:1110`); nur bei Action `'wp_handle_upload'` prüft `_wp_handle_upload` `is_uploaded_file()`/`move_uploaded_file()` (`file.php:936`, `:1019-1027`), sonst `is_readable()` + `copy()`/`unlink()`. Divi übergibt selbst `['action' => 'wp_handle_upload_rest']` (`CustomFontController.php:68-75`) und installiert so **serverseitige Temp-Dateien**: `AiAgentImageUtility::install_to_custom_fonts()` (`includes/builder-5/server/VisualBuilder/AiAgent/AiAgentImageUtility.php:860-1009`, `wp_tempnam` → `copy` → `font_add([...'tmp_name'=>$temp...], …, ['action'=>'wp_handle_upload_rest'])`). → Kein eigener Sideload und kein Nachbau des `et_uploaded_fonts`-Eintrags nötig; Spec §8.2 („nur echte Browser-Uploads“, „Nachbau mit Fingerabdruck“) ist überholt. Mirror höchstens für die *Aufrufkonvention* (`install_to_custom_fonts`).
2. **Divi-Rolle für Schriften ist `custom_fonts_management`**, nicht `use_visual_builder` (`CustomFontController.php:141-152`, `:214-225`; Rollen-Editor `includes/builder/functions.php:9595`). Plus `upload_files`. Spec §10 nachziehen.
3. **Standard-Formate nur `ttf`, `otf`:** `et_pb_get_supported_font_formats()` (`includes/builder/core.php:5090`, Filter `et_pb_supported_font_formats`). Der REST-Store nimmt nur diese Felder an; `font_add` selbst akzeptiert alles aus `mime_types_font()` (otf, ttf, woff, woff2, eot), das Frontend (`et_builder_enqueue_user_fonts`, `includes/builder/functions.php:3813ff`) gibt alle fünf aus. woff/woff2 sind also technisch möglich, aber über Divis UI nicht vorgesehen → Entscheidung nötig.
4. **„Standard-Schrift“ wird im Customizer als `false` gespeichert, nicht als `'none'`.** `et_sanitize_font_choices('none')` → `false` (`'none'` steht nicht in `et_builder_get_fonts()`), WP speichert `false` (nur `null`/`WP_Error` werden verworfen, `wp-includes/class-wp-customize-manager.php:1848-1851`). Leser: Divi-4-CSS/Google-URL `et_pb_get_specific_default_font()` behandelt `false`/`''`/`'none'` gleich (`functions.php:1287-1290`, lose `in_array`); Divi 5 `Fonts::enqueue()` und `GlobalData::get_customizer_fonts()` nehmen bei *fehlendem* Schlüssel `'Open Sans'`, bei `false` ebenfalls `'Open Sans'` (`FrontEnd/Module/Fonts.php:100-129`), bei `'none'` aber `self::add('none')` (`:138-144`). → „Standard“ = Schlüssel entfernen (`et_delete_option`), nie `'none'` schreiben.
5. **Divis Sanitizer prüfen keine Bereiche.** `absint`, `intval`, `floatval` klemmen nicht auf min/max der Controls; `et_sanitize_*_choices` liefern bei Unbekanntem `false` (kein Fehler). Plugin muss Bereiche aus den Control-`input_attrs` selbst prüfen und `false` als Ablehnung werten.
6. **`body_header_style` ist im Control mehrfach wählbar (`italic|uppercase`, Checkbox-Liste mit `|`, `functions.php:5457-5481`), der Sanitizer `et_divi_global_font_style` akzeptiert aber nur genau einen Schlüssel** → jede Kombination und auch `''` (alles abgewählt) wird zu `false`. Divi-Bug; Tool sollte nur einen Wert (oder „unset“) zulassen.
7. **`et_builder_get_google_fonts()` kann einen HTTP-Request auslösen** (`et_builder_google_fonts_sync()`, `includes/builder/core.php:4868-4905`: einmal je 24 h per Transient `fonts_cache_status`, nur wenn Google-API-Key gesetzt). Sonst Option `et_google_fonts_cache` bzw. 1,3-MB-JSON (`core/json-data/google-fonts.json`, 1945 Familien), statisch gecacht. `et_sanitize_font_choices` ruft das bei jedem Aufruf (ksort über ~1950 Einträge) → Liste je Request einmal holen.
8. **Namens-Eindeutigkeit in `font_add` prüft nur Google-Fonts und `et_uploaded_fonts`, nicht Web-Safe-Fonts** (`FontsUtility.php:125-131`) – „Arial“ wäre hochladbar. Plugin-Regel strenger halten.
9. **`font_add` meldet Teilerfolg als Erfolg:** schlägt eine von mehreren Dateien fehl, wird die Schrift mit den übrigen gespeichert, die Fehler gehen verloren (`FontsUtility.php:157-190`). Plugin: vorab jede Datei mit `HooksRegistration::check_filetype_and_ext_font()` prüfen (wie Divis AI-Pfad) und Ergebnis `font_file`-Schlüssel gegen Eingabe vergleichen.
10. **`ET_Core_PageResource::remove_static_resources` hat jetzt 7 Parameter** (`+ $skip_external_cache`, `core/components/PageResource.php:1398`); Signatur-Fingerabdruck prüfen.
11. **`GlobalData::get_customizer_fonts()` cached per `static` ohne Reset** (`GlobalData.php:446-451`) → Zurücklesen nach dem Schreiben im selben Request über `get_option('et_divi')`/`et_get_option`, nicht darüber.
12. **`et_update_option` schreibt das ganze `et_divi`-Array aus dem Request-Global `$et_theme_options`** (`epanel/custom_functions.php:297-310`); wurde das Global früher im Request geladen und die Option inzwischen von einem anderen Prozess geändert, werden dessen Änderungen überschrieben (Lost Update). Für `design_rev`-Vergleich und Snapshot `get_option('et_divi')` lesen; Restrisiko dokumentieren.
## A. Theme-Optionen
Allgemein für alle 20 Settings (Registrierung in `et_divi_customizer_theme_settings()`, `functions.php:1371ff`, Hook `customize_register` → `et_divi_customize_register` `functions.php:1306`/`:1363`):
- `type => 'option'`, ID `et_divi[<key>]` → Speicher `get_option('et_divi')[<key>]` (Divi: `$shortname = 'divi'`, `$et_store_options_in_one_row = true`, `functions.php:16-17`).
- `capability => 'edit_theme_options'`, `transport => 'postMessage'`.
- Sanitizer sind **globale Funktionen**, immer geladen: `includes/functions/sanitization.php` und `choices.php` werden in `et_setup_theme()` (`after_setup_theme`, `functions.php:50-52`, `:150`) ohne Bedingung eingebunden; Builder-Listen in `includes/builder/core.php` (immer geladen). WP ruft sie als Filter `customize_sanitize_{id}` mit `($value, $setting)` (`class-wp-customize-setting.php:581`); alle Divi-Sanitizer nehmen nur `$value`, also direkt mit 1 Argument aufrufbar. Kein Customizer-Kontext nötig.
- Rückgabe `false` bei ungültiger Auswahl (`et_sanitize_key_based_option($choosen, $options, $default = false)`, `sanitization.php:45-52`; `isset($options[$choosen])`, also **case-sensitiv**, numerische String-Schlüssel ≙ int).
- Gespeicherte Typen: `absint` → int, `et_sanitize_float_number` → float, `et_sanitize_int_number` → int, Auswahl-Sanitizer → der übergebene Wert (String) oder `false`.
| Schlüssel | Setting (file:line) | Standard (Setting) | sanitize_callback (Definition) | Control / Choices / min–max–step | Zusammenspiel / Leser |
|---|---|---|---|---|---|
| `heading_font` | `functions.php:2563-2572`, Control `:2574-2586` | `'none'` | `et_sanitize_font_choices($choosen)` `sanitization.php:59-66` (Google+Websafe via `et_builder_get_fonts()` + `et_builder_get_custom_fonts()`) | `ET_Divi_Select_Option`, `type select`, Choices `$font_choices` (`functions.php:1407-1440`: `'none'` + alle Fonts, im UI nach Locale-Zeichensatz gefiltert, Sanitizer filtert **nicht** nach Locale) | `'none'` → `false` gespeichert (§0.4). Builder/D5-Sicht `'Open Sans'` (`GlobalData::$customizer_fonts` `GlobalData.php:108-119`, `Fonts.php:100`). Speist `--et_global_heading_font` (`Fonts.php:148-175`), D4-Font-URL `functions.php:432`, `:7216`, `et_builder_get_font_family('--et_global_heading_font')` `includes/builder/core.php:4683-4686`. Alias `Source Sans Pro`→`Source Sans 3` nur im D5-Enqueue (`Fonts.php:113-127`). |
| `body_font` | `functions.php:2636-2645`, Control `:2647-2659` | `'none'` | `et_sanitize_font_choices` | wie oben | wie oben, `--et_global_body_font`; zusätzlich `functions.php:334`, `:474`, `includes/builder/functions.php:4254`, `Fonts.php:134-136` (`'none'`/`''` = „Theme lädt Open Sans selbst“). |
| `primary_nav_font` | `functions.php:3477-3486`, Control `:3488-3500` | `'none'` | `et_sanitize_font_choices` | Select, `$font_choices` (Section `et_divi_header_primary`) | nur D4-Header/Google-URL (`functions.php:435`); kein D5-Global. |
| `secondary_nav_font` | `functions.php:3601-3610`, Control `:3612-3624` | `'none'` | `et_sanitize_font_choices` | Select, `$font_choices` (Section `et_divi_header_secondary`) | `functions.php:436`; kein D5-Global. |
| `heading_font_weight` | `functions.php:2588-2597`, Control `:2599-2611` | `'500'` | `et_sanitize_global_font_weight($choosen)` `sanitization.php:345-347` | `ET_Divi_Custom_Select_Option`, Choices `et_builder_get_font_weight_list()` (`includes/builder/core.php:4792-4807`: `'100'…'900'`, Filter `et_builder_all_font_weights`) | `--et_global_heading_font_weight` (`Fonts.php:151`, leer → `'500'`), D4-CSS `functions.php:5827`, `:6943ff` (nur wenn ≠ 500). |
| `body_font_weight` | `functions.php:2661-2670`, Control `:2672-2684` | `'500'` | `et_sanitize_global_font_weight` | wie oben | `--et_global_body_font_weight`, `functions.php:5828`, `:5992ff`. |
| `body_font_size` | `functions.php:2287-2296`, Control `:2298-2313` | `'14'` | `absint` (WP) | `ET_Divi_Range_Option`, `range` 10–32, step 1 | `--et_global_body_font_size` (`Fonts.php:104-107`, `''` → 14), D4 `functions.php:5819`, VB-Settings `SettingsDataCallbacks.php:2379`. |
| `tablet_body_font_size` | `functions.php:2371-2380`, Control `:2382-2397` | `et_get_option('body_font_size','14')` (dynamisch) | `absint` | Range 10–32, step 1 (Section `et_divi_mobile_tablet`) | Leser nehmen fest `'14'` (`functions.php:5841`, `Fonts.php:108`), CSS nur wenn `≠14 && ≠ body` (`functions.php:7019`) → unset ≙ erbt Desktop. |
| `phone_body_font_size` | `functions.php:2343-2352`, Control `:2354-2369` | `et_get_option('tablet_body_font_size','14')` | `absint` | Range 10–32, step 1 (`et_divi_mobile_phone`) | Leser: Standard = Tablet-Wert (`functions.php:5846`, `Fonts.php:109`); CSS nur wenn `≠14 && ≠ tablet` (`:7112`). |
| `body_header_size` | `functions.php:2399-2408`, Control `:2410-2425` | `'30'` | `absint` | Range 22–72, step 1 | `functions.php:5821`, VB `SettingsDataCallbacks.php:2391`. |
| `tablet_header_font_size` | `functions.php:2535-2544`, Control `:2546-2561` | `et_get_option('body_header_size','30')` | `absint` | Range 22–72, step 1 | Leser fest `'30'` (`functions.php:5840`), CSS nur wenn `≠30 && ≠ body_header_size` (`:7039`). |
| `phone_header_font_size` | `functions.php:2507-2516`, Control `:2518-2533` | `et_get_option('tablet_header_font_size','30')` | `absint` | Range 22–72, step 1 | Standard = Tablet (`functions.php:5845`), CSS `:7132`. |
| `body_font_height` | `functions.php:2315-2324`, Control `:2326-2341` | `'1.7'` | `et_sanitize_float_number($number)` `sanitization.php:7-9` (`floatval`) | Range 0.8–3, step 0.1 | `--et_global_body_font_height` (`Fonts.php:105-107`, Einheit `em`), D4 `functions.php:5820`, VB `:2378`. |
| `body_header_height` | `functions.php:2455-2464`, Control `:2466-2481` | `'1'` | `et_sanitize_float_number` | Range 0.8–3, step 0.1 | D4 `functions.php:5824`, `:6604`. |
| `body_header_spacing` | `functions.php:2427-2436`, Control `:2438-2453` | `'0'` | `et_sanitize_int_number($number)` `sanitization.php:16-18` (`intval`) | Range −2–10, step 1 | D4 `functions.php:5823`, `:6604`. Negativ erlaubt. |
| `body_header_style` | `functions.php:2483-2492`, Control `:2494-2505` | `''` | `et_divi_global_font_style($choosen)` `sanitization.php:358-360` | `ET_Divi_Font_Style_Option` (`functions.php:5457`, Checkboxen, Wert `a|b`), Choices `et_divi_global_font_style_choices()` (`includes/functions/choices.php:23-33`: `italic`, `uppercase`, `underline`; Filter `et_divi_global_font_style_choices`) – **kein `bold`** (das gibt es nur bei `et_divi_font_style_choices`/`et_sanitize_font_style`) | Sanitizer nimmt nur genau einen Wert, `''` und Kombinationen → `false` (§0.6). Leser `et_get_option('body_header_style','','',true)` + `et_pb_print_font_style()` (`functions.php:5822`, `:6616`). |
| `content_width` | `functions.php:1903-1912`, Control `:1914-1929` | `'1080'` | `absint` | Range 960–1920, step 1 (`et_divi_general_layout`) | D4 `functions.php:5833`, `:6133`; VB `SettingsDataCallbacks.php:2387`. |
| `gutter_width` | `functions.php:1931-1940`, Control `:1942-1957` | `'3'` | `absint` | Range 1–4, step 1 | Standard für Section/Row-`gutter_width` (D4 `includes/builder/main-structure-elements.php:239`, `:1953`), Body-Klasse `functions.php:8581`. In D5-Servercode kein Leser gefunden. |
| `section_padding` | `functions.php:2006-2015`, Control `:2017-2032` | `'4'` | `absint` | Range 0–10, step 1 | D4 `functions.php:5837`. |
| `row_padding` | `functions.php:2090-2099`, Control `:2101-2116` | `'2'` | `absint` | Range 0–10, step 1 | D4 `functions.php:5838`. |
Abhängige Standards: `tablet_*` / `phone_*` werden im Customizer beim Registrieren aus dem aktuellen Größeren berechnet; die CSS-Leser nutzen feste 14/30 und „gleich dem Größeren = keine Regel“. Für das Tool: Standard von `tablet_body_font_size` = effektiver `body_font_size`, `phone_*` = effektiver `tablet_*`; „unset“ heißt „erbt“.
### A.1 Builder-Globals (`--et_global_*`)
- `GlobalData::$customizer_fonts` (`includes/builder-5/server/Packages/GlobalData/GlobalData.php:108-119`): `--et_global_heading_font` → `heading_font`, `--et_global_body_font` → `body_font`, beide Standard `'Open Sans'`. `get_customizer_fonts()` (`:446-470`) liefert `{id,label,value,status:'active',order}`, `static` gecacht.
- Frontend-Variablen (`FrontEnd/Module/Fonts.php:99-191`, `Fonts::enqueue()`): `--et_global_heading_font`, `--et_global_body_font`, `--et_global_heading_font_weight`, `--et_global_body_font_weight`, `--et_global_body_font_size` (+ responsive aus `tablet_/phone_body_font_size`), `--et_global_body_font_height` – also speisen **heading_font, body_font, heading_font_weight, body_font_weight, body_font_size, tablet_body_font_size, phone_body_font_size, body_font_height** Divi-5-Globals; inline ausgegeben (`<style class="et-vb-global-data et-vb-global-fonts">`), nicht im Static-CSS gecacht.
- REST `POST /global-data/global-fonts` (`includes/builder-5/server/VisualBuilder/REST/RESTRegistration.php:1677`) → `GlobalDataController::update_global_fonts()` (`GlobalDataController.php:166-191`): `et_update_option('heading_font', …)`, `et_update_option('body_font', …)` (Args `required`, nur `sanitize_text_field`, `:215-227`, **keine Font-Prüfung**), dann `ET_Core_PageResource::remove_static_resources('all','all',true)`. Rechte nur `edit_posts` (`:243-249`) + Routen-Nonce. Weitere Schreiber derselben Schlüssel: AJAX `et_update_customizer_fonts` (`includes/builder/functions.php:13583-13603`, `edit_theme_options`), Import `PortabilityPost.php:4130-4136`.
### A.2 Was ein Customizer-Speichern zusätzlich tut
- WP schreibt das Setting direkt per `update_option('et_divi', …)` (multidimensional), **nicht** über `et_update_option`.
- `customize_save_after`-Hooks in Divi (vollständig per grep): `ET_Core_PageResource::customize_save_after_cb` → `remove_static_resources('all','all')` (`core/components/PageResource.php:953`, `:1252-1254`) und `et_pb_force_regenerate_templates` (`includes/builder/core.php:3811-3817`, `:3825`: `et_update_option('et_pb_clear_templates_cache', true)` + `et_fb_delete_builder_assets()` `core.php:6592ff`, löscht VB-Helper-JS/`.data`/Bild-Caches).
- Keine Hooks auf `update_option_et_divi` / `pre_update_option_et_divi` in Divi.
- `remove_static_resources` (`PageResource.php:1398-1417`): wirkungslos bei `DOING_AUTOSAVE`, ohne `wp_doing_cron()` bzw. `et_core_security_check_passed('edit_posts')`, oder ohne Schreibrecht im Dateisystem; `$force` löscht nur zusätzlich die Option `et_core_page_resource_remove_all` (`:1596-1598`); `$skip_external_cache=false` ruft `et_core_clear_wp_cache()` (Fremd-Page-Caches). Die Customizer-CSS liegt im Static-Resource `et-divi-customizer-global-*` (`functions.php:5790`, `:7331`) und wird damit invalidiert.
- `et_update_option` (`epanel/custom_functions.php:276-315`): lädt `$et_theme_options` nur, wenn das Global fehlt oder `is_customize_preview()`; setzt Schlüssel, `update_option('et_divi', $et_theme_options)`; aktualisiert damit auch das Global (spätere `et_get_option` im Request sehen den neuen Wert). Keine weiteren Seiteneffekte.
### A.3 Minimale korrekte In-Process-Schreibfolge (≙ Customizer-Speichern)
1. Rechte: `current_user_can('edit_theme_options')` **und** `et_pb_is_allowed('theme_customizer')` (Customizer: `functions.php:1357`, `wp_die`; Rollen-Editor `includes/builder/functions.php:9523`). `et_pb_is_allowed` (`includes/builder/core.php:1136-1170`) ist ohne Eintrag im Rollen-Editor `true` (nicht „dangerous“); nur eine Fähigkeit je Aufruf übergeben (bei Arrays entscheidet die erste gespeicherte).
2. Je Schlüssel: Divi-Sanitizer aufrufen; `false` → Validierungsfehler (statt wie Divi `false` zu speichern); Bereich aus Tabelle selbst prüfen; Ergebnis vergleichen (z. B. `absint('15.7')` = 15 → als Änderung melden oder ablehnen).
3. Schreiben: `et_update_option($key, $sanitized)` je Schlüssel; „unset“: `et_delete_option($key)`.
4. Caches: `ET_Core_PageResource::remove_static_resources('all', 'all')` (Customizer) – oder `('all','all',true)` wie `global-fonts`; plus `et_pb_force_regenerate_templates()`. Benutzer muss angemeldet sein (`edit_posts`).
5. Zurücklesen: `get_option('et_divi')` (WP-Cache wird von `update_option` aktualisiert), nicht `GlobalData::get_customizer_fonts()`.
## B. Schriften
### B.1 Listen
| Funktion | Ort | Signatur | Rückgabe / Kosten |
|---|---|---|---|
| `et_builder_get_google_fonts()` | `includes/builder/core.php:4911-4929` | `()` | `[] ` wenn Google Fonts aus (`et_core_use_google_fonts()`, `core/functions.php:223-228`, Option `et_google_api_settings['use_google_fonts']`, Standard `'on'`). Sonst ruft **`et_builder_google_fonts_sync()`** (`:4868-4905`, ggf. `wp_remote_get` an Google, 1×/24 h, nur mit API-Key) und liefert Option `et_google_fonts_cache` (Array oder JSON-String, dann jedes Mal geparst) bzw. `et_core_get_saved_google_fonts()` (`core/functions.php:1970-1985`, 1,3 MB JSON, 1945 Familien, `static`). Filter `et_builder_google_fonts`. Form: `Family => {styles:'100,100italic,…', character_set:'latin,…', type:'sans-serif|serif|display|handwriting|monospace', is_variable:bool, axes:[{tag,min,max,default?}]}` (`et_core_parse_google_fonts_json`, `core/functions.php:1898-1958`, `ksort`). |
| `et_builder_get_websafe_fonts()` | `core.php:4783-4785` | `()` | → `et_core_get_websafe_fonts()` (`core/functions.php:1993-2030`): `Arial`, `Georgia`, `Times New Roman`, `Trebuchet`, `Verdana` mit `styles`, `character_set`, `type`, ggf. `add_ms_version`; Filter `et_websafe_fonts`. |
| `et_builder_get_fonts( $settings = array() )` | `core.php:4730-4751` | 0–1 Arg (`prepend_standard_fonts`) | Websafe + Google, `ksort`; bei abgeschalteten Google Fonts nur Websafe. **Ohne** hochgeladene Schriften. |
| `et_builder_get_custom_fonts()` | `core.php:4822-4831` | `()` | `get_option('et_uploaded_fonts', [])`, nicht-Array → `[]`, Filter `et_builder_custom_fonts`. |
| `et_builder_get_font_family( $font_name, $use_important = false )` | `core.php:4683-4723` | 1–2 | CSS-String `font-family: '…', <stack>;` – kein Listen-Lieferant. Löst `--et_global_*_font` auf. |
| `et_builder_old_fonts_mapping()` | `core.php:4839ff` | `()` | Alt-Namen (`Raleway Light`, `Open Sans Light` …) → `parent_font` + Gewicht. |
| `et_builder_get_font_weight_list()` | `core.php:4792-4807` | `()` | `'100'…'900'` → Label. |
Gesamtliste wie der Customizer-Sanitizer: `array_merge(et_builder_get_custom_fonts(), et_builder_get_fonts())` (`sanitization.php:59-66`). Gültiger Wert = **exakter Familienname als Schlüssel** (case-sensitiv), keine `:`/`|`-Qualifizierer; `'none'` ist kein Schlüssel. Gewichte/Stile sind eigene Settings.
### B.2 Hochgeladene Schriften
**Option `et_uploaded_fonts`** (autoload Standard), geschrieben von `FontsUtility::font_add()`:
```
'<Name>' => [
'font_file' => [ '<ext>' => esc_url('<absoluter Dateipfad>') , … ], // Pfad durch esc_url
'font_url' => [ '<ext>' => esc_url('<URL>'), … ],
'styles' => '100,200,…,900' | '<eingegebene Liste>', // nur wenn $font_settings nicht leer
'type' => '<generic_family>' | 'serif', // nur wenn $font_settings nicht leer
]
```
(Divi-4-Altformat von `et_pb_add_font`, `includes/builder/core.php:5183-5270`, ist gleich; `font_file`/`font_url` können dort auch Strings sein – `et_builder_enqueue_user_fonts` behandelt beides, `functions.php:3818`, `:3869`.)
**`FontsUtility::font_add( array $font_files, string $font_name, array $font_settings, array $overrides = [] )`** (`includes/builder-5/server/VisualBuilder/Fonts/FontsUtility.php:102-213`, Namespace `ET\Builder\VisualBuilder\Fonts`):
- `$font_files`: `['<ext>' => <$_FILES-Element {name,type,tmp_name,error,size}>]` (nicht Pfade, trotz Docblock). Leer → `WP_Error file_empty`.
- Name: `preg_replace('/[^A-Za-z0-9\s\_-]/', '', $name)` (stilles Entfernen, kein Fehler) → leer: `font_name_empty`; > `FONT_NAME_MAX_LENGTH` (100, `:39`) Zeichen nach `StringUtility::strlen_utf8`: `font_name_too_long`; bereits in `get_option('et_uploaded_fonts')` (ungefiltert) oder `et_builder_get_google_fonts()` (exakt, case-sensitiv): `font_name_exists`. Websafe nicht geprüft. Der tatsächlich gespeicherte Name steht in `uploaded_font` der Rückgabe.
- Ablage: `add_filter('upload_dir', [HooksRegistration::class, 'upload_dir_font'])` → `<uploads basedir>/et-fonts`, URL `<baseurl>/et-fonts`, `subdir '/et-fonts'` (keine Jahr/Monat-Ordner; `HooksRegistration.php:318-332`); `add_filter('wp_check_filetype_and_ext', [HooksRegistration::class, 'check_filetype_and_ext_font'], 999, 3)`; beide nach der Schleife entfernt (`:174-178`).
- Je Datei `wp_handle_upload($file, wp_parse_args(['test_size'=>true,'test_type'=>true,'test_form'=>false], $overrides))` → die drei Tests sind fest, `action` (und andere Overrides wie `unique_filename_callback`, `mimes`) kommen durch. Dateiname `wp_unique_filename` → keine Überschreibung.
- Typprüfung `check_filetype_and_ext_font` (`HooksRegistration.php:77-127`): mit `fileinfo`: Endung (lowercase) muss in `mime_types_font()` stehen und `finfo` (Magic) einen der MIME-Typen liefern: otf `font/otf|application/x-font-opentype|application/x-font-ttf|application/vnd.ms-opentype`, ttf `font/ttf|font/sfnt|application/font-sfnt|application/x-font-ttf`, woff `font/woff|application/font-woff`, woff2 `font/woff2|application/font-woff2`, eot `application/vnd.ms-fontobject` (`FontsUtility.php:290-316`); `proper_filename = sanitize_file_name()`. Ohne `fileinfo`: Fallback auf WPs Ergebnis. **Achtung:** `_wp_handle_upload` lässt bei `current_user_can('unfiltered_upload')` auch fehlgeschlagene Typprüfung durch (`wp-admin/includes/file.php:971`) → eigene Magic-Byte-Prüfung vorab ist Pflicht (Spec hat sie schon).
- Gewichte: `font_weights` fehlt oder `'all'` → `'100,200,300,400,500,600,700,800,900'`, sonst **ungeprüft übernommen**; `generic_family` ungeprüft, Standard `'serif'`. Kein Größenlimit außer `test_size` (>0).
- Ergebnis: mind. eine Datei ok → `update_option('et_uploaded_fonts', …)`, `do_action('divi_visual_builder_fonts_custom_font_added')` (→ `et_fb_delete_builder_assets`, `HooksRegistration.php:345`), Rückgabe `['uploaded_font' => $name, 'updated_fonts' => $all]`. Keine Datei ok → `WP_Error` (gesammelte Upload-Fehler, ggf. `fileinfo_extension_not_loaded`, sonst `upload_error_unknown`).
- **Keine** Rechte-/Nonce-Prüfung in `font_add`/`font_remove`; die liegt im REST-Controller: `CustomFontController::store_permission()` / `destroy_permission()` (`includes/builder-5/server/VisualBuilder/REST/CustomFont/CustomFontController.php:141-152`, `:214-225`): `upload_files` + `et_pb_is_allowed('custom_fonts_management')`; Routen `POST /custom-font/add`, `/custom-font/remove` (`RESTRegistration.php:1168-1183`) mit Divi-Routen-Nonce. Store nimmt nur `et_pb_font_file_<fmt>` für `et_pb_get_supported_font_formats()` (Standard ttf, otf).
**In-process mit Nicht-HTTP-Dateien:** möglich. `wp_handle_upload()` setzt `$action = $overrides['action'] ?? 'wp_handle_upload'` (`wp-admin/includes/file.php:1110`); `_wp_handle_upload` prüft `is_uploaded_file()` und nutzt `move_uploaded_file()` nur bei `'wp_handle_upload' === $action`, sonst `is_readable()`, `filesize()`, `copy()` + `unlink(tmp)`; zusätzlich laufen `{$action}_prefilter`/`{$action}_overrides`-Filter (also `wp_handle_upload_rest_prefilter`, nicht `wp_handle_upload_prefilter`). Divi macht genau das in `AiAgentImageUtility::install_to_custom_fonts()` (`AiAgentImageUtility.php:860-1009`): `check_filetype_and_ext_font()` vorab, `wp_tempnam($filename)`, `copy`, `font_add([$ext => ['name'=>$filename,'type'=>$mime,'tmp_name'=>$temp,'error'=>0,'size'=>filesize($temp)]], $name, ['font_weights'=>'all','generic_family'=>'sans-serif'], ['action'=>'wp_handle_upload_rest'])`, danach Temp löschen. → Plugin: `wp_safe_remote_get` → Temp (`wp_tempnam`, Name mit korrekter Endung) → Magic-Bytes + `check_filetype_and_ext_font` → `font_add(..., ['action' => 'wp_handle_upload_rest'])`. **Kein Sideload-Nachbau, kein Nachbau des Options-Formats.** `wp_handle_sideload` wäre gleichwertig, aber unnötig.
**`FontsUtility::font_remove( string $font_name )`** (`:242-276`): `''` → `file_name_empty`; fehlt Schlüssel oder `font_file` → `font_not_exist`; jede Datei `et_pb_safe_unlink_font_file()` (`includes/builder/core.php:5336-5381`: nur echte Pfade unter `uploads/et-fonts`, Font-Endung), `unset`, `update_option`, `do_action('divi_visual_builder_fonts_custom_font_removed')`. Für Rollback nach fehlgeschlagenem Zurücklesen nutzbar.
**Zu rufende Divi-Symbole für den Upload:** `FontsUtility::font_add` (4), `HooksRegistration::check_filetype_and_ext_font` (3, Vorprüfung je Datei), `FontsUtility::mime_types_font` (0, erlaubte Endungen), `FontsUtility::FONT_NAME_MAX_LENGTH` (Konstante), `et_pb_get_supported_font_formats` (0, falls Divi-UI-Formate gelten sollen), `et_builder_get_google_fonts` (0) / `et_builder_get_websafe_fonts` (0) / `et_builder_get_custom_fonts` (0) für Namensprüfung, `et_pb_is_allowed` (1, `custom_fonts_management`), optional `FontsUtility::font_remove` (1) für Rollback. `HooksRegistration::upload_dir_font` muss nicht gerufen werden (setzt `font_add` selbst).
**Mirror-Kandidaten (statt Speicher-Nachbau):**
- `new Mirror('font-install', 'includes/builder-5/server/VisualBuilder/AiAgent/AiAgentImageUtility.php', 'install_to_custom_fonts', 'AiAgentImageUtility', 'Aufrufkonvention von font_add mit Temp-Datei (check_filetype_and_ext_font, wp_tempnam, action wp_handle_upload_rest)')` – fängt, wenn Divi den In-Process-Weg ändert.
- Optional `new Mirror('font-add', 'includes/builder-5/server/VisualBuilder/Fonts/FontsUtility.php', 'font_add', 'FontsUtility', 'Namensregeln und Eintragsformat von et_uploaded_fonts (Plugin-Vorprüfung, Rückgabe-Vergleich)')` – nur nötig, wenn das Plugin Namensregeln (Regex, 100 Zeichen, Kollision) vorab nachbildet, um im Probelauf ohne Upload zu melden.
- `new Mirror('customizer-font-sanitize', 'includes/functions/sanitization.php', 'et_sanitize_font_choices', null, …)` nur, wenn die Font-Validierung nicht über den Sanitizer, sondern über die gemergte Liste nachgebaut wird.
## C. Snapshot / Restore „theme_options“
- Inhalt: genau die 20 Whitelist-Schlüssel aus `get_option('et_divi')`, je Schlüssel **vorhanden + Rohwert (mit Typ: int/float/string/false)** oder **nicht vorhanden**. Darstellung z. B. `{"set": {key: value, …}, "unset": [key, …]}` oder Wert-Objekt `{present: bool, value}` – wichtig ist die Unterscheidung „fehlt“ vs. `false` vs. `''`, weil `et_get_option` bei vorhandenem Schlüssel (auch `false`) den Wert, sonst den Standard liefert (`custom_functions.php:238`, `:246-248`; Bedingung `! empty($default_value)`). Farben (`accent_color` …) nicht hier (Snapshot `colors`).
- Restore: für „vorhanden“ `et_update_option($key, $raw)` (Rohwert ohne Sanitizer zurückschreiben – er stammt aus Divi), für „fehlt“ `et_delete_option($key)`; danach Caches wie A.3. `et_update_option` allein reicht nicht, weil es Schlüssel nicht entfernen kann.
- `et_delete_option( $option_name )` (`epanel/custom_functions.php:320-333`, 1 Arg, untypisiert): lädt das Global nur, wenn nicht gesetzt (ignoriert `is_customize_preview`), `unset`, `update_option('et_divi', …)`. Entfernt den Schlüssel also wirklich. Sonderfall: ist `et_divi` gar nicht vorhanden (`false`), schreibt es `false` zurück (unkritisch).
- „Unset“ im Tool-Input: eigener Wert (z. B. `null` = „auf Divi-Standard zurück“) → `et_delete_option`. Nicht `'none'`, nicht `false` schreiben.
- `get` liefert Divi-Standard je Feld aus der Tabelle (Settings-`default`; für `tablet_*`/`phone_*` den effektiven Wert des Größeren; für Schriften Customizer-Standard `none` = „Theme-Standard“, Builder-Anzeige `Open Sans`).
## D. Divi-Symbole für das Manifest
Arg-Anzahl = was das Plugin übergibt; „untyp.“ = Parameter ohne Typ-Deklaration (Signatur-Fingerabdruck weniger aussagekräftig).
**Optionen lesen**
- `et_get_option` – 2 (von 8), untyp. (`epanel/custom_functions.php:215`) – optional; für Rohsicht direkt `get_option('et_divi')` (Key `et_divi`).
- `ET\Builder\Packages\GlobalData\GlobalData::get_customizer_fonts` – 0, typisiert `(): array` (`GlobalData.php:446`) – nur Builder-Sicht, gecacht.
- Keys: `et_divi`, `et_uploaded_fonts`, `et_google_fonts_cache`, `et_google_api_settings`, Statische Eigenschaft `GlobalData::$customizer_fonts`.
**Standards / Choices**
- `et_builder_get_font_weight_list` – 0 (`includes/builder/core.php:4792`).
- `et_divi_global_font_style_choices` – 0 (`includes/functions/choices.php:23`).
- Standardwerte und min/max/step selbst sind nur in `et_divi_customizer_theme_settings()` (`functions.php:1371`) als Literale → nicht aufrufbar (braucht `WP_Customize_Manager`) → **Mirror** `new Mirror('customizer-typography', 'functions.php', 'et_divi_customizer_theme_settings', null, 'Standards, Sanitize-Callbacks und Bereiche der 20 Whitelist-Settings')` (große Funktion; Fingerabdruck schlägt bei jeder Customizer-Änderung an – akzeptabel oder engere Referenz wählen).
**Sanitize**
- `absint` (WP-Core, nicht ins Manifest).
- `et_sanitize_float_number` – 1, untyp. (`includes/functions/sanitization.php:7`).
- `et_sanitize_int_number` – 1, untyp. (`:16`).
- `et_sanitize_font_choices` – 1, untyp. (`:59`).
- `et_sanitize_global_font_weight` – 1, untyp. (`:345`).
- `et_divi_global_font_style` – 1, untyp. (`:358`).
- (indirekt `et_sanitize_key_based_option` – 3, untyp., `:45`.)
**Schreiben**
- `et_update_option` – 2 (von 6), untyp. (`epanel/custom_functions.php:276`).
- `et_delete_option` – 1, untyp. (`:320`).
**Rechte**
- `et_pb_is_allowed` – 1, untyp. (`includes/builder/core.php:1136`) mit `theme_customizer` bzw. `custom_fonts_management`.
**Caches**
- `ET_Core_PageResource::remove_static_resources` – 2 oder 3 (von 7), untyp. (`core/components/PageResource.php:1398`).
- `et_pb_force_regenerate_templates` – 0 (`includes/builder/core.php:3811`).
**Schriftlisten**
- `et_builder_get_fonts` – 0, untyp. (`core.php:4730`).
- `et_builder_get_google_fonts` – 0 (`core.php:4911`; Seiteneffekt HTTP, §0.7).
- `et_builder_get_websafe_fonts` – 0 (`core.php:4783`).
- `et_builder_get_custom_fonts` – 0 (`core.php:4822`).
- optional `et_core_use_google_fonts` – 0 (`core/functions.php:223`), `et_builder_old_fonts_mapping` – 0 (`core.php:4839`).
**Font-Upload**
- `ET\Builder\VisualBuilder\Fonts\FontsUtility::font_add` – 4, typisiert `(array, string, array, array = [])`, Rückgabe untyp. (`FontsUtility.php:102`).
- `ET\Builder\VisualBuilder\Fonts\FontsUtility::font_remove` – 1, `(string)`, Rückgabe untyp. (`:242`).
- `ET\Builder\VisualBuilder\Fonts\FontsUtility::mime_types_font` – 0, `(): array` (`:290`).
- Konstante `FontsUtility::FONT_NAME_MAX_LENGTH` (`:39`).
- `ET\Builder\VisualBuilder\Hooks\HooksRegistration::check_filetype_and_ext_font` – 3, `(array, string, string): array` (`HooksRegistration.php:77`).
- `et_pb_get_supported_font_formats` – 0 (`includes/builder/core.php:5090`).
- Hooks (nur Namen, Keys): `divi_visual_builder_fonts_custom_font_added`, `upload_dir`/`wp_check_filetype_and_ext` (von Divi gesetzt).