PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
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.63.2.1 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 }