PluginProbe
Extendify / 2.2.0
Extendify v2.2.0
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 0.7.0 All 126 releases
extendify / app / Agent / Controllers / WPController.php

WPController.php in Extendify 2.2.0, at app/Agent/Controllers/WPController.php

351 lines 11.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * WP Controller
5 */
6
7 namespace Extendify\Agent\Controllers;
8
9 defined('ABSPATH') || die('No direct access.');
10
11 /**
12 * The controller for interacting with WordPress.
13 */
14
15 class WPController
16 {
17 /**
18 * $ignoredKeys are only removed top-level (line 94) and not recursively
19 *
20 * @var string[]
21 */
22 public static $ignoredKeys = [
23 'title',
24 '$schema',
25 'version',
26 'slug',
27 ];
28 /**
29 * Allowed variations for the extendable theme
30 *
31 * @var string[]
32 */
33 public static $allowedVariationsList = [
34 'bloom',
35 'brick',
36 'cobalt',
37 'coral',
38 'evergreen',
39 'gold',
40 'lilac',
41 'lime',
42 'midnight',
43 'moss',
44 'neon',
45 'rosewood',
46 'slate',
47 'onyx',
48 'glasgow',
49 'royal',
50 'obsidian',
51 ];
52
53 /**
54 * Recursively filter an array to include only specified properties.
55 *
56 * This function traverses the array structure and retains only the properties
57 * specified in the allowed keys, preserving the original hierarchical structure.
58 * Keys that don't match the allowed set are excluded from the result.
59 *
60 * @param array $data The input array to filter
61 * @param array $allowedKeys Associative array of allowed property keys (keys as indices)
62 * @return array Filtered array containing only allowed properties, maintaining structure
63 */
64 protected static function filterArrayByProperties(array $data, array $allowedKeys)
65 {
66 if (empty($allowedKeys) || empty($data)) {
67 return [];
68 }
69
70 $result = [];
71 foreach ($data as $key => $value) {
72 if (isset($allowedKeys[$key])) {
73 $result[$key] = $value;
74 } elseif (is_array($value)) {
75 // Recursively filter nested arrays
76 $filtered = self::filterArrayByProperties($value, $allowedKeys);
77 if (!empty($filtered)) {
78 $result[$key] = $filtered;
79 }
80 }
81 }
82 return $result;
83 }
84
85 /**
86 * Validates if a variation contains only specified properties.
87 *
88 * This function checks whether the variation array contains exclusively the
89 * specified properties throughout its entire hierarchy.
90 *
91 * @param array $variation The theme variation arrays to validate
92 * @param array $allowedKeys List of property names that should be the only ones present
93 * @return bool TRUE if only specified properties exist, FALSE otherwise
94 */
95 protected static function variationHasProperties(array $variation, array $allowedKeys)
96 {
97 if (empty($variation) || empty($allowedKeys)) {
98 return false;
99 }
100
101 $allowedKeys = array_flip($allowedKeys);
102 $data = array_diff_key($variation, array_flip(self::$ignoredKeys));
103 $filtered = self::filterArrayByProperties($data, $allowedKeys);
104
105 return serialize($filtered) === serialize($data);
106 }
107
108 /**
109 * Get the CSS for each variation.
110 *
111 * @param array $variations The theme variations to process.
112 * @param \WP_Theme_JSON $current The current theme JSON data.
113 * @param bool $includeLayoutStyles Whether to include layout styles in the CSS.
114 * @return array The variations with their corresponding CSS.
115 */
116 protected static function getCss($variations, $current, $includeLayoutStyles)
117 {
118 $deduped = [];
119 foreach ($variations as $variation) {
120 $title = $variation['title'] ?? null;
121 if (!$title || isset($deduped[$title])) {
122 continue;
123 }
124 $theme = new \WP_Theme_JSON();
125 $theme->merge($current);
126 $theme->merge(new \WP_Theme_JSON($variation));
127 $css = $theme->get_stylesheet(
128 ["variables", "styles", "presets"],
129 null,
130 ["skip_root_layout_styles" => !$includeLayoutStyles, 'include_block_style_variations' => true]
131 );
132 $variation['css'] = $css;
133 // to make sure we exit early
134 $deduped[$title] = $variation;
135 }
136
137 return array_values($deduped);
138 }
139
140 /**
141 * Get Theme Variations and the compiled CSS for each variation.
142 *
143 * @param \WP_REST_Request $request The REST API request object.
144 * @return \WP_REST_Response
145 */
146 public static function getVariations($request)
147 {
148 $includeLayoutStyles = $request->has_param('includeLayoutStyles');
149 $current = \WP_Theme_JSON_Resolver::get_merged_data();
150 $unfiltered = \WP_Theme_JSON_Resolver::get_style_variations();
151
152 $variations = array_filter($unfiltered, function ($variation) {
153 return self::variationHasProperties($variation, ['color']);
154 });
155
156 $buildSlugMap = function ($unfiltered) {
157 $slugMap = [];
158
159 if (!is_array($unfiltered)) {
160 return $slugMap;
161 }
162
163 foreach ($unfiltered as $rawSlug => $rawVariation) {
164 $title = is_array($rawVariation) ? ($rawVariation['title'] ?? null) : null;
165 $slug = is_array($rawVariation)
166 ? ($rawVariation['slug'] ?? (is_string($rawSlug) ? $rawSlug : null))
167 : null;
168
169 if ($title && $slug && !isset($slugMap[$title])) {
170 $slugMap[$title] = $slug;
171 }
172 }
173 return $slugMap;
174 };
175 $slugMap = $buildSlugMap($unfiltered);
176 array_walk($variations, function (&$variation) use ($slugMap) {
177 if (!is_array($variation) || isset($variation['slug'])) {
178 return;
179 }
180
181 $title = $variation['title'] ?? null;
182 if ($title && isset($slugMap[$title])) {
183 $variation['slug'] = $slugMap[$title];
184 }
185 });
186
187 $deduped = static::getCss($variations, $current, $includeLayoutStyles);
188 // if the theme is extendable we need to filter the variations using the allowed variations list
189 if (\get_option('stylesheet') === 'extendable') {
190 $deduped = array_filter($deduped, function ($variation) {
191 return in_array($variation['slug'], self::$allowedVariationsList);
192 });
193 }
194
195
196 return new \WP_REST_Response(array_values($deduped));
197 }
198
199 /**
200 * Get Theme fonts Variations and the compiled CSS for each variation.
201 *
202 * @param \WP_REST_Request $request The REST API request object.
203 * @return \WP_REST_Response
204 */
205 public static function getFontsVariations($request)
206 {
207 $includeLayoutStyles = $request->has_param('includeLayoutStyles');
208 $current = \WP_Theme_JSON_Resolver::get_merged_data();
209 $unfiltered = \WP_Theme_JSON_Resolver::get_style_variations();
210
211 $fontsVariations = array_filter($unfiltered, function ($variation) {
212 return self::variationHasProperties($variation, ['elements', 'typography']);
213 });
214
215 $processedFonts = array_map(function ($variation) {
216 if (!isset($variation['styles']['elements']) || !is_array($variation['styles']['elements'])) {
217 return $variation;
218 }
219
220 $variation['styles']['elements'] = array_map(
221 [self::class, 'normalizeElementTypography'],
222 $variation['styles']['elements']
223 );
224
225 if (!isset($variation['styles']['typography'])) {
226 $variation['styles']['typography'] = [
227 'fontFamily' => 'var(--wp--preset--font-family--inter)'
228 ];
229 }
230
231 // Removing the settings that cause the style to change.
232 unset($variation['settings']);
233
234 return $variation;
235 }, $fontsVariations);
236
237 $deduped = static::getCss($processedFonts, $current, $includeLayoutStyles);
238 return new \WP_REST_Response($deduped);
239 }
240
241 /**
242 * Normalize typography properties for theme element styles.
243 *
244 * @param array $elementStyles The element styles array containing typography configuration
245 * @return array Normalized typography properties with filtered null values
246 */
247 protected static function normalizeElementTypography(array $elementStyles)
248 {
249 $typography = $elementStyles['typography'] ?? [];
250
251 return [
252 'typography' => array_filter([
253 'fontFamily' => $typography['fontFamily'] ?? null,
254 'fontSize' => $typography['fontSize'] ?? null,
255 'lineHeight' => $typography['lineHeight'] ?? null,
256 'letterSpacing' => $typography['letterSpacing'] ?? null,
257 'fontStyle' => $typography['fontStyle'] ?? null,
258 'fontWeight' => $typography['fontWeight'] ?? null,
259 'textTransform' => $typography['textTransform'] ?? 'none',
260 ], function ($v) {
261 return $v !== null;
262 })
263 ];
264 }
265
266
267 /**
268 * Get the HTML of a specific tagged block code
269 *
270 * @param \WP_REST_Request $request The REST API request object.
271 * @return \WP_REST_Response
272 */
273 public static function getBlockCode(\WP_REST_Request $request)
274 {
275 $blockId = (int) $request->get_param('blockId');
276 $postId = (int) $request->get_param('postId');
277
278 if ($blockId < 1) {
279 return new \WP_REST_Response(['error' => 'Invalid blockId'], 400);
280 }
281
282 $post = \get_post($postId);
283 if (!$post) {
284 return new \WP_REST_Response(['error' => 'Post not found'], 404);
285 }
286
287 $ignored = ['core/query', 'core/post-template', 'core/post-content'];
288
289 $ast = array_values(array_filter(
290 parse_blocks($post->post_content),
291 static fn ($b) => is_array($b) && !empty($b['blockName'])
292 ));
293
294 $seq = 0;
295 $found = null;
296
297 $walk = function (array $list) use (&$walk, &$seq, $blockId, &$found, $ignored) {
298 foreach ($list as $b) {
299 $name = $b['blockName'] ?? null;
300 if (!$name) {
301 continue;
302 }
303
304 // Ignore this block and its subtree (matches tagger behavior)
305 if (in_array($name, $ignored, true)) {
306 continue; // do NOT increment seq, do NOT traverse children
307 }
308
309 $seq++;
310 if ($seq === $blockId) {
311 $found = $b;
312 return true;
313 }
314
315 if (!empty($b['innerBlocks']) && $walk($b['innerBlocks'])) {
316 return true;
317 }
318 }
319 return false;
320 };
321 $walk($ast);
322
323 if (!is_array($found) || empty($found['blockName'])) {
324 return new \WP_REST_Response(['error' => 'Block id not found in this post'], 404);
325 }
326
327 return new \WP_REST_Response([
328 'postId' => $postId,
329 'blockId' => $blockId,
330 'name' => $found['blockName'],
331 'attrs' => $found['attrs'] ?? (object)[],
332 'block' => serialize_blocks([$found]),
333 ], 200);
334 }
335
336
337 /**
338 * Get the rendered HTML of some block code
339 *
340 * @param \WP_REST_Request $request The REST API request object.
341 * @return \WP_REST_Response
342 */
343 public static function getBlockHtml($request)
344 {
345 $blockCode = $request->get_param('blockCode');
346 $content = \do_blocks($blockCode);
347
348 return new \WP_REST_Response(['content' => trim($content)]);
349 }
350 }
351