PluginProbe
Extendify / trunk
Extendify vtrunk
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
← All changes | app/Agent/TemplatePartBlockFinder.php +162 -40 3.1.5 → trunk View file →
@@ -10,22 +10,153 @@
10 10 // Shared by SaveController (write) and WPController::getBlockCode (read) so the
11 11 // two halves can never resolve a different block for the same id.
12 12 class TemplatePartBlockFinder
13 13 {
14 - // Preorder walk matching TagTemplateParts numbering: nested
15 - // core/template-part blocks open a separate seq space (skipped), and the
16 - // shared $ignored dynamic blocks (mini-cart, loops, …) count as one leaf
17 - // each but are not descended into — the tagger skips their rendered subtree.
18 - //
19 - // Ref-based navigation blocks (`core/navigation` with a `ref` attr) resolve
20 - // their items at render time from a separate wp_navigation post.
21 - // TagTemplateParts fires for those rendered items inside the outer
22 - // template-part's frame, so each one increments the outer seq. parse_blocks
23 - // of the post we're walking only sees the navigation block (empty
24 - // innerBlocks for refs), so a naïve walk would under-count. Resolve the nav
25 - // ref here and add its block count to the running counter so blockIds
26 - // *after* the navigation (e.g. social-link in a sibling social-links block)
27 - // still line up.
14 + // These resolve by ref; template parts by slug.
15 + // phpcs:ignore PSR12.Properties.ConstantVisibility.NotFound
16 + const REF_POST_TYPES = [
17 + 'block' => 'wp_block',
18 + 'navigation' => 'wp_navigation',
19 + ];
20 +
21 + // phpcs:ignore PSR12.Properties.ConstantVisibility.NotFound
22 + const COMPOSITE_ID_PATTERN = '/^(block|navigation):(\d+):(\d+)$/';
23 +
24 + // phpcs:ignore PSR12.Properties.ConstantVisibility.NotFound
25 + const REF_BLOCKS = [
26 + 'core/block' => 'block',
27 + 'core/navigation' => 'navigation',
28 + ];
29 +
30 + // Both taggers emit this; this class parses it back.
31 + public static function refPrefix(string $type, array $block): string
32 + {
33 + return $type . ':' . (int) ($block['attrs']['ref'] ?? 0) . ':';
34 + }
35 +
36 + // An inline navigation keeps its items in the containing post.
37 + public static function isRefNav(array $block): bool
38 + {
39 + return ($block['blockName'] ?? '') === 'core/navigation'
40 + && !empty($block['attrs']['ref']);
41 + }
42 +
43 + // These live in their own post, so the id has to name that post.
44 + public static function parseCompositeId($blockId)
45 + {
46 + if (!preg_match(self::COMPOSITE_ID_PATTERN, (string) $blockId, $matches)) {
47 + return null;
48 + }
49 + return [
50 + 'postType' => self::REF_POST_TYPES[$matches[1]],
51 + 'ref' => (int) $matches[2],
52 + 'seq' => (int) $matches[3],
53 + ];
54 + }
55 +
56 + public static function owningPost($blockId, \WP_Post $container)
57 + {
58 + $composite = self::parseCompositeId($blockId);
59 + if ($composite === null) {
60 + return preg_match('/^\d+$/', (string) $blockId) && (int) $blockId > 0
61 + ? ['post' => $container, 'blockId' => (int) $blockId]
62 + : null;
63 + }
64 + $post = \get_post($composite['ref']);
65 + if (!$post || $post->post_type !== $composite['postType']) {
66 + return null;
67 + }
68 + if (!self::rendersRef($container, $composite['ref'])) {
69 + return null;
70 + }
71 + return ['post' => $post, 'blockId' => $composite['seq']];
72 + }
73 +
74 + // Without this, any wp_block on the site is writable from any page.
75 + public static function rendersRef(\WP_Post $container, int $ref): bool
76 + {
77 + return self::contentRendersRef($container->post_content, $ref);
78 + }
79 +
80 + // An untouched theme-file part has no post, so containment reads its markup.
81 + public static function contentRendersRef(string $content, int $ref): bool
82 + {
83 + $seen = [];
84 + return self::reachesRef(\parse_blocks($content), $ref, $seen);
85 + }
86 +
87 + private static function reachesRef(array $blocks, int $ref, array &$seen): bool
88 + {
89 + foreach ($blocks as $block) {
90 + if (!is_array($block)) {
91 + continue;
92 + }
93 + $name = $block['blockName'] ?? '';
94 + if (isset(self::REF_BLOCKS[$name])) {
95 + $blockRef = (int) ($block['attrs']['ref'] ?? 0);
96 + if ($blockRef === $ref) {
97 + return true;
98 + }
99 + if ($blockRef && self::reachesInside("ref:{$blockRef}", $ref, $seen)) {
100 + return true;
101 + }
102 + }
103 + if ($name === 'core/template-part' && !empty($block['attrs']['slug'])) {
104 + $slug = (string) $block['attrs']['slug'];
105 + if (self::reachesInside("part:{$slug}", $ref, $seen)) {
106 + return true;
107 + }
108 + }
109 + if (!empty($block['innerBlocks']) && self::reachesRef($block['innerBlocks'], $ref, $seen)) {
110 + return true;
111 + }
112 + }
113 + return false;
114 + }
115 +
116 + // A pattern can reference itself, directly or through a part.
117 + private static function reachesInside(string $key, int $ref, array &$seen): bool
118 + {
119 + if (isset($seen[$key])) {
120 + return false;
121 + }
122 + $seen[$key] = true;
123 + $content = self::contentFor($key);
124 + return $content === null
125 + ? false
126 + : self::reachesRef(\parse_blocks($content), $ref, $seen);
127 + }
128 +
129 + private static function contentFor(string $key)
130 + {
131 + list($kind, $value) = explode(':', $key, 2);
132 + if ($kind === 'ref') {
133 + $post = \get_post((int) $value);
134 + return $post ? $post->post_content : null;
135 + }
136 + $template = \get_block_template(
137 + \wp_get_theme()->get_stylesheet() . '//' . $value,
138 + 'wp_template_part'
139 + );
140 + return $template ? $template->content : null;
141 + }
142 +
143 + // Lets the render-time tagger number a ref'd post the way find() walks it.
144 + public static function outline(array $blocks): array
145 + {
146 + $max = 0;
147 + $visited = [];
148 + self::find($blocks, 0, $max, $visited);
149 + return $visited;
150 + }
151 +
152 + // Their content belongs to another post's id space.
153 + private static function opensOwnScope(string $name): bool
154 + {
155 + return in_array($name, ['core/template-part', 'core/block'], true);
156 + }
157 +
158 + // Diverging from TagTemplateParts' numbering here drifts every later id.
28 159 public static function find(
29 160 array $blocks,
30 161 int $targetId,
31 162 int &$maxCounter = 0,
@@ -46,9 +177,9 @@
46 177 $walk($block['innerBlocks'], array_merge($pathSoFar, [$i, 'innerBlocks']));
47 178 }
48 179 continue;
49 180 }
50 - if ($name === 'core/template-part') {
181 + if (self::opensOwnScope($name)) {
51 182 continue;
52 183 }
53 184 $counter++;
54 185 $visited[] = ['c' => $counter, 'n' => $name];
@@ -55,30 +186,12 @@
55 186 if ($counter === $targetId) {
56 187 $found = ['block' => $block, 'path' => array_merge($pathSoFar, [$i])];
57 188 return;
58 189 }
59 - // Dynamic self-rendering blocks (loops, woocommerce/mini-cart, …)
60 - // count as one leaf but are not descended into: TagTemplateParts
61 - // skips their render-injected subtree, so descending here would
62 - // drift every later id. Shares the one $ignored list.
190 + // The tagger skips their render-injected subtree.
63 191 if (in_array($name, TagTemplateParts::$ignored, true)) {
64 192 continue;
65 193 }
66 - if ($name === 'core/navigation' && !empty($block['attrs']['ref'])) {
67 - $navPost = get_post((int) $block['attrs']['ref']);
68 - if ($navPost) {
69 - $navBlocks = parse_blocks($navPost->post_content);
70 - $before = $counter;
71 - self::countBlocksPreorder($navBlocks, $counter);
72 - for ($j = $before + 1; $j <= $counter; $j++) {
73 - $visited[] = ['c' => $j, 'n' => '(nav-ref-item)'];
74 - }
75 - }
76 - // Ref navs have no innerBlocks in the parsed tree — items
77 - // live in the wp_navigation post and can't be replaced from
78 - // here anyway (WPNavigationController owns that).
79 - continue;
80 - }
81 194 if (!empty($block['innerBlocks'])) {
82 195 $walk($block['innerBlocks'], array_merge($pathSoFar, [$i, 'innerBlocks']));
83 196 }
84 197 }
@@ -89,27 +202,36 @@
89 202
90 203 return $found;
91 204 }
92 205
93 - private static function countBlocksPreorder(array $blocks, int &$counter)
206 + // Stamps in find()'s order so a splice resolves the id the client read.
207 + public static function stamp(array $blocks): array
94 208 {
95 - foreach ($blocks as $block) {
209 + $seq = 0;
210 + return self::stampWalk($blocks, $seq);
211 + }
212 +
213 + private static function stampWalk(array $list, int &$seq): array
214 + {
215 + foreach ($list as $i => $block) {
96 216 $name = $block['blockName'] ?? '';
97 217 if ($name === '') {
98 218 if (!empty($block['innerBlocks'])) {
99 - self::countBlocksPreorder($block['innerBlocks'], $counter);
219 + $list[$i]['innerBlocks'] = self::stampWalk($block['innerBlocks'], $seq);
100 220 }
101 221 continue;
102 222 }
103 - if ($name === 'core/template-part') {
223 + if (self::opensOwnScope($name)) {
104 224 continue;
105 225 }
106 - $counter++;
226 + $seq++;
227 + $list[$i][PostBlockFinder::REF_KEY] = $seq;
107 228 if (in_array($name, TagTemplateParts::$ignored, true)) {
108 229 continue;
109 230 }
110 231 if (!empty($block['innerBlocks'])) {
111 - self::countBlocksPreorder($block['innerBlocks'], $counter);
232 + $list[$i]['innerBlocks'] = self::stampWalk($block['innerBlocks'], $seq);
112 233 }
113 234 }
235 + return $list;
114 236 }
115 237 }