$block['blockName'] || '' !== trim($block['innerHTML']), ), ); } public static function find(string $content, array $path) { if (!$path || count($path) > 30) { return self::invalid(); } $nodes = self::nodes(parse_blocks($content)); $ancestors = []; foreach ($path as $index) { if (!is_int($index) || $index < 0 || !isset($nodes[$index])) { return self::invalid(); } $block = $nodes[$index]; $ancestors[] = $block; $siblings = $nodes; $nodes = self::nodes($block['innerBlocks']); } return ['block' => $block, 'ancestors' => $ancestors, 'siblings' => $siblings]; } private static function invalid(): WP_Error { return new WP_Error('apeari_target', __('Zaznacz aktualny blok w podglądzie.', 'apeari'), [ 'status' => 422, ]); } public static function snapshot(string $session_id, string $ref) { if (!preg_match('/^[a-f0-9]{48}$/', $session_id)) { return new WP_Error('apeari_session', __('Sesja edytora wygasła.', 'apeari'), ['status' => 410]); } $session = APEARI_Workspace::load($session_id); if ( is_wp_error($session) || $session['user'] !== get_current_user_id() || $session['theme'] !== get_stylesheet() ) { return new WP_Error('apeari_session', __('Sesja edytora wygasła. Odśwież edytor.', 'apeari'), [ 'status' => 410, ]); } $document = APEARI_Workspace::page($session, $ref); if (is_wp_error($document)) { return $document; } $content = $document['content']; if (!empty($session['drafts'][$ref])) { if ($session['drafts'][$ref]['baseHash'] !== $document['hash']) { return self::stale(); } $content = $session['drafts'][$ref]['content']; } return ['document' => $document, 'content' => $content, 'version' => hash('sha256', $content)]; } public static function stale(): WP_Error { return new WP_Error( 'apeari_stale_target', __( 'Źródło zaznaczonego elementu zmieniło się. Wczytaj podgląd i zaznacz element ponownie.', 'apeari', ), ['status' => 409], ); } private static function brief(array $block): array { return [ 'blockName' => $block['blockName'] ?: 'core/freeform', 'text' => mb_substr(wp_strip_all_tags($block['innerHTML']), 0, 400), ]; } public static function create(array $input) { $session_id = is_string($input['sessionId'] ?? null) ? $input['sessionId'] : ''; $ref = is_string($input['documentRef'] ?? null) ? $input['documentRef'] : ''; $snapshot = self::snapshot($session_id, $ref); if (is_wp_error($snapshot)) { return $snapshot; } if (($input['documentVersion'] ?? '') !== $snapshot['version']) { return self::stale(); } $path = $input['blockPath'] ?? null; if (!is_array($path)) { return self::invalid(); } $found = self::find($snapshot['content'], $path); if (is_wp_error($found)) { return $found; } $block = $found['block']; if (!in_array($block['blockName'], APEARI_Plans::BLOCKS, true)) { return self::invalid(); } foreach ($found['ancestors'] as $ancestor) { if ( !empty($ancestor['attrs']['templateLock']) || !empty($ancestor['attrs']['lock']['edit']) || !empty($ancestor['attrs']['metadata']['bindings']) ) { return self::invalid(); } } $placement = $input['placement'] ?? 'edit'; if (!in_array($placement, ['edit', 'before', 'after', 'inside'], true)) { return self::invalid(); } if ('inside' === $placement && !in_array($block['blockName'], APEARI_Plans::CONTAINERS, true)) { return new WP_Error( 'apeari_container', __( 'Wstawianie wewnątrz wymaga zaznaczenia grupy, kolumny albo innego kontenera Gutenberga.', 'apeari', ), ['status' => 422], ); } $dom = $input['element'] ?? []; if (!is_array($dom) || strlen(wp_json_encode($dom)) > 6000) { return self::invalid(); } $element = []; foreach (['tag', 'classes', 'path', 'text'] as $field) { if (isset($dom[$field]) && is_string($dom[$field])) { $element[$field] = sanitize_text_field(mb_substr($dom[$field], 0, 1200)); } } $source = serialize_block($block); $target = [ 'documentRef' => $ref, 'documentVersion' => $snapshot['version'], 'baseHash' => $snapshot['document']['hash'], 'blockPath' => $path, 'blockHash' => hash('sha256', $source), 'placement' => $placement, ]; $scope = self::VERSION . '|' . get_current_blog_id() . '|' . get_current_user_id() . '|' . get_stylesheet() . '|' . $session_id; $id = hash('sha256', $scope . '|' . wp_json_encode($target) . '|' . wp_json_encode($element)); $cached = get_transient('apeari_context_' . $id); APEARI_Privacy::track('apeari_context_' . $id); if (is_array($cached)) { return ['contextId' => $id, 'cacheHit' => true, 'target' => $target]; } $index = end($path); $context = [ 'schemaVersion' => self::VERSION, 'target' => $target, 'blockName' => $block['blockName'], 'attributes' => $block['attrs'], 'source' => mb_substr($source, 0, 16000), 'sourceTruncated' => mb_strlen($source) > 16000, 'element' => $element, 'ancestors' => array_map([self::class, 'brief'], array_slice($found['ancestors'], 0, -1)), 'allowedStyles' => APEARI_Providers::allowed_styles($block['blockName']), 'previous' => $index > 0 ? self::brief($found['siblings'][$index - 1]) : null, 'next' => isset($found['siblings'][$index + 1]) ? self::brief($found['siblings'][$index + 1]) : null, ]; if (strlen(wp_json_encode($context)) > 60000) { return new WP_Error('apeari_context_size', __('Zaznacz mniejszy fragment strony.', 'apeari'), [ 'status' => 413, ]); } set_transient( 'apeari_context_' . $id, [ 'user' => get_current_user_id(), 'theme' => get_stylesheet(), 'sessionId' => $session_id, 'context' => $context, ], self::TTL, ); return ['contextId' => $id, 'cacheHit' => false, 'target' => $target]; } public static function resolve(string $id) { if (!preg_match('/^[a-f0-9]{64}$/', $id)) { return self::invalid(); } $cached = get_transient('apeari_context_' . $id); if ( !is_array($cached) || $cached['user'] !== get_current_user_id() || $cached['theme'] !== get_stylesheet() ) { return new WP_Error( 'apeari_context_expired', __('Kontekst AI wygasł. Zaznacz element ponownie.', 'apeari'), [ 'status' => 410, ], ); } $target = $cached['context']['target']; $snapshot = self::snapshot($cached['sessionId'], $target['documentRef']); if (is_wp_error($snapshot)) { return $snapshot; } if ( $snapshot['version'] !== $target['documentVersion'] || $snapshot['document']['hash'] !== $target['baseHash'] ) { return self::stale(); } return $cached['context']; } }