7, 'post_type' => 'page', 'post_status' => 'draft', 'post_title' => 'Start', 'post_content' => $content], $fields )); Functions\when('get_post')->justReturn($post); Functions\when('current_user_can')->justReturn(true); Functions\when('get_permalink')->justReturn('https://kunde.test/?page_id=7'); Functions\when('add_query_arg')->alias(static fn (string $key, string $value, string $url): string => $url . '&' . $key . '=' . $value); return $post; } public static function schema(bool $diviEnabled = true): ContentSchema { $schema = Mockery::mock(ContentSchema::class); $schema->allows('isManaged')->andReturn(true); $schema->allows('diviEnabled')->andReturn($diviEnabled); return $schema; } /** Shapes as GlobalData / GlobalPreset return them (Divi 5.2.1, verified in DDEV). */ public static function designSystem(): DesignSystem { return DesignSystem::fromDivi( [ 'gcid-primary-color' => ['color' => '#2ea3f2', 'label' => 'Primärfarbe', 'status' => 'active'], 'gcid-body-color' => ['color' => '#666666', 'label' => 'Fließtextfarbe', 'status' => 'active'], 'gcid-a1b2' => ['color' => '#0b3d59', 'label' => 'Nachtblau', 'status' => 'active'], 'gcid-old' => ['color' => '#ff0000', 'label' => 'Alt', 'status' => 'inactive'], ], [ 'numbers' => ['gvid-r1' => ['id' => 'gvid-r1', 'label' => 'Radius', 'value' => '8px', 'status' => 'active']], 'strings' => ['gvid-s1' => ['id' => 'gvid-s1', 'label' => 'Telefon', 'value' => '0221 123', 'status' => 'active']], // Divi returns every variable type as stdClass (verified in DDEV). 'fonts' => (object) [ '--et_global_heading_font' => ['id' => '--et_global_heading_font', 'label' => 'Überschrift', 'value' => 'Open Sans', 'status' => 'active'], '--et_global_body_font' => ['id' => '--et_global_body_font', 'label' => 'Fließtext', 'value' => 'Lato', 'status' => 'active'], ], 'links' => new \stdClass(), ], [ 'module' => ['divi/button' => ['default' => 'p-1', 'items' => [ 'p-1' => ['id' => 'p-1', 'name' => 'Standard', 'moduleName' => 'divi/button'], 'p-2' => ['id' => 'p-2', 'name' => 'CTA primär', 'moduleName' => 'divi/button'], ]]], 'group' => ['divi/font' => ['default' => '', 'items' => [ 'g-1' => ['id' => 'g-1', 'name' => 'Fließtext groß', 'groupName' => 'divi/font'], ]]], ], ['body_font_size' => '16'] ); } /** Real Divi 5.2.1 metadata (tests/Integration/divi-metadata-fixture.php). */ public static function catalog(): ModuleCatalog { $sample = json_decode( (string) file_get_contents(dirname(__DIR__) . '/Fixtures/divi/metadata-sample.json'), true ); return new ModuleCatalog( $sample['metadata'], static fn (string $name): array => (array) ($sample['defaults'][$name] ?? []) ); } /** * Shape of DynamicContentOptions::get_options($pageId, 'edit') in Divi 5.2.1 (excerpt of a * live dump for a page: no post_categories/post_tags – pages have no such taxonomies). * * @return array> */ public static function dynamicContentRegistry(): array { $text = static fn (): array => ['label' => 'x', 'type' => 'text', 'default' => '']; $option = static fn (string $id, string $group, array $fields): array => [ 'id' => $id, 'label' => $id, 'type' => 'text', 'custom' => false, 'group' => $group, 'fields' => $fields, ]; return [ 'post_title' => $option('post_title', 'Default', ['before' => $text(), 'after' => $text()]), 'post_excerpt' => $option('post_excerpt', 'Default', [ 'before' => $text(), 'after' => $text(), 'words' => $text(), 'read_more_label' => $text(), ]), 'post_date' => $option('post_date', 'Default', [ 'before' => $text(), 'after' => $text(), 'date_format' => ['label' => 'x', 'type' => 'select', 'default' => 'default'], 'custom_date_format' => $text(), ]), 'post_author' => $option('post_author', 'Default', [ 'before' => $text(), 'after' => $text(), 'name_format' => ['type' => 'select'], 'link' => ['type' => 'yes_no_button'], ]), 'site_title' => $option('site_title', 'Default', ['before' => $text(), 'after' => $text()]), 'post_link_url' => $option('post_link_url', 'Default', []), 'post_link_url_page' => $option('post_link_url_page', 'Default', ['post_id' => ['type' => 'select_post']]), 'post_featured_image' => $option('post_featured_image', 'Default', []), 'post_meta_key' => $option('post_meta_key', 'Standard', [ 'before' => $text(), 'after' => $text(), 'select_meta_key' => ['type' => 'select'], 'meta_key' => $text(), 'enable_html' => ['type' => 'yes_no_button'], ]), 'loop_post_title' => $option('loop_post_title', 'Loop', [ 'before' => $text(), 'after' => $text(), 'loop_position' => $text(), ]), 'loop_post_meta_key_manual_custom_field' => $option('loop_post_meta_key_manual_custom_field', 'Loop', [ 'loop_meta_key' => $text(), ]), // Odd entries a third-party filter could add. 'broken_fields' => $option('broken_fields', 'Default', ['a' => 'not-an-array', 'b' => ['type' => 3]]), 'no_fields' => ['id' => 'no_fields', 'type' => 'text'], 7 => $option('7', 'Default', []), 'not_an_array' => 'x', ]; } /** * Hand-crafted attacks (preflight §4 "Malicious $variable tests"). * * @return array Value => part of the German error. */ public static function maliciousRefs(): array { $end = self::escape('0029') . '$'; return [ 'escaped end marker in before' => [self::content('post_title', '{"before":"x' . $end . ' y","after":""}'), 'allein im Wert'], 'escaped end marker in after' => [self::content('post_title', '{"before":"","after":"' . $end . '"}'), 'allein im Wert'], 'escaped end marker in date format' => [self::content('post_date', '{"custom_date_format":"d' . $end . '"}'), 'allein im Wert'], 'escaped end marker in name' => [self::content('post_title' . $end, '{}'), 'Unbekannter Name'], 'select with markup' => [self::content('post_date', '{"date_format":"d"}'), 'date_format'], 'type evil' => ['$variable({"type":"evil","value":{"name":"gcid-a1b2","settings":{}}})$', 'Typ'], 'unknown gcid' => ['$variable({"type":"color","value":{"name":"gcid-nope","settings":{}}})$', 'gcid-nope'], 'unknown gvid' => [self::content('gvid-nope', '{}'), 'gvid-nope'], 'content gvid as color' => ['$variable({"type":"color","value":{"name":"gvid-s1","settings":{}}})$', 'gvid-s1'], 'color gcid as content' => [self::content('gcid-a1b2', '{}'), 'gcid-a1b2'], 'name with paren' => ['$variable({"type":"color","value":{"name":"gcid-a1b2)","settings":{}}})$', 'Name'], 'name with style breakout' => [ '$variable({"type":"color","value":{"name":"gcid-a1b2;}","settings":{}}})$', 'Name', ], 'settings on a global variable' => [self::content('gvid-s1', '{"before":"x"}'), 'before'], 'settings on a theme font' => [self::content('--et_global_heading_font', '{"after":""}'), 'after'], 'unknown settings key' => [self::content('post_title', '{"before":"","foo":"1"}'), 'foo'], 'setting the option lacks' => [self::content('post_featured_image', '{"before":"x"}'), 'before'], 'enable_html' => [self::content('custom_meta_telefon', '{"before":"","after":"","enable_html":"on"}'), 'enable_html'], 'color setting not numeric' => [ '$variable({"type":"color","value":{"name":"gcid-a1b2","settings":{"opacity":"1;}"}}})$', 'opacity', ], 'protected meta key' => [self::content('custom_meta__edit_lock', '{}'), 'custom_meta__edit_lock'], 'doubled meta prefix' => [ self::content('custom_meta_custom_meta__edit_lock', '{}'), 'custom_meta_custom_meta__edit_lock', ], 'loop content outside a loop' => [self::content('loop_post_title', '{}'), 'loop_post_title'], 'unknown dynamic content' => [self::content('post_meta_key', '{}'), 'post_meta_key'], 'extra top-level key' => ['$variable({"type":"color","value":{"name":"gcid-a1b2","settings":{}},"x":1})$', 'x'], 'extra value key' => ['$variable({"type":"color","value":{"name":"gcid-a1b2","settings":{},"x":1}})$', 'x'], 'greedy smuggling' => ['$variable({"a":"})$"})$', 'allein im Wert'], 'nested reference in before' => [self::content('post_title', '{"before":"$variable(x)$","after":""}'), 'allein im Wert'], // S28: a marker the sanitizer rebuilds inside a cleaned setting (html(), text()). 'tag-split marker in before' => [self::content('post_title', '{"before":"$variable(y","after":""}'), 'allein im Wert'], 'NUL-split marker in after' => [ self::content('post_title', '{"before":"","after":"$variable' . self::escape('0000') . '(y"}'), 'allein im Wert', ], 'octet-split marker in date format' => [self::content('post_date', '{"custom_date_format":"$variable%41(y"}'), 'allein im Wert'], // S32: text() keeps control characters (like sanitize_text_field); VariableRef strips them. 'NUL-split marker in date format' => [ self::content('post_date', '{"custom_date_format":"$variable' . self::escape('0000') . '(y"}'), 'allein im Wert', ], 'DEL-split marker in date format' => [ self::content('post_date', '{"custom_date_format":"$variable' . self::escape('007f') . '(y"}'), 'allein im Wert', ], 'nested marker in before' => [self::content('post_title', '{"before":"$variable(","after":""}'), 'allein im Wert'], 'broken json' => ['$variable(kaputt)$', 'JSON'], 'trailing text' => [self::content('post_title', '{}') . ' ', 'allein im Wert'], 'embedded in text' => ['

Hi ' . self::content('post_title', '{"before":"","after":""}') . '

', 'allein im Wert'], ]; } private static function content(string $name, string $settings): string { return '$variable({"type":"content","value":{"name":"' . $name . '","settings":' . $settings . '}})$'; } /** A JSON "\uXXXX" escape, built from bytes so no tool turns it into the character itself. */ private static function escape(string $hex): string { return chr(92) . 'u' . $hex; } }