Files
WP-LIT-MCP/src/Integrations/Divi/BlockTree/NodeIndex.php
T
janniclas.loosenandClaude Opus 5.5 b24eb6c3e9 fix(security): no direct HTTP access to plugin PHP files (audit L2, cheap part)
Every file under src/ exits outside WordPress (defined('ABSPATH') ||
exit; after the use block, so a direct request no longer prints a fatal
error with the server path); divi-convert-fixture.php gets the WP_CLI
guard the other integration scripts have. A unit test keeps both true;
PSR1 side-effects warning excluded for the guard.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
2026-09-26 19:32:12 +02:00

149 lines
4.6 KiB
PHP
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.
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\BlockTree;
defined('ABSPATH') || exit;
/**
* Path IDs of a Document (spec §5.3): s0, s0.r1, s0.r1.c0.m2. One letter per role –
* s section, r row / row-inner, c column / column-inner, m module (incl. group and child
* items), g global module or global layout, x non-Divi block – numbered per parent and letter.
* divi/placeholder wrappers are transparent (their children count as top level). Opaque and
* non-Divi blocks get an ID but are not descended into. IDs are only valid for the tree_rev
* they were read with; build a new index after every change.
*/
final class NodeIndex
{
public const ID_PATTERN = '/^[srcmgx]\d+(\.[srcmgx]\d+)*$/D';
/** @var array<string, Block> */
private array $byId = [];
/** @var array<int, string> spl_object_id(block) => id */
private array $ids = [];
/** @var array<int, Container> spl_object_id(block) => container holding it */
private array $parents = [];
/** @var list<array{id: string, block: Block, depth: int}> */
private array $entries = [];
private function __construct(private Container $root)
{
}
public static function build(Document $document): self
{
$root = $document;
foreach ($document->children() as $block) {
if ($block->isPlaceholder()) {
$root = $block;
break;
}
}
$index = new self($root);
$counters = [];
$index->walk($document, '', 0, $counters);
return $index;
}
public static function letter(Block $block): string
{
if ($block->isGlobal()) {
return 'g';
}
return match ($block->name()) {
'divi/section' => 's',
'divi/row', 'divi/row-inner' => 'r',
'divi/column', 'divi/column-inner' => 'c',
default => $block->isDivi() ? 'm' : 'x',
};
}
/** Container that receives new top-level sections: the first divi/placeholder, else the document. */
public function root(): Container
{
return $this->root;
}
public function find(string $id): ?Block
{
return $this->byId[$id] ?? null;
}
public function idOf(Block $block): ?string
{
return $this->ids[spl_object_id($block)] ?? null;
}
public function parentOf(Block $block): ?Container
{
return $this->parents[spl_object_id($block)] ?? null;
}
/** ID of the parent node, null for top-level nodes (their container is the root or a placeholder). */
public function parentId(Block $block): ?string
{
$parent = $this->parentOf($block);
return $parent instanceof Block ? $this->idOf($parent) : null;
}
/** The parent node of $block; null at the top level (the root or a placeholder holds it). */
public function parentBlock(Block $block): ?Block
{
$parent = $this->parentOf($block);
return $parent instanceof Block && ! $parent->isPlaceholder() ? $parent : null;
}
/** Admin label of the section around $block (or $block itself), for automatic labels ("" when none). */
public function sectionLabel(?Block $block): string
{
for ($node = $block; $node !== null; $node = $this->parentBlock($node)) {
if ($node->name() === 'divi/section') {
return NodeSummary::label($node);
}
}
return '';
}
/** @return list<array{id: string, block: Block, depth: int}> In document order. */
public function entries(): array
{
return $this->entries;
}
/** @param array<string, int> $counters */
private function walk(Container $container, string $prefix, int $depth, array &$counters): void
{
foreach ($container->children() as $block) {
if ($prefix === '' && $block->isPlaceholder()) {
$this->walk($block, '', $depth, $counters);
continue;
}
$letter = self::letter($block);
$number = $counters[$letter] ?? 0;
$counters[$letter] = $number + 1;
$id = ($prefix === '' ? '' : $prefix . '.') . $letter . $number;
$this->byId[$id] = $block;
$this->ids[spl_object_id($block)] = $id;
$this->parents[spl_object_id($block)] = $container;
$this->entries[] = ['id' => $id, 'block' => $block, 'depth' => $depth];
if ($block->isDivi() && ! $block->isOpaque()) {
$childCounters = [];
$this->walk($block, $id, $depth + 1, $childCounters);
}
}
}
}