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
extendify / app / Agent / TemplatePartBlockFinder.php

TemplatePartBlockFinder.php in Extendify 3.2.1, at app/Agent/TemplatePartBlockFinder.php

238 lines 8.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Extendify\Agent;
4
5 defined('ABSPATH') || die('No direct access.');
6
7 // Resolves a block inside a template-part by the same preorder numbering
8 // TagTemplateParts assigns at render time, so a client blockId (read off a
9 // data-extendify-part-block-id attribute) maps back to the parsed block.
10 // Shared by SaveController (write) and WPController::getBlockCode (read) so the
11 // two halves can never resolve a different block for the same id.
12 class TemplatePartBlockFinder
13 {
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.
159 public static function find(
160 array $blocks,
161 int $targetId,
162 int &$maxCounter = 0,
163 array &$visited = []
164 ) {
165 $counter = 0;
166 $found = null;
167
168 $walk = function (array &$list, array $pathSoFar)
169 use (&$walk, &$counter, &$found, $targetId, &$visited) {
170 foreach ($list as $i => &$block) {
171 if ($found !== null) {
172 return;
173 }
174 $name = $block['blockName'] ?? '';
175 if ($name === '') {
176 if (!empty($block['innerBlocks'])) {
177 $walk($block['innerBlocks'], array_merge($pathSoFar, [$i, 'innerBlocks']));
178 }
179 continue;
180 }
181 if (self::opensOwnScope($name)) {
182 continue;
183 }
184 $counter++;
185 $visited[] = ['c' => $counter, 'n' => $name];
186 if ($counter === $targetId) {
187 $found = ['block' => $block, 'path' => array_merge($pathSoFar, [$i])];
188 return;
189 }
190 // The tagger skips their render-injected subtree.
191 if (in_array($name, TagTemplateParts::$ignored, true)) {
192 continue;
193 }
194 if (!empty($block['innerBlocks'])) {
195 $walk($block['innerBlocks'], array_merge($pathSoFar, [$i, 'innerBlocks']));
196 }
197 }
198 unset($block);
199 };
200 $walk($blocks, []);
201 $maxCounter = $counter;
202
203 return $found;
204 }
205
206 // Stamps in find()'s order so a splice resolves the id the client read.
207 public static function stamp(array $blocks): array
208 {
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) {
216 $name = $block['blockName'] ?? '';
217 if ($name === '') {
218 if (!empty($block['innerBlocks'])) {
219 $list[$i]['innerBlocks'] = self::stampWalk($block['innerBlocks'], $seq);
220 }
221 continue;
222 }
223 if (self::opensOwnScope($name)) {
224 continue;
225 }
226 $seq++;
227 $list[$i][PostBlockFinder::REF_KEY] = $seq;
228 if (in_array($name, TagTemplateParts::$ignored, true)) {
229 continue;
230 }
231 if (!empty($block['innerBlocks'])) {
232 $list[$i]['innerBlocks'] = self::stampWalk($block['innerBlocks'], $seq);
233 }
234 }
235 return $list;
236 }
237 }
238