# Etappe 4 – Datenformate des Divi-Designsystems (Divi 5.2.1) Stand 2026-09-28. Grundlage: Quelltext von Divi 5.2.1 und echte Daten, die über Divis eigene Speicherwege angelegt wurden (Seed-Skript `tests/Integration/divi-seed-stage4-design.php`, Fixtures `tests/Fixtures/divi/stage4/`). Erfüllt Spec §14 („Datenformate Designsystem nur aus Code abgeleitet“) und §15 („Format der Presets/Variablen an echten Daten verifizieren“). Pfadangaben relativ zu `wp-content/themes/Divi/`. Abkürzung **B5** = `includes/builder-5/server/`. Minifizierte Builder-Dateien liegen in `includes/builder-5/visual-builder/build/`; dort steht statt einer Zeile ein Suchbegriff. ## Nachtrag Divi 5.15.1 (Etappe 4a, 2026-10-16) Geprüft gegen Divi 5.15.1 (installiert, freigegeben): - `GlobalPreset::save_data()` hat den zweiten Parameter `$skip_external_cache = false`; das Plugin übergibt ihn nicht. Der Parameter wird mit einem internen Flag (`$_skip_external_cache_nested`) verodert und an `ET_Core_PageResource::remove_static_resources()` weitergereicht. - `GlobalPresetController::sync_permission()` verlangt jetzt **zusätzlich `edit_theme_options`** (5.2.1: nur Visual-Builder-Recht). Die Rechte in Spec §10 entsprechen damit Divi. - Der Sync nimmt Chunks an (`requestId`, `chunkIndex`, `totalChunks`, Transient `et_global_preset_chunks__`); das Plugin schickt nie Chunks. - Schrittfolge des Sync (ohne Chunks) unverändert: `prepare_data` → `get_data` → `validate_preset_count` → bei Löschaktionen `get_deleted_preset_ids` + `remove_preset_from_legacy_option` → `save_data`. Das Plugin wiederholt sie in `DesignSystemWriter::savePresets()` (Mirror `preset-sync`); diese Klasse existiert noch nicht (geplant für Etappe 4a). - Signaturen aus Plan Step 1 bestätigt (`prepare_data`, `validate_preset_count`, `save_data`, `get_deleted_preset_ids`, `remove_preset_from_legacy_option`, `can_current_user_use_visual_builder`); `option_name`, `get_data`, `get_preset_attrs`, `get_preset_attrs_mapping` vorhanden. - Abweichungen bei der Formatprüfung am lebenden Bestand (Seed-Skript lief erfolgreich, Seite 170): - Zeilennummern in den Abschnitten 1–8 beziehen sich auf 5.2.1 und stimmen nicht mehr (z. B. `save_data` jetzt `GlobalPreset.php:548`, `sync_permission` jetzt `GlobalPresetController.php:465`). - Gruppen-Preset-Einträge haben `groupName` und `groupId` (wie in Abschnitt 3), aber kein `renderAttrs` im Seed-Bestand; `renderAttrs` ist optional. - Das Mapping (`Conversion::get_preset_attrs_mapping("divi/button")`) liefert `attrName`, `preset` und bei Bedarf `subName`. `preset` ist nicht immer eine Liste: gesehen wurden `"meta"`, `"content"` (String) und `["html"]` (Array). - Keine weiteren Abweichungen zu §3, außer den oben genannten. ## Nachtrag Divi 5.15.1 – Farben und Variablen (Etappe 4c, 2026-10-19) Geprüft gegen den installierten Quelltext `includes/builder-5/server/Packages/GlobalData/GlobalData.php` (5.15.1): - **Farben unverändert:** `set_global_colors(array $data, $already_sanitized = false): void` mischt mit `true` (`array_merge`), ersetzt mit `false` und bereinigt dann selbst; die fünf Hauptfarben gehen über `et_update_option()` in `et_divi` und werden aus den Daten entfernt; danach `remove_static_resources('all', 'all', true)`. `sanitize_global_colors_data(array $data): array` ist öffentlich und verlangt, dass `usedInPosts` ein Array ist. - **`get_customizer_colors()` hat einen `static`-Cache ohne Reset.** Nach einer Änderung der Hauptfarbe liefert `get_global_colors()` im selben Request noch den alten Wert (`$_cached_data` wird zurückgesetzt, der `static` nicht). LIT MCP liest die fünf Hauptfarben deshalb für das Zurücklesen aus `et_divi` (`GlobalColorsAdapter::read()`). - **Variablen: neue Signatur und Prüfungen.** `set_global_variables(array $data, ?array $expected_variables = null): ?WP_Error` kehrt weiterhin still zurück, wenn `edit_theme_options` oder `variables_manager` fehlt; sonst läuft es unter einer MySQL-Sperre (`GET_LOCK`), prüft optional den erwarteten Bestand (`global_variables_stale`, 409), verlangt, dass **jede gespeicherte `gvid-`-ID im neuen Bestand vorkommt** (`validate_global_variable_ids()` → `global_variable_ids_missing`, 400) und prüft geänderte Einträge (`validate_global_variable_changes()`: `status` `active`|`archived`, aktive Variablen mit nicht leerem `value`). **Folge: Variablen lassen sich über Divis Funktionen nicht löschen, nur archivieren** (Ruling V1 des Plans 4c). - **Neuer Typ `gradients`** (`GLOBAL_VARIABLE_TYPES = numbers, strings, images, links, fonts, gradients`); `strings` und `gradients` behalten ihren Wert zeichengetreu. `get_global_variables()` liefert zusätzlich die leere Gruppe `colors`, wandelt Objekt-Gruppen in Arrays und fügt `allowedActions` und die Customizer-Schriften hinzu (jede Gruppe als `stdClass`); beim Zurückschreiben bleiben nur Speicherformat-Felder. - **Archivierte Variablen:** `_sort_global_variable_snapshot()` behandelt `order` archivierter Variablen als `''`; der Builder setzt beim Archivieren `order: null`, das Sanitize macht daraus `''`. ## Nachtrag Divi 5.15.1 – Theme-Optionen und Schriften (Etappe 4d, 2026-10-19) Zeilenangaben und Belege: `2026-10-19-etappe-4d-divi-recherche.md` (Abschnitt 0 und A–D). 1. **`FontsUtility::font_add()` ist im Prozess aufrufbar:** mit `['action' => 'wp_handle_upload_rest']` nimmt `wp_handle_upload()` serverseitige Temp-Dateien (`copy`/`unlink` statt `move_uploaded_file`), so wie Divis KI-Pfad `AiAgentImageUtility::install_to_custom_fonts()`; ein Nachbau des `et_uploaded_fonts`-Eintrags entfällt. 2. **Divi-Rolle für Schriften ist `custom_fonts_management`** (plus `upload_files`), nicht `use_visual_builder`. 3. **Divis UI kennt nur `ttf`/`otf`** (`et_pb_get_supported_font_formats()`); `font_add` und das Frontend verarbeiten auch `woff`, `woff2` und `eot` (`mime_types_font()`). 4. **Die Standardschrift steht im Customizer als `false`, nicht als `'none'`;** ein gespeichertes `'none'` ist in Divi 5 ein echter Schriftname, „Standard“ heißt deshalb Schlüssel entfernen (`et_delete_option`). 5. **Divis Sanitizer klemmen keine Bereiche** (`absint`, `intval`, `floatval`) und liefern bei ungültiger Auswahl `false` statt eines Fehlers; Bereiche gehören ins Plugin. 6. **`body_header_style` ist im Control mehrfach wählbar, der Sanitizer nimmt aber genau einen Wert;** Kombinationen und `''` werden zu `false`. 7. **`et_builder_get_google_fonts()` kann einen HTTP-Abruf auslösen** (einmal je 24 h, nur mit API-Schlüssel) und lädt sonst 1,3 MB JSON; die Liste wird je Request einmal geholt. 8. **`font_add` prüft den Namen nur gegen Google-Fonts und `et_uploaded_fonts`,** nicht gegen Web-Safe-Schriften, und entfernt unzulässige Zeichen stillschweigend. 9. **`font_add` meldet Teilerfolg als Erfolg;** das Plugin vergleicht die gelieferten `font_file`-Schlüssel mit der Eingabe und entfernt die Schrift bei Abweichung. 10. **`ET_Core_PageResource::remove_static_resources` hat sieben Parameter** (neu: `$skip_external_cache`) und ist ohne angemeldeten Benutzer wirkungslos. 11. **`GlobalData::get_customizer_fonts()` cacht per `static` ohne Reset;** Zurücklesen läuft über `get_option('et_divi')`. 12. **`et_update_option` schreibt das ganze `et_divi`-Array aus einem Request-Global** (Lost-Update-Risiko bei gleichzeitigen Änderungen); `design_rev` und Sperre mindern es. ## 0. Grundlagen: wo Divi speichert `et_get_option()`/`et_update_option()` (`epanel/custom_functions.php:211`, `:272`) haben drei Ziele: - normale Theme-Option → Schlüssel im Array der Option **`et_divi`** (autoload); - „Product Setting“ (letzter Parameter `true`) → **eigene Option `et_divi_`**, autoload aus (`:224`, `:288-292`); - `et_delete_option()` (`:316`) kennt **keine** Product Settings und löscht immer nur den Schlüssel in `et_divi`. Folge: `GlobalPreset::delete_data()` (B5 `Packages/GlobalData/GlobalPreset.php:149-158`) ruft `et_delete_option(option_name())` auf und entfernt die Option `et_divi_builder_global_presets_d5` **nicht** – nur den statischen Cache. Für Etappe 4 nicht verwenden. `et_update_option()` aktualisiert zusätzlich das globale `$et_theme_options`; direktes `update_option('et_divi', …)` lässt diesen Cache im selben Request veralten. ## 1. Globale Farben **Speicherort:** `et_divi['et_global_data']['global_colors']` (normale Theme-Option, kein Product Setting). Die fünf Customizer-Farben liegen **nicht** dort, sondern als `et_divi['accent_color']`, `secondary_accent_color`, `header_color`, `font_color`, `link_color` (Zuordnung `GlobalData::$customizer_colors`, B5 `Packages/GlobalData/GlobalData.php:58-83`). **Format eines Eintrags** (Schlüssel = ID): ```json "gcid-q9swzy97df": { "color": "#5b3a29", "folder": "", "label": "[Fixture] Holz dunkel", "lastUpdated": "2026-09-27T22:21:39.000Z", "status": "active", "usedInPosts": [] } ``` - ID: `gcid-` + 10 Zeichen `[0-9a-z]` (Builder: `generateVariableId`/`generateNanoUuid`, `global-data.js`, Suche ``S=(e,t)=>e||("colors"===t?`gcid-${_()}``). Ältere Daten können UUIDs haben (`gcid-79d0acb1-…`, Beispiele `GlobalData.php:700-770`). Prüfung serverseitig nur `substr($id, 0, 5) === 'gcid-'` (`:597-603`). - `status`: `active` | `inactive` | `temporary`. Der Builder **löscht nie**: „Löschen“ setzt `status: inactive` (`global-data.js`, Suche `case"DELETE_GLOBAL_COLOR"`). - `label`/`folder` werden gespeichert, obwohl der Konverter-Kommentar „not until D6“ sagt (`:159-160`); der Builder liest `label` (Suche `label:t.label||""`). - `lastUpdated`: Format `Y-m-d\TH:i:s.v\Z`. - **Relative Farbe** (Farbe aus anderer Farbe): `color` enthält eine Referenz `$variable({"type":"color","value":{"name":"gcid-q9swzy97df","settings":{"lightness":30}}})$` mit `settings` aus `hue`, `saturation`, `lightness`, `opacity` (`GlobalData::transform_state_into_global_color_value`, `:181-206`). CSS-Ausgabe: `--gcid-989stx15zc: hsl(from var(--gcid-q9swzy97df) calc(h + 0) calc(s + 0) calc(l + 30));` (`FrontEnd/Module/Style.php:910-947` über `StyleLibrary/Utils/Utils.php:86-130`). Abhängigkeiten werden rekursiv aufgelöst (`GlobalData.php:220-310`, Tiefe max. 10 in `:1336ff`). **Lesen:** `GlobalData::get_global_colors()` (`:333-354`) liefert Customizer-Farben (neu erzeugt, `folder: customizer`, übersetztes Label, `lastUpdated` = jetzt, `:370-398`) **vor** den gespeicherten Farben; gleichnamige gespeicherte Customizer-IDs werden verworfen. Ergebnis wird statisch gecacht (`$_cached_data`), `set_global_colors()` setzt den Cache zurück, `get_customizer_colors()` hat einen eigenen `static`-Cache ohne Reset. **Schreiben:** `GlobalData::set_global_colors(array $data, $already_sanitized = false)` (`:650-691`): 1. ohne `$already_sanitized`: `sanitize_global_colors_data()` (`:585-616`) – verwirft Einträge ohne `color` oder ohne `gcid-`, wendet `sanitize_text_field` auf jeden skalaren Wert an; `usedInPosts` wird mit `array_map` behandelt und **muss ein Array sein** (sonst `TypeError`). 2. Customizer-IDs im Payload → `et_update_option(, color)` und aus dem Payload entfernt (`:664-674`). 3. **`$already_sanitized = false` ersetzt alle gespeicherten Farben, `true` mischt** (`array_merge`) mit den vorhandenen (`:678-684`). 4. `et_update_option('et_global_data', …)`, dann `ET_Core_PageResource::remove_static_resources('all', 'all', true)` (`:687-690`). **Builder-Speicherweg:** `POST divi/v1/global-data/global-colors` (`VisualBuilder/REST/RESTRegistration.php:1001-1008`) → Schema-Sanitize `sanitize_global_colors_data` (`GlobalDataController.php:100-108`) → `set_global_colors($colors, true)` (`:71-78`), also **Mischen**. Der Builder schickt dabei die komplette Farbliste **einschließlich der fünf Customizer-Farben** (`global-data.js`, Suche `restRoute:"/divi/v1/global-data/global-colors"`); Entfernen einer Farbe ist über diesen Weg nicht möglich. **Seiteneffekte:** - Customizer-Sync ist direkt eingebaut (Schritt 2); ein separater Hook existiert nur für Divi 4 (`includes/builder/functions.php:13205-13237`, `et_global_colors_saved`, mit veralteten IDs `gcid-header-color`/`gcid-font-color`). - `usedInPosts` pflegt nur der Builder im Browser (`UPDATE_GLOBAL_COLORS_USED_IN_POSTS`); serverseitig wird es nie berechnet. Über PHP angelegte Farben behalten `[]`. - Einmalige Migration Divi 4 → 5 (`maybe_convert_global_colors_data`, `:526-551`) läuft beim Laden der Builder-Einstellungen (`Framework/Settings/Settings.php:159-162`). **Rechte:** REST-Route nur `current_user_can('edit_posts')` (`GlobalDataController.php:124-130`); `set_global_colors()` selbst prüft nichts. Zusätzlich verlangt jede Divi-REST-Route den Nonce `X-ET-Nonce` für die Aktion `/divi/v1/--POST` (`Framework/Route/RESTRoute.php:98`, `Framework/Controllers/RESTController.php:158-178`). ## 2. Globale Variablen **Speicherort:** Product Setting **`et_divi_global_variables`** (eigene Option, autoload aus), gespeichert mit `et_update_option('global_variables', $data, false, '', '', true)` (`GlobalData.php:1016`). **Gespeichertes Format** (PHP-Arrays): ```json { "numbers": { "gvid-f6yq6mayfy": { "id": "gvid-f6yq6mayfy", "label": "[Fixture] Eckenradius", "value": "8px", "order": "1", "status": "active", "lastUpdated": "…", "variableType": "numbers" } }, "strings": { … }, "images": { … }, "links": { … }, "fonts": { … } } ``` - Typen: nur `numbers`, `strings`, `images`, `links`, `fonts` (`sanitize_global_variables_data`, `:804-850`). **`colors` wird beim Speichern verworfen** – „Farb-Variablen“ im Variablen-Manager sind die globalen Farben aus Abschnitt 1. - ID: `gvid-` + 10 Zeichen `[0-9a-z]`; Pflicht: `gvid-`-Präfix und nicht-leeres `label`. - **Das Feld `id` im Eintrag ist nötig:** die CSS-Ausgabe benutzt `$value['id']`, nicht den Schlüssel (`FrontEnd/Module/Style.php:1002`). - Sanitize: `value` bei `strings` zeichengetreu (`wp_check_invalid_utf8` + `wp_kses_no_null`), bei `links` `esc_url_raw`, sonst `sanitize_text_field` – dadurch wird `order` zum String (`"1"`). `allowedActions` wird verworfen. Der Builder speichert außerdem `lastUpdated` und `variableType` mit (`global-data.js`, Suche `case"ADD_GLOBAL_VARIABLE"`). - `status`: `active` | `archived`. Builder-„Löschen“ setzt `archived` und `order: null` (Suche `case"DELETE_GLOBAL_VARIABLE"`), echtes Entfernen nur ohne Label/Wert. - `numbers` erlaubt beliebige CSS-Ausdrücke (`clamp(1.5rem, 4vw, 2.5rem)` bleibt erhalten). **Lesen:** `GlobalData::get_global_variables()` (`:896-963`) liefert ein Array, **jede Gruppe als `stdClass`** (`(object) $items`, `:959-962`), die Einträge darin als Arrays; ergänzt `allowedActions` und stellt die Customizer-Schriften `--et_global_heading_font` / `--et_global_body_font` (aus `et_divi['heading_font']`/`['body_font']`, Standard „Open Sans“, `:93-104`, `:415-437`) an den Anfang von `fonts`. Leere Gruppen sind leere `stdClass` (JSON `{}`). Eine beschädigte `fonts`-Gruppe wird still auf `[]` gesetzt (`:925`). **Schreiben:** `GlobalData::set_global_variables(array $data)` (`:1002-1020`) **ersetzt die komplette Option**. Vorher also vollständig lesen, `stdClass` → Array wandeln, `allowedActions` und die beiden Customizer-Schriften herausnehmen (sonst landen sie in der Option), dann schreiben. `import_global_variables()` (`:1087-1137`) mischt, arbeitet aber mit `stdClass`-Gruppen und schreibt nur `id/label/value/status`. Danach `remove_static_resources('all', 'all', true)`. **Rechte:** `set_global_variables()` prüft `current_user_can('edit_theme_options') && et_pb_is_allowed('variables_manager')` und **kehrt sonst ohne Fehler zurück** (`:1003-1005`). Die REST-Route prüft nur `edit_posts` (`GlobalDataController.php:350-356`) und meldet danach Erfolg mit den unveränderten Daten – ein stiller Fehlschlag. `variables_manager` ist ein Recht des Divi-Rollen-Editors (`includes/builder/functions.php:9219-9222`, Auswertung `includes/builder/core.php:1128ff`, Option `et_pb_role_settings`; ohne Eintrag gilt „an“). **Referenzen in Modulen und Presets:** `$variable({"type":"content","value":{"name":"gvid-…","settings":{}}})$` – für Zahlen/Schriften wird daraus `var(--gvid-…)` (`StyleLibrary/Utils/Utils.php:86-130`), für Strings/Links/Bilder der Wert beim Rendern (Dynamic Content, `Packages/Module/Layout/Components/DynamicContent/DynamicContentGlobalVariableOptions.php`). `:root` enthält nur `numbers` und `fonts` (`Style.php:991-1030`), Schriftwerte in Anführungszeichen. In `post_content` ist `"` innerhalb der Referenz als `"` kodiert (WordPress-Block-Serialisierung). **Schriften der Customizer-Seite** schreibt der Builder separat: `POST divi/v1/global-data/global-fonts` → `et_update_option('heading_font'|'body_font')` + Cache leeren (`GlobalDataController.php:166-190`, Recht `edit_posts`). ## 3. Presets (Modul- und Optionsgruppen-Presets) **Speicherort:** Product Setting **`et_divi_builder_global_presets_d5`** (`GlobalPreset::option_name()` = `builder_global_presets_d5`, `GlobalPreset.php:129`), autoload aus. Divi-4-Presets: `et_divi_builder_global_presets_ng` (wird bei Bedarf importiert, Flag `builder_is_legacy_presets_imported_to_d5`). **Gespeichertes Format:** ```json { "module": { "divi/button": { "default": "", "items": { "3gbmxlurey": { …Modul-Preset… } } }, "divi/divider": { "default": "g7kvfq5uh8", "items": { "g7kvfq5uh8": { … } } } }, "group": { "divi/font-body": { "default": "", "items": { "ja6o54jtlc": { …Gruppen-Preset… } } } } } ``` Modul-Preset-Eintrag: `type: "module"`, `id`, `name`, `moduleName`, `version` (Builder-Version, hier `"5.2.1"`), `created`/`updated` (**Millisekunden**, Builder `Date.now()`; PHP-Hilfen `_generate_new_preset` nutzen `time()` in Sekunden, `:1695-1707`), `attrs`, optional `styleAttrs`, `renderAttrs`, `groupPresets`, `priority`, `order`. Gruppen-Preset-Eintrag zusätzlich `groupName` (z. B. `divi/font-body`), `groupId` (Attributpfad im Quellmodul, z. B. `content.decoration.bodyFont`), `moduleName` (Quellmodul), optional `primaryAttrName` (nur über JS-Filter gesetzt, `module-utils.js`, Suche `divi.optionGroupPresetPrimaryAttrNameResolver`). - **IDs** 10 Zeichen `[0-9a-z]`, ohne Präfix (Builder `generateDefaultPresetName`, `global-data.js`). Namen sind **nicht eindeutig** – der Builder zählt „Button 1, Button 2 …“. - **`attrs`** = normale Modulattribute (Pfad wie im Block, z. B. `button.decoration.background.desktop.value.color`); Gruppen-Presets tragen die **vollen Pfade des Quellmoduls** und werden beim Anwenden auf andere Module umgerechnet (`GlobalPresetItemGroup::_attrs_resolver`, `GlobalPresetItemGroup.php:245ff`, `GlobalPresetItemGroupAttrNameResolver`). - **`styleAttrs`/`renderAttrs`**: Teilmengen von `attrs` für CSS bzw. HTML/Skript. Serverseitig berechenbar mit `Conversion::get_preset_attrs($attrs, ['style'] | ['html','script'], Conversion::get_preset_attrs_mapping($moduleName))` (B5 `Packages/Conversion/Conversion.php:2242`, `:2479`; genutzt in `convert_preset`, `:2144-2145`). Das Frontend benutzt `renderAttrs`/`styleAttrs` der Gruppen-Presets direkt (`Packages/ModuleLibrary/ModuleRegistration.php:326-340`). - **`groupPresets`** im Modul-Preset (verschachtelte Gruppen-Presets): `{ "": { "presetId": [""], "groupName": "divi/…" } }`. - **`default`** je Modul/Gruppe: ID des Standard-Presets oder `""`. Der Builder legt beim Öffnen des Preset-Managers leere Standard-Presets an (`ADD_DEFAULT_MODULE_PRESET`); ein Standard-Preset **ohne `attrs`** erzeugt keine Klasse (`get_module_preset_class_names`, `GlobalPreset.php:730-800`). - **`priority`** fehlt meist und gilt dann als `10` (`:676`, `:2078`, `:2761`); das REST-Schema-Default `10` (`GlobalPresetController.php`, `preset_type_properties`) wird für verschachtelte Einträge nicht eingesetzt. **Verweise in Blöcken** (`post_content`): - Modul-Preset: `"modulePreset": ["3gbmxlurey"]` – **Array** (Stapel); ein String ist Altformat und wird noch akzeptiert (`normalize_preset_stack`, `:2627-2680`). `""`, `"default"`, `"_initial"` bedeuten „Standard-Preset“ (`is_preset_id_as_default`, `:2691`). Fehlt `modulePreset`, gilt das Standard-Preset des Moduls. - Gruppen-Preset: `"groupPreset": { "module.decoration.spacing": { "presetId": ["apzj7e17ok"], "groupName": "divi/spacing" } }` (`get_selected_group_presets`, `:2021-2320`; Überlagerung `:2148`). - Stapel: letztes Element gewinnt bei `get_selected_preset` (`:522-600`); `renderAttrs` werden nach `priority` aufsteigend gemischt (`:626-712`). Verschachtelte und explizite Gruppen-Presets werden gestapelt, nicht ersetzt (`:2148-2163`). - Welche Gruppen ein Modul hat und wie sie heißen: `GlobalPreset::get_group_preset_default_attr(ModuleRegistration::get_module_settings($name))` (`:2332-2465`), Namensliste `get_default_group_name` (`:2475-2511`). Beispiel `divi/text`: `content.decoration.bodyFont` → `divi/font-body`, `module.decoration.spacing` → `divi/spacing`; `divi/button`: `button.decoration.font` → `divi/font`, `button.decoration.button` → `divi/button`. **CSS-Klassen im Frontend** (`GlobalPresetItemUtils::generate_preset_class_name`, `GlobalPresetItemUtils.php:43-78`): `preset--module--divi-button--3gbmxlurey`, Standard-Preset `preset--module--divi-divider--default`, Gruppe `preset--group--divi-blurb--divi-spacing--apzj7e17ok`, verschachtelt `preset--group--divi-text--divi-font-body--nested--ja6o54jtlc`. Keine Klassen für `divi/id-classes`, `divi/animation`. Am Fixture geprüft (README in `tests/Fixtures/divi/stage4/`). **Schreiben:** `GlobalPreset::save_data(array $data)` (`:419-431`) schreibt die **komplette Option ungeprüft**, leert Caches (`remove_static_resources('all','all',true,'all',true)` – Builder-CSS bleibt erhalten) und setzt `self::$_data` zurück. Sanitisierung gibt es nur in `GlobalPreset::prepare_data()` (`:213-268`): wandelt das REST-Format (`{module: [{default, items: [ … ]}], group: …}`, Listen statt ID-Schlüssel) ins Speicherformat, entfernt leere Attribut-Arrays und sanitisiert `attrs`/`renderAttrs`/`styleAttrs` mit `SavingUtility::sanitize_block_attrs` bzw. `sanitize_group_attrs` (`VisualBuilder/Saving/SavingUtility.php:2153`, `:2296`; ohne `unfiltered_html` wird gefiltert). **Builder-Speicherweg:** `POST divi/v1/global-data/global-preset/sync` (`RESTRegistration.php:1035-1045`) → `GlobalPresetController::sync` (`GlobalPresetController.php:45-187`): optional in Blöcken (Transient 5 min), `prepare_data`, **`validate_preset_count`** (`GlobalPreset.php:335-410`: weniger Presets als gespeichert → Fehler 400 `preset_count_decreased`, außer `actionType` `DELETE_MODULE_PRESET`/`DELETE_OPTION_GROUP_PRESET`), bei Löschaktionen Bereinigung der Divi-4-Option (`remove_preset_from_legacy_option`), dann `save_data`. Der Builder schickt immer den **vollständigen** Bestand. **Rechte:** `sync_permission()` = `UserRole::can_current_user_use_visual_builder()` (`GlobalPresetController.php:460`, `Framework/UserRole/UserRole.php:38-70`) – nur das Rollen-Recht „Visual Builder verwenden“ (Standard „an“), **kein** `edit_theme_options`. Dazu der Routen-Nonce wie in Abschnitt 1. **Preset-Historie:** `et_divi_builder_presets_history_meta` (`{index, size}`) und `et_divi_builder_presets_history_item_` gehören zu Divi 4 (`includes/builder/feature/global-presets/History.php:4-10`, Schreiben nur per AJAX `et_builder_save_global_presets_history` mit `switch_themes`, `:75-139`). Der Divi-5-Sync schreibt dort **nichts**; Rückgängig im Builder ist rein clientseitig. Lokal: `{index: -1, size: 0}`. Für Etappe 4 heißt das: eine Wiederherstellung gibt es nur über eigene Snapshots (Spec §7). ## 4. Theme-Optionen (`et_divi`) Lokal ist **keiner** der Whitelist-Schlüssel gesetzt (Fixture `et-divi-subset.json`) – überall gelten Divi-Standards. | Gruppe | Schlüssel in `et_divi` | Schreibweg in Divi | Sanitize / Standard (Customizer, `functions.php`) | |---|---|---|---| | Schriften | `heading_font`, `body_font`, `primary_nav_font`, `secondary_nav_font`, `all_buttons_font` | Customizer, `global-fonts`-REST (nur heading/body) | `et_sanitize_font_choices`, Standard `none` (`:2484`, `:2557`, `:3398`, `:3522`, `:4996`) | | | `heading_font_weight`, `body_font_weight` | Customizer | `et_sanitize_global_font_weight`, `500` (`:2509`, `:2582`) | | Größen | `body_font_size` (14), `tablet_body_font_size`, `phone_body_font_size`, `body_header_size` (30), `tablet_header_font_size`, `phone_header_font_size` | Customizer | `absint` (`:2208-2456`); Tablet/Phone-Standard = nächstgrößerer Wert | | | `body_font_height` (1.7), `body_header_height` (1) | Customizer | `et_sanitize_float_number` (`:2236`, `:2376`) | | | `body_header_spacing` (0), `body_header_style` (`''`) | Customizer | `et_sanitize_int_number`, `et_divi_global_font_style` (`:2348`, `:2404`) | | Layout | `content_width` (1080), `gutter_width` (3), `section_padding` (4), `row_padding` (2) | Customizer | `absint` (`:1824`, `:1852`, `:1927`, `:2011`) | | Farben | `accent_color`, `secondary_accent_color`, `header_color`, `font_color`, `link_color` | Customizer **und** `set_global_colors` (Abschnitt 1) | `et_sanitize_alpha_color` (`:2534-2665`) | | Buttons | `all_buttons_*` (19 Schlüssel inkl. `*_hover`, z. B. `all_buttons_bg_color`, `all_buttons_border_radius_hover`) | Customizer | je Feld (`:4843ff`) | | Logo | `divi_logo` | ePanel „Theme-Optionen“ | Typ `upload`, URL-String (`options_divi.php:81`) | | SEO | `divi_seo_home_*`, `divi_seo_single_*`, `divi_seo_index_*` | ePanel | Checkboxen `'on'`/`'false'`, Texte, Selects (`options_divi.php:721-894`; Speichern `epanel/core_functions.php:868ff`, Checkbox `:969-970`/`:1049`) | - Customizer-Einstellungen sind `type => 'option'` mit Schlüssel `et_divi[]` und `capability => 'edit_theme_options'`; Divi prüft zusätzlich das Rollen-Recht `theme_customizer` (`functions.php:1278`). ePanel: `edit_theme_options` (`epanel/core_functions.php:873`) und Rollen-Recht `theme_options` (`functions.php:8834-8846`). - **Standardwerte weichen voneinander ab:** `secondary_accent_color` hat im Customizer `#8800FF` (`functions.php:2665`), in `GlobalData::$customizer_colors` und im Builder `#2ea3f2` (`GlobalData.php:64-68`, `includes/builder/core.php:7293`). Wer die Builder-Sicht (`get_global_colors()`) zurückschreibt, setzt die Customizer-Farbe damit fest auf `#2ea3f2`. Ebenso Schriften: Customizer-Standard `none`, Builder-Sicht „Open Sans“. - Caches: nach Customizer-Speichern `remove_static_resources('all','all')` (`core/components/PageResource.php:944`, `:1139-1141`) und `et_pb_force_regenerate_templates` (`includes/builder/core.php:3771`). Für eigene Schreibzugriffe gilt dasselbe. ## 5. Caches `ET_Core_PageResource::remove_static_resources($post_id, $owner, $force, $slug, $preserve_vb_files, $delete_files)` (`core/components/PageResource.php:1213-1232`) läuft **nur**, wenn `wp_doing_cron()` oder `et_core_security_check_passed('edit_posts')` – ohne angemeldeten Benutzer (z. B. WP-CLI ohne `--user`) ist der Aufruf wirkungslos. Farben und Variablen rufen `('all','all',true)`, Presets `('all','all',true,'all',true)` bereits selbst auf; der in Spec §7 vorgesehene Zusatzaufruf ist harmlos, aber redundant. In-Request-Caches: `GlobalData::$_cached_data` (Reset bei `set_*`), `GlobalPreset::$_data` und `$_normalized_preset_stack_cache` (Reset in `save_data`/`delete_data`), `static` in `get_customizer_colors()`/`get_customizer_fonts()` und `get_group_preset_default_attr()` (kein Reset). ## 6. Rechte im Überblick | Weg | Prüfung in Divi | |---|---| | REST `global-colors`, `global-fonts`, `global-variables` | `edit_posts` + Routen-Nonce | | `GlobalData::set_global_variables()` | `edit_theme_options` **und** `et_pb_is_allowed('variables_manager')`, sonst stilles `return` | | `GlobalData::set_global_colors()`, `GlobalPreset::save_data()` | keine | | REST `global-preset/sync` | Rollen-Recht `use_visual_builder` + Routen-Nonce | | Customizer | `edit_theme_options` + Rollen-Recht `theme_customizer` | | ePanel | `edit_theme_options` + Rollen-Recht `theme_options` | | `remove_static_resources` | `edit_posts` (oder Cron), sonst wirkungslos | ## 7. Abweichungen und Ergänzungen zu Spec §7 1. **„Variablen … über `GlobalData::set_global_variables`“** – stimmt, aber die Funktion ersetzt die ganze Option, liefert keinen Fehler bei fehlenden Rechten und kennt keinen Typ `colors`. Das Tool muss vorher lesen, `stdClass` → Array wandeln, `allowedActions` und die zwei Customizer-Schriften entfernen und nach dem Schreiben per Lesen prüfen. 2. **„globale Farben … inkl. der fünf Hauptfarben (über `set_global_colors`)“** – die Hauptfarben landen in `et_divi['accent_color']` usw.; der zweite Parameter entscheidet zwischen Ersetzen (`false`) und Mischen (`true`). Für Änderungen einzelner Farben: Mischen; Löschen wie der Builder als `status: inactive`. Hauptfarben nur schicken, wenn sie wirklich geändert werden sollen (Standardwert-Abweichung, Abschnitt 4). 3. **„Presets … per Name“** – Namen sind nicht eindeutig; IDs sind 10-stellige Zeichenketten. Schreiben immer als vollständiger Bestand; `save_data()` sanitisiert nicht → vorher `prepare_data()` (REST-Format) und `validate_preset_count()` wie der Controller verwenden. `styleAttrs`/`renderAttrs` müssen mitgeschrieben werden (Frontend liest sie). 4. **„Voraussetzung: `edit_theme_options`; für Variablen zusätzlich `variables_manager`“** – strenger als Divi selbst (Farben: `edit_posts`; Presets: nur Visual-Builder-Recht). Als bewusste Plugin-Entscheidung beibehalten; für Variablen ist die Doppelprüfung zwingend, weil Divi sonst still nichts tut. 5. **„danach `remove_static_resources('all','all',true)`“** – Divis Setter tun das schon; wirkungslos ohne angemeldeten Benutzer. Für Presets nutzt Divi `preserve_vb_files = true`. 6. **Snapshots** sind die einzige Wiederherstellung: Divi 5 führt für Farben, Variablen und Presets keine serverseitige Historie; die Option `et_divi_builder_presets_history_meta` gehört zu Divi 4. 7. **Theme-Optionen:** `divi_logo` und `divi_seo_*` sind ePanel-Felder (andere Sanitize-Regeln, Checkbox-Werte `'on'`/`'false'`), keine Customizer-Einstellungen. Die übrigen Whitelist-Felder haben die Customizer-Sanitize-Callbacks aus Abschnitt 4 – diese sollten wiederverwendet werden. 8. **Lesepfad des Plugins** (`src/Integrations/Divi/Design/DesignSystem.php`): Farb-„Variablen“ (`variables['colors']`) sind in 5.2.1 immer leer – globale Farben tragen Label und ID selbst. `fonts()` liefert die Builder-Sicht („Open Sans“), nicht den Customizer-Standard `none`. ## 8. Stolpersteine (Kurzliste) - `get_global_variables()` liefert `stdClass` je Gruppe, gespeichert sind Arrays; leere Gruppe = `{}` im JSON. - Variableneinträge brauchen das Feld `id`; `order` wird zum String. - `usedInPosts` muss ein Array sein; wird nur vom Builder gepflegt. - `created`/`updated` in Millisekunden (Builder) vs. Sekunden (PHP-Hilfen). - `modulePreset` ist ein Array; Standard-Preset wirkt nur mit `attrs`. - Sync-Endpunkt verweigert kleinere Preset-Anzahl ohne `DELETE_*`-actionType. - `GlobalPreset::delete_data()` löscht die Product-Setting-Option nicht. - Divi-REST-Routen verlangen `X-ET-Nonce` pro Route; direkte PHP-Aufrufe umgehen Nonce, Schema und Rechteprüfung. - `secondary_accent_color`: Customizer-Standard `#8800FF`, Builder-Standard `#2ea3f2`.