12 ) { return new WP_Error('apeari_plan', __('Serwer zwrócił nieprawidłowy plan zmian.', 'apeari'), [ 'status' => 502, ]); } $clean = ['summary' => sanitize_text_field(mb_substr($plan['summary'], 0, 500)), 'operations' => []]; $node_count = 0; foreach ($plan['operations'] as $op) { if (!is_array($op) || empty($op['type'])) { return self::invalid(); } switch ($op['type']) { case 'remove_block': case 'duplicate_block': if ('remove_block' === $op['type'] && count($plan['operations']) !== 1) { return self::invalid(); } $clean['operations'][] = ['type' => $op['type']]; break; case 'move_block': if (!in_array($op['direction'] ?? '', ['up', 'down'], true)) { return self::invalid(); } $clean['operations'][] = ['type' => 'move_block', 'direction' => $op['direction']]; break; case 'set_layout': if ( !in_array($op['layout'] ?? '', ['constrained', 'flex', 'grid'], true) || !in_array($op['orientation'] ?? '', ['horizontal', 'vertical'], true) ) { return self::invalid(); } $clean['operations'][] = [ 'type' => 'set_layout', 'layout' => $op['layout'], 'orientation' => $op['orientation'], ]; break; case 'set_alignment': if (!in_array($op['value'] ?? '', ['left', 'center', 'right'], true)) { return self::invalid(); } $clean['operations'][] = ['type' => 'set_alignment', 'value' => $op['value']]; break; case 'set_heading_level': if (!is_int($op['level'] ?? null) || $op['level'] < 1 || $op['level'] > 6) { return self::invalid(); } $clean['operations'][] = ['type' => 'set_heading_level', 'level' => $op['level']]; break; case 'set_link': $url = $op['url'] ?? null; if ( !is_string($url) || strlen($url) > 2000 || !is_bool($op['newTab'] ?? null) || !preg_match('~^(?:https?://|/|\#|mailto:|tel:)~i', $url) || preg_match('/[\x00-\x20<>"\x27]/', $url) || wp_parse_url($url, PHP_URL_USER) || wp_parse_url($url, PHP_URL_PASS) ) { return self::invalid(); } $clean['operations'][] = ['type' => 'set_link', 'url' => $url, 'newTab' => $op['newTab']]; break; case 'set_text': if (!isset($op['text']) || !is_string($op['text']) || strlen($op['text']) > 8000) { return self::invalid(); } $clean['operations'][] = [ 'type' => 'set_text', 'text' => sanitize_textarea_field($op['text']), ]; break; case 'set_style': if ( !isset($op['property'], $op['value']) || !in_array($op['property'], self::STYLES, true) || !is_string($op['value']) ) { return self::invalid(); } $color = in_array($op['property'], ['color', 'backgroundColor', 'borderColor'], true); $pattern = $color ? '/^#[a-fA-F0-9]{6}$/' : ('lineHeight' === $op['property'] ? '/^(?:[0-9](?:\.\d{1,2})?|\d{1,3}(?:\.\d{1,2})?(?:px|rem|em|%))$/' : '/^(?:0|(?:\d{1,3}(?:\.\d{1,2})?)(?:px|rem|em|%))$/'); if (!preg_match($pattern, $op['value'])) { return self::invalid(); } $clean['operations'][] = [ 'type' => 'set_style', 'property' => $op['property'], 'value' => $op['value'], ]; break; case 'set_image': case 'insert_image': if (!is_string($op['mediaKey'] ?? null) || !preg_match('/^m[0-7]$/', $op['mediaKey'])) { return self::invalid(); } if ( 'insert_image' === $op['type'] && !in_array($op['position'] ?? '', ['before', 'after', 'inside'], true) ) { return self::invalid(); } $clean['operations'][] = array_filter( [ 'type' => $op['type'], 'mediaKey' => $op['mediaKey'], 'position' => $op['position'] ?? null, ], static fn($value) => null !== $value, ); break; case 'insert_gallery': if ( !in_array($op['position'] ?? '', ['before', 'after', 'inside'], true) || !is_array($op['mediaKeys'] ?? null) || 1 === count($op['mediaKeys']) || count($op['mediaKeys']) > 16 ) { return self::invalid(); } if (!in_array($op['presentation'] ?? 'gallery', ['gallery', 'carousel'], true)) { return self::invalid(); } foreach ($op['mediaKeys'] as $key) { if (!is_string($key) || !preg_match('/^m[0-7]$/', $key)) { return self::invalid(); } } if (count(array_unique($op['mediaKeys'])) !== count($op['mediaKeys'])) { return self::invalid(); } $clean['operations'][] = [ 'type' => 'insert_gallery', 'position' => $op['position'], 'mediaKeys' => array_values($op['mediaKeys']), 'presentation' => $op['presentation'] ?? 'gallery', ]; break; case 'insert_section': if ( !isset($op['title'], $op['body']) || !is_string($op['title']) || !is_string($op['body']) || strlen($op['title']) > 500 || strlen($op['body']) > 8000 ) { return self::invalid(); } if (!in_array($op['position'] ?? 'after', ['before', 'after', 'inside'], true)) { return self::invalid(); } $clean['operations'][] = [ 'type' => 'insert_section', 'title' => sanitize_text_field($op['title']), 'body' => sanitize_textarea_field($op['body']), 'position' => $op['position'] ?? 'after', ]; break; case 'insert_blocks': if ( !in_array($op['position'] ?? '', ['before', 'after', 'inside'], true) || !is_array($op['blocks'] ?? null) || !$op['blocks'] ) { return self::invalid(); } $blocks = self::nodes($op['blocks'], 1, $node_count); if (is_wp_error($blocks)) { return $blocks; } $clean['operations'][] = [ 'type' => 'insert_blocks', 'position' => $op['position'], 'blocks' => $blocks, ]; break; default: return self::invalid(); } } return $clean; } private static function nodes(array $nodes, int $depth, int &$count) { if ($depth > 5) { return self::invalid(); } $result = []; foreach ($nodes as $node) { if ( ++$count > 40 || !is_array($node) || !in_array($node['blockName'] ?? '', array_diff(self::BLOCKS, ['core/image']), true) || !is_string($node['text'] ?? null) || strlen($node['text']) > 8000 || !is_array($node['children'] ?? null) ) { return self::invalid(); } if ($node['children'] && !in_array($node['blockName'], self::CONTAINERS, true)) { return self::invalid(); } $children = $node['children'] ? self::nodes($node['children'], $depth + 1, $count) : []; if (is_wp_error($children)) { return $children; } foreach ($children as $child) { if (!self::allowed_parent($child['blockName'], $node['blockName'])) { return self::invalid(); } } $result[] = [ 'blockName' => $node['blockName'], 'text' => sanitize_textarea_field($node['text']), 'children' => $children, ]; } return $result; } public static function allowed_parent(string $child, ?string $parent): bool { $required = [ 'core/column' => 'core/columns', 'core/button' => 'core/buttons', 'core/list-item' => 'core/list', ]; $only = [ 'core/columns' => 'core/column', 'core/buttons' => 'core/button', 'core/list' => 'core/list-item', 'core/gallery' => 'core/image', ]; return (!isset($required[$child]) || $required[$child] === $parent) && (!isset($only[$parent ?? '']) || $only[$parent] === $child); } public static function for_context(array $plan, array $context) { $placement = $context['target']['placement']; $parents = $context['ancestors']; $parent = $parents ? end($parents)['blockName'] : null; foreach ($plan['operations'] as $op) { if (in_array($op['type'], ['remove_block', 'duplicate_block', 'move_block'], true)) { $attrs = $context['attributes'] ?? []; if ( !empty($attrs['templateLock']) || !empty($attrs['lock']['remove']) || ('move_block' === $op['type'] && !empty($attrs['lock']['move'])) ) { return self::invalid(); } } if ('set_layout' === $op['type'] && 'core/group' !== $context['blockName']) { return self::invalid(); } if ('set_heading_level' === $op['type'] && 'core/heading' !== $context['blockName']) { return self::invalid(); } if ('set_link' === $op['type'] && 'core/button' !== $context['blockName']) { return self::invalid(); } if ( 'set_alignment' === $op['type'] && !in_array($context['blockName'], ['core/paragraph', 'core/heading', 'core/button'], true) ) { return self::invalid(); } if ( 'set_text' === $op['type'] && !in_array( $context['blockName'], ['core/paragraph', 'core/heading', 'core/button', 'core/list-item'], true, ) ) { return self::invalid(); } if ('set_image' === $op['type'] && 'core/image' !== $context['blockName']) { return self::invalid(); } if ( 'set_style' === $op['type'] && !in_array($op['property'], APEARI_Providers::allowed_styles($context['blockName']), true) ) { return self::invalid(); } $insertion = in_array( $op['type'], ['insert_section', 'insert_blocks', 'insert_image', 'insert_gallery'], true, ); if ('edit' !== $placement && (!$insertion || $op['position'] !== $placement)) { return new WP_Error( 'apeari_placement', __( 'Plan AI nie odpowiada wybranemu miejscu wstawienia. Nie zastosowano zmian.', 'apeari', ), ['status' => 422], ); } if (!$insertion) { continue; } $destination = 'inside' === $op['position'] ? $context['blockName'] : $parent; if ('inside' === $op['position'] && !in_array($destination, self::CONTAINERS, true)) { return self::invalid(); } $nodes = 'insert_section' === $op['type'] ? [['blockName' => 'core/group']] : ('insert_image' === $op['type'] ? [['blockName' => 'core/image']] : ('insert_gallery' === $op['type'] ? [['blockName' => 'core/gallery']] : $op['blocks'])); foreach ($nodes as $node) { if (!self::allowed_parent($node['blockName'], $destination)) { return self::invalid(); } } } return $plan; } private static function invalid(): WP_Error { return new WP_Error( 'apeari_operation', __('Plan zawiera nieobsługiwaną lub niebezpieczną operację.', 'apeari'), [ 'status' => 422, ], ); } public static function demo(string $instruction) { $lower = mb_strtolower($instruction); $ops = []; if ( preg_match( '/(?:tekst|treść|tresc|napis|nagłówek|naglowek).*?[„"“](.+?)[”"“]/us', $instruction, $m, ) ) { $ops[] = ['type' => 'set_text', 'text' => $m[1]]; } $colors = [ 'niebiesk' => '#2563eb', 'zielon' => '#16a34a', 'czerwon' => '#dc2626', 'czarn' => '#111827', 'biał' => '#ffffff', 'bial' => '#ffffff', 'fiolet' => '#7c3aed', 'pomarańcz' => '#ea580c', 'pomarancz' => '#ea580c', ]; $color = null; if (preg_match('/#[a-f0-9]{6}\b/i', $instruction, $m)) { $color = $m[0]; } if (!$color) { foreach ($colors as $word => $hex) { if (str_contains($lower, $word)) { $color = $hex; break; } } } if ($color) { $ops[] = [ 'type' => 'set_style', 'property' => preg_match('/tło|tlo|background/u', $lower) ? 'backgroundColor' : 'color', 'value' => $color, ]; } foreach ( [ 'fontSize' => '/(?:font|czcionk|rozmiar\s+tekstu)[^\d]{0,25}(\d{1,3}(?:\.\d{1,2})?\s*(?:px|rem|em))/u', 'padding' => '/(?:padding|odstęp\s+wewn|odstep\s+wewn)[^\d]{0,25}(\d{1,3}(?:\.\d{1,2})?\s*(?:px|rem|em))/u', 'borderRadius' => '/(?:zaokrągl|zaokragl|radius)[^\d]{0,25}(\d{1,3}(?:\.\d{1,2})?\s*(?:px|rem|em))/u', ] as $property => $regex ) { if (preg_match($regex, $lower, $m)) { $ops[] = [ 'type' => 'set_style', 'property' => $property, 'value' => preg_replace('/\s/', '', $m[1]), ]; } } if (preg_match('/dodaj.*sekcj/u', $lower)) { $ops = [ [ 'type' => 'insert_blocks', 'position' => 'after', 'blocks' => [ [ 'blockName' => 'core/group', 'text' => '', 'children' => [ [ 'blockName' => 'core/heading', 'text' => __('Porozmawiajmy o Twoim projekcie', 'apeari'), 'children' => [], ], [ 'blockName' => 'core/paragraph', 'text' => __( 'Opisz swój pomysł. Wspólnie znajdziemy rozwiązanie dopasowane do Twoich potrzeb.', 'apeari', ), 'children' => [], ], ], ], ], ], ]; } if (!$ops) { return new WP_Error( 'apeari_demo', __( 'Tryb demo rozpoznaje przykłady: Zmień tekst na „Witaj”, Ustaw tło #2563eb, Ustaw rozmiar tekstu 32px, Ustaw padding 24px, Dodaj sekcję. Swobodne polecenia wymagają podłączenia API AI.', 'apeari', ), ['status' => 422], ); } return self::validate([ 'summary' => __('Zmiana demonstracyjna — sprawdź rezultat w podglądzie.', 'apeari'), 'operations' => $ops, ]); } }