PluginProbe
Extendify / 3.1.4
Extendify v3.1.4
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 / Controllers / WPController.php

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

799 lines 27.2 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 use Extendify\Constants;
12 use Extendify\Shared\Services\Sanitizer;
13
14 /**
15 * The controller for interacting with WordPress.
16 */
17
18 class WPController
19 {
20 /**
21 * $ignoredKeys are only removed top-level (line 94) and not recursively
22 *
23 * @var string[]
24 */
25 public static $ignoredKeys = [
26 'title',
27 '$schema',
28 'version',
29 'slug',
30 ];
31 /**
32 * Allowed variations for the extendable theme
33 *
34 * @var string[]
35 */
36 public static $allowedVariationsList = [
37 'bloom',
38 'brick',
39 'cobalt',
40 'coral',
41 'evergreen',
42 'gold',
43 'lilac',
44 'lime',
45 'midnight',
46 'moss',
47 'neon',
48 'rosewood',
49 'slate',
50 'onyx',
51 'glasgow',
52 'royal',
53 'obsidian',
54 ];
55
56 /**
57 * Recursively filter an array to include only specified properties.
58 *
59 * This function traverses the array structure and retains only the properties
60 * specified in the allowed keys, preserving the original hierarchical structure.
61 * Keys that don't match the allowed set are excluded from the result.
62 *
63 * @param array $data The input array to filter
64 * @param array $allowedKeys Associative array of allowed property keys (keys as indices)
65 * @return array Filtered array containing only allowed properties, maintaining structure
66 */
67 protected static function filterArrayByProperties(array $data, array $allowedKeys)
68 {
69 if (empty($allowedKeys) || empty($data)) {
70 return [];
71 }
72
73 $result = [];
74 foreach ($data as $key => $value) {
75 if (isset($allowedKeys[$key])) {
76 $result[$key] = $value;
77 } elseif (is_array($value)) {
78 // Recursively filter nested arrays
79 $filtered = self::filterArrayByProperties($value, $allowedKeys);
80 if (!empty($filtered)) {
81 $result[$key] = $filtered;
82 }
83 }
84 }
85 return $result;
86 }
87
88 /**
89 * Validates if a variation contains only specified properties.
90 *
91 * This function checks whether the variation array contains exclusively the
92 * specified properties throughout its entire hierarchy.
93 *
94 * @param array $variation The theme variation arrays to validate
95 * @param array $allowedKeys List of property names that should be the only ones present
96 * @return bool TRUE if only specified properties exist, FALSE otherwise
97 */
98 protected static function variationHasProperties(array $variation, array $allowedKeys)
99 {
100 if (empty($variation) || empty($allowedKeys)) {
101 return false;
102 }
103
104 $allowedKeys = array_flip($allowedKeys);
105 $data = array_diff_key($variation, array_flip(self::$ignoredKeys));
106 $filtered = self::filterArrayByProperties($data, $allowedKeys);
107
108 return serialize($filtered) === serialize($data);
109 }
110
111 /**
112 * Get the CSS for each variation.
113 *
114 * @param array $variations The theme variations to process.
115 * @param \WP_Theme_JSON $current The current theme JSON data.
116 * @param bool $includeLayoutStyles Whether to include layout styles in the CSS.
117 * @return array The variations with their corresponding CSS.
118 */
119 protected static function getCss($variations, $current, $includeLayoutStyles)
120 {
121 $deduped = [];
122 foreach ($variations as $variation) {
123 $title = $variation['title'] ?? null;
124 if (!$title || isset($deduped[$title])) {
125 continue;
126 }
127 $theme = new \WP_Theme_JSON();
128 $theme->merge($current);
129 $theme->merge(new \WP_Theme_JSON($variation));
130 $css = $theme->get_stylesheet(
131 ["variables", "styles", "presets"],
132 null,
133 ["skip_root_layout_styles" => !$includeLayoutStyles, 'include_block_style_variations' => true]
134 );
135 $variation['css'] = $css;
136 // to make sure we exit early
137 $deduped[$title] = $variation;
138 }
139
140 return array_values($deduped);
141 }
142
143 /**
144 * Get Theme Variations and the compiled CSS for each variation.
145 *
146 * @param \WP_REST_Request $request The REST API request object.
147 * @return \WP_REST_Response
148 */
149 public static function getVariations($request)
150 {
151 $includeLayoutStyles = $request->has_param('includeLayoutStyles');
152 $current = \WP_Theme_JSON_Resolver::get_merged_data();
153 $unfiltered = \WP_Theme_JSON_Resolver::get_style_variations();
154
155 $variations = array_filter($unfiltered, function ($variation) {
156 return self::variationHasProperties($variation, ['color']);
157 });
158
159 $buildSlugMap = function ($unfiltered) {
160 $slugMap = [];
161
162 if (!is_array($unfiltered)) {
163 return $slugMap;
164 }
165
166 foreach ($unfiltered as $rawSlug => $rawVariation) {
167 $title = is_array($rawVariation) ? ($rawVariation['title'] ?? null) : null;
168 $slug = is_array($rawVariation)
169 ? ($rawVariation['slug'] ?? (is_string($rawSlug) ? $rawSlug : null))
170 : null;
171
172 if ($title && $slug && !isset($slugMap[$title])) {
173 $slugMap[$title] = $slug;
174 }
175 }
176 return $slugMap;
177 };
178 $slugMap = $buildSlugMap($unfiltered);
179 array_walk($variations, function (&$variation) use ($slugMap) {
180 if (!is_array($variation) || isset($variation['slug'])) {
181 return;
182 }
183
184 $title = $variation['title'] ?? null;
185 if ($title && isset($slugMap[$title])) {
186 $variation['slug'] = $slugMap[$title];
187 }
188 });
189
190 $deduped = static::getCss($variations, $current, $includeLayoutStyles);
191 // if the theme is extendable we need to filter the variations using the allowed variations list
192 if (\get_option('stylesheet') === 'extendable') {
193 $deduped = array_filter($deduped, function ($variation) {
194 return in_array($variation['slug'], self::$allowedVariationsList);
195 });
196 }
197
198
199 return new \WP_REST_Response(array_values($deduped));
200 }
201
202 /**
203 * Get Theme fonts Variations and the compiled CSS for each variation.
204 *
205 * @param \WP_REST_Request $request The REST API request object.
206 * @return \WP_REST_Response
207 */
208 public static function getFontsVariations($request)
209 {
210 $includeLayoutStyles = $request->has_param('includeLayoutStyles');
211 $current = \WP_Theme_JSON_Resolver::get_merged_data();
212 $unfiltered = \WP_Theme_JSON_Resolver::get_style_variations();
213
214 $fontsVariations = array_filter($unfiltered, function ($variation) {
215 return self::variationHasProperties($variation, ['elements', 'typography']);
216 });
217
218 $processedFonts = array_map(function ($variation) {
219 if (!isset($variation['styles']['elements']) || !is_array($variation['styles']['elements'])) {
220 return $variation;
221 }
222
223 $variation['styles']['elements'] = array_map(
224 [self::class, 'normalizeElementTypography'],
225 $variation['styles']['elements']
226 );
227
228 if (!isset($variation['styles']['typography'])) {
229 $variation['styles']['typography'] = [
230 'fontFamily' => 'var(--wp--preset--font-family--inter)'
231 ];
232 }
233
234 // Removing the settings that cause the style to change.
235 unset($variation['settings']);
236
237 return $variation;
238 }, $fontsVariations);
239
240 $deduped = static::getCss($processedFonts, $current, $includeLayoutStyles);
241 return new \WP_REST_Response($deduped);
242 }
243
244 /**
245 * Get block style variations (vibes) from merged global styles
246 *
247 * @param \WP_REST_Request $request The request.
248 * @return \WP_REST_Response
249 */
250 public static function getBlockStyleVariations($request)
251 {
252 // Get theme + DB merged Global Styles
253 $merged = wp_get_global_styles();
254 $blocks = $merged['blocks'] ?? [];
255
256 $variations = [];
257
258 foreach ($blocks as $blockName => $blockData) {
259 if (!isset($blockData['variations'])) {
260 continue;
261 }
262
263 $variations[$blockName] = $blockData['variations'];
264 }
265
266 return new \WP_REST_Response($variations, 200);
267 }
268
269 /**
270 * Normalize typography properties for theme element styles.
271 *
272 * @param array $elementStyles The element styles array containing typography configuration
273 * @return array Normalized typography properties with filtered null values
274 */
275 protected static function normalizeElementTypography(array $elementStyles)
276 {
277 $typography = $elementStyles['typography'] ?? [];
278
279 return [
280 'typography' => array_filter([
281 'fontFamily' => $typography['fontFamily'] ?? null,
282 'fontSize' => $typography['fontSize'] ?? null,
283 'lineHeight' => $typography['lineHeight'] ?? null,
284 'letterSpacing' => $typography['letterSpacing'] ?? null,
285 'fontStyle' => $typography['fontStyle'] ?? null,
286 'fontWeight' => $typography['fontWeight'] ?? null,
287 'textTransform' => $typography['textTransform'] ?? 'none',
288 ], function ($v) {
289 return $v !== null;
290 })
291 ];
292 }
293
294
295 /**
296 * Get the HTML of a specific tagged block code
297 *
298 * @param \WP_REST_Request $request The REST API request object.
299 * @return \WP_REST_Response
300 */
301 public static function getBlockCode(\WP_REST_Request $request)
302 {
303 $blockId = (int) $request->get_param('blockId');
304
305 if ($blockId < 1) {
306 return new \WP_REST_Response(['error' => 'Invalid blockId'], 400);
307 }
308
309 // A template-part source resolves the part and walks it with the shared
310 // preorder finder, instead of the post path below.
311 $partSlug = (string) $request->get_param('partSlug');
312 if ($partSlug !== '') {
313 return self::getTemplatePartBlockCode($partSlug, $blockId);
314 }
315
316 $postId = (int) $request->get_param('postId');
317 $post = \get_post($postId);
318 if (!$post) {
319 return new \WP_REST_Response(['error' => 'Post not found'], 404);
320 }
321
322 $found = \Extendify\Agent\PostBlockFinder::find(parse_blocks($post->post_content), $blockId);
323
324 if (!$found || empty($found['block']['blockName'])) {
325 return new \WP_REST_Response(['error' => 'Block id not found in this post'], 404);
326 }
327
328 $block = $found['block'];
329 return new \WP_REST_Response([
330 'postId' => $postId,
331 'blockId' => $blockId,
332 'name' => $block['blockName'],
333 'attrs' => $block['attrs'] ?? (object)[],
334 'block' => serialize_blocks([$block]),
335 ], 200);
336 }
337
338 // Resolves the part the same way SaveController does — active-theme-scoped
339 // get_block_template, not a raw get_posts by name — then walks it with the
340 // shared preorder finder so the blockId lands on the block save will write.
341 private static function getTemplatePartBlockCode(string $slug, int $blockId)
342 {
343 $stylesheet = wp_get_theme()->get_stylesheet();
344 $template = get_block_template("{$stylesheet}//{$slug}", 'wp_template_part');
345 if (!$template || empty($template->wp_id)) {
346 return new \WP_REST_Response(['error' => 'Template part not found'], 404);
347 }
348
349 $post = \get_post($template->wp_id);
350 if (!$post) {
351 return new \WP_REST_Response(['error' => 'Template part not found'], 404);
352 }
353
354 $found = \Extendify\Agent\TemplatePartBlockFinder::find(
355 parse_blocks($post->post_content),
356 $blockId
357 );
358 if (!is_array($found) || empty($found['block']['blockName'])) {
359 return new \WP_REST_Response(['error' => 'Block id not found in this template part'], 404);
360 }
361
362 $block = $found['block'];
363 return new \WP_REST_Response([
364 'partSlug' => $slug,
365 'blockId' => $blockId,
366 'name' => $block['blockName'],
367 'attrs' => $block['attrs'] ?? (object)[],
368 'block' => serialize_blocks([$block]),
369 ], 200);
370 }
371
372 /**
373 * Get the rendered HTML of some block code
374 *
375 * @param \WP_REST_Request $request The REST API request object.
376 * @return \WP_REST_Response
377 */
378 public static function getBlockHtml($request)
379 {
380 $blockCode = $request->get_param('blockCode');
381 $content = \do_blocks($blockCode);
382
383 // Layout supports register per-container CSS for a page-side enqueue
384 // that never happens on a REST fragment — ship it with the markup.
385 $styles = function_exists('wp_style_engine_get_stylesheet_from_context')
386 ? \wp_style_engine_get_stylesheet_from_context('block-supports')
387 : '';
388
389 return new \WP_REST_Response(['content' => trim($content), 'styles' => $styles]);
390 }
391
392 /**
393 * Get the Hero Patterns from the API
394 *
395 * @param string $title The title to replace in the pattern code
396 * @param string $description The description to replace in the pattern code
397 * @param array $images The images to replace in the pattern code, as an array of urls
398 * @param array $cta The cta to replace in the pattern code, as an array with 'label' and 'link' keys
399 * @param bool $featuredOnly Whether to limit to featured patterns
400 * @param string|null $source What triggered the request, e.g. 'change-site-design-workflow'
401 * @return array|\WP_Error|array<string|int, mixed> The hero patterns data or a WP_Error on failure
402 */
403 protected static function getHeroPatternsData(
404 $title,
405 $description,
406 $images,
407 $cta,
408 $featuredOnly = false,
409 $source = null
410 ) {
411 $response = \wp_remote_post(
412 Constants::PATTERNS_HOST . '/api/heros',
413 [
414 'headers' => [
415 'Content-Type' => 'application/json',
416 'Accept' => 'application/json',
417 ],
418 'body' => wp_json_encode([
419 "wpVersion" => \get_bloginfo('version'),
420 "wpLanguage" => \get_locale(),
421 "featured" => $featuredOnly,
422 "source" => $source,
423 ])
424 ]
425 );
426
427 if (\is_wp_error($response)) {
428 return $response;
429 }
430
431 $body = json_decode(\wp_remote_retrieve_body($response), true);
432
433 $cursor = 0;
434 $imageCount = count($images);
435 $heroPatterns = [];
436
437 foreach ($body as $heroPattern) {
438 $code = $heroPattern['code'] ?? '';
439
440 if ($title) {
441 $code = preg_replace(
442 '/(<!-- wp:heading[^>]*-->[\s\S]*?<h1[^>]*>)[\s\S]*?(<\/h1>[\s\S]*?<!-- \/wp:heading -->)/m',
443 '${1}' . esc_html($title) . '${2}',
444 $code,
445 1
446 );
447 }
448
449 if ($description) {
450 $code = preg_replace(
451 '/(<!-- wp:paragraph[^>]*-->[\s\S]*?<p[^>]*>)[\s\S]*?(<\/p>[\s\S]*?<!-- \/wp:paragraph -->)/m',
452 '${1}' . esc_html($description) . '${2}',
453 $code,
454 1
455 );
456 }
457
458 if ($cta['label'] ?? null) {
459 $code = preg_replace(
460 '/(<!-- wp:button[^>]*-->[\s\S]*?<a[^>]*>)[\s\S]*?(<\/a>[\s\S]*?<!-- \/wp:button -->)/m',
461 '${1}' . esc_html($cta['label']) . '${2}',
462 $code,
463 1
464 );
465 }
466
467 if ($cta['link'] ?? null) {
468 $code = preg_replace(
469 '/(<!-- wp:button[\s\S]*?<a[^>]*\shref=")[^"]*(")/m',
470 '${1}' . esc_url($cta['link']) . '${2}',
471 $code,
472 1
473 );
474 }
475
476 $patternUrls = $heroPattern['urls'] ?? [];
477 foreach ($patternUrls as $key => $url) {
478 if (!$imageCount) {
479 break;
480 }
481 $code = str_replace($url, $images[($cursor + $key) % $imageCount], $code);
482 }
483 $cursor = $imageCount ? ($cursor + count($patternUrls)) % $imageCount : 0;
484
485 $renderedHtml = do_blocks(str_replace('ext-animate--on', '', $code));
486
487 $blockSupportsCss = function_exists('wp_style_engine_get_stylesheet_from_context')
488 ? wp_style_engine_get_stylesheet_from_context('block-supports')
489 : '';
490
491 // Clear block-supports store so CSS doesn't accumulate across patterns.
492 \WP_Style_Engine_CSS_Rules_Store::remove_all_stores();
493
494 $linkStyles = array_values(
495 array_filter(
496 array_map(
497 function ($style) {
498 return wp_styles()->registered[$style]->src ?? null;
499 },
500 wp_styles()->queue ?? []
501 )
502 )
503 );
504
505 /**
506 * Clear queue for the next pattern.
507 *
508 * `do_blocks` appends to the global queue the styles needed for the blocks.
509 */
510 wp_styles()->queue = [];
511
512 $heroPatterns[] = [
513 'id' => $heroPattern['id'],
514 'name' => $heroPattern['name'],
515 'code' => $code,
516 'renderedHtml' => $renderedHtml,
517 'blockSupportsCss' => $blockSupportsCss,
518 'linkStyles' => $linkStyles,
519 ];
520 }
521
522 return $heroPatterns;
523 }
524
525 /**
526 * Get the Hero Patterns
527 *
528 * @param \WP_REST_Request $request The REST API request object.
529 * @return \WP_REST_Response
530 */
531 public static function getHeroPatterns(\WP_REST_Request $request)
532 {
533 $title = $request->get_param('title');
534 $description = $request->get_param('description');
535 $images = $request->get_param('images');
536 $cta = $request->get_param('cta');
537
538 $heroPatterns = self::getHeroPatternsData($title, $description, $images, $cta);
539
540 if (\is_wp_error($heroPatterns)) {
541 return new \WP_REST_Response([], 500);
542 }
543
544 $blockEditorContext = new \WP_Block_Editor_Context(array( 'name' => 'core/edit-post' ));
545 $editorSettings = get_block_editor_settings([], $blockEditorContext);
546
547 return new \WP_REST_Response(['patterns' => $heroPatterns, 'blockEditorSettings' => $editorSettings ?? null]);
548 }
549
550 /**
551 * Build the image list for the hero section.
552 *
553 * Returns up to 6 images: unused site images first, already-used images last.
554 * When already at capacity, reverses the array so the last-used image leads next time.
555 */
556 protected static function getHeroSectionImages(array $images, array $siteImages, int $postId): array
557 {
558 $maxSlots = 6;
559
560 if (count($images) >= $maxSlots) {
561 return array_reverse($images);
562 }
563
564 if (!$postId || empty($siteImages)) {
565 return $images;
566 }
567
568 $usedImages = self::resolveUsedImages($postId);
569 $siteImages = array_map(function ($url) {
570 return self::stripQueryString($url);
571 }, $siteImages);
572
573 $unusedSiteImages = array_values(array_filter(
574 $siteImages,
575 function ($url) use ($usedImages, $images) {
576 return !in_array($url, $usedImages, true) && !in_array($url, $images, true);
577 }
578 ));
579
580 $unusedSlots = $maxSlots - count($images);
581 return array_merge(array_slice($unusedSiteImages, 0, $unusedSlots), $images);
582 }
583
584 protected static function stripQueryString(string $url): string
585 {
586 $parsed = wp_parse_url($url);
587 return ($parsed['scheme'] ?? 'https') . '://' . ($parsed['host'] ?? '') . ($parsed['path'] ?? '');
588 }
589
590 /**
591 * Get all Unsplash URLs used in a post.
592 */
593 protected static function resolveUsedImages(int $postId): array
594 {
595 $post = get_post($postId);
596 if (!$post) {
597 return [];
598 }
599
600 $processor = new \WP_HTML_Tag_Processor($post->post_content);
601 $usedImages = [];
602
603 while ($processor->next_tag('img')) {
604 $src = $processor->get_attribute('src');
605 if (!$src) {
606 continue;
607 }
608
609 $baseUrl = self::stripQueryString($src);
610
611 if (str_contains($src, 'unsplash.com')) {
612 $usedImages[] = $baseUrl;
613 continue;
614 }
615
616 $attachmentId = attachment_url_to_postid(esc_url($baseUrl));
617
618 if (!$attachmentId) {
619 continue;
620 }
621
622 $sourceUrl = get_post_meta($attachmentId, '_extendify_source_url', true);
623
624 if (!$sourceUrl) {
625 continue;
626 }
627
628 $usedImages[] = self::stripQueryString($sourceUrl);
629 }
630
631 return array_values(array_unique($usedImages));
632 }
633
634 public static function getSiteDesignVariations(\WP_REST_Request $request)
635 {
636 $title = $request->get_param('title');
637 $description = $request->get_param('description');
638 $images = $request->get_param('images') ?? [];
639 $cta = $request->get_param('cta');
640 $featuredOnly = true; // Only show featured patterns
641 $currentHeroPattern = $request->get_param('currentHeroPattern');
642 $postId = (int) $request->get_param('postId');
643 $siteImages = $request->get_param('siteImages') ?? [];
644 $source = $request->get_param('source');
645
646 $images = self::getHeroSectionImages($images, $siteImages, $postId);
647
648 $heroPatterns = self::getHeroPatternsData($title, $description, $images, $cta, $featuredOnly, $source);
649
650 if (\is_wp_error($heroPatterns)) {
651 return new \WP_REST_Response([], 500);
652 }
653
654 $heroPatterns = array_values(
655 array_filter(
656 $heroPatterns,
657 function ($heroPattern) use ($currentHeroPattern) {
658 if (!$currentHeroPattern) {
659 return true;
660 }
661
662 return $heroPattern['name'] !== $currentHeroPattern;
663 }
664 )
665 );
666
667 $current = \WP_Theme_JSON_Resolver::get_merged_data('theme');
668
669 $unfiltered = \WP_Theme_JSON_Resolver::get_style_variations();
670
671 // Keep only full style variations — exclude color-only and font-only
672 // presets that get_style_variations() returns from styles/colors/* and
673 // styles/typography/*.
674 $colorAndFontsVariations = array_filter($unfiltered, function ($variation) {
675 $hasPalette = ($variation['settings']['color']['palette'] ?? []) !== [];
676 $hasTypography = ($variation['styles']['typography'] ?? []) !== []
677 || ($variation['settings']['typography'] ?? []) !== [];
678 $hasElements = ($variation['styles']['elements'] ?? []) !== [];
679 return $hasPalette && $hasTypography && $hasElements;
680 });
681
682 $buildSlugMap = function ($unfiltered) {
683 $slugMap = [];
684
685 if (!is_array($unfiltered)) {
686 return $slugMap;
687 }
688
689 foreach ($unfiltered as $rawSlug => $rawVariation) {
690 $title = is_array($rawVariation) ? ($rawVariation['title'] ?? null) : null;
691 $slug = is_array($rawVariation)
692 ? ($rawVariation['slug'] ?? (is_string($rawSlug) ? $rawSlug : null))
693 : null;
694
695 if ($title && $slug && !isset($slugMap[$title])) {
696 $slugMap[$title] = $slug;
697 }
698 }
699 return $slugMap;
700 };
701 $slugMap = $buildSlugMap($unfiltered);
702 array_walk($colorAndFontsVariations, function (&$variation) use ($slugMap) {
703 if (!is_array($variation) || isset($variation['slug'])) {
704 return;
705 }
706
707 $title = $variation['title'] ?? null;
708 if ($title && isset($slugMap[$title])) {
709 $variation['slug'] = $slugMap[$title];
710 }
711 });
712
713 $processedFonts = array_map(function ($variation) {
714 if (!isset($variation['styles']['elements']) || !is_array($variation['styles']['elements'])) {
715 return $variation;
716 }
717
718 $variation['styles']['elements'] = array_map(
719 [self::class, 'normalizeElementTypography'],
720 $variation['styles']['elements']
721 );
722
723 if (!isset($variation['styles']['typography'])) {
724 $variation['styles']['typography'] = [
725 'fontFamily' => 'var(--wp--preset--font-family--inter)'
726 ];
727 }
728
729 return $variation;
730 }, $colorAndFontsVariations);
731
732 $deduped = static::getCss($processedFonts, $current, true);
733
734 $blockEditorContext = new \WP_Block_Editor_Context(array( 'name' => 'core/edit-post' ));
735 $editorSettings = get_block_editor_settings([], $blockEditorContext);
736 $editorSettings['styles'] = [];
737
738 return new \WP_REST_Response(
739 [
740 'patterns' => $heroPatterns,
741 'colorAndFontsVariations' => $deduped,
742 'blockEditorSettings' => $editorSettings ?? null,
743 ]
744 );
745 }
746
747 /**
748 * Sets a lock on a post to prevent concurrent editing.
749 *
750 * @param \WP_REST_Request $request The REST API request object containing postId.
751 * @return \WP_REST_Response Response indicating success of the lock operation.
752 */
753 public static function lockPost($request)
754 {
755 $postId = (int) $request->get_param('postId');
756 require_once ABSPATH . '/wp-admin/includes/post.php';
757 $data = \wp_set_post_lock($postId);
758 return new \WP_REST_Response(['success' => $data !== false]);
759 }
760
761 /**
762 * Persist the data
763 *
764 * @param \WP_REST_Request $request - The request.
765 * @return \WP_REST_Response
766 */
767 public static function updateOption($request)
768 {
769 $params = $request->get_json_params();
770 $key = $params['option'];
771 $sanitized = Sanitizer::sanitizeUnknown($params['value']);
772
773 if (strpos($key, 'extendify_') === 0) {
774 $key = substr($key, 10);
775 }
776 \update_option('extendify_' . $key, $sanitized);
777
778 return new \WP_REST_Response('OK');
779 }
780
781 /**
782 * Get the data
783 *
784 * @param \WP_REST_Request $request - The request.
785 * @return \WP_REST_Response
786 */
787 public static function getOption($request)
788 {
789 $key = $request->get_param('option');
790
791 if (strpos($key, 'extendify_') === 0) {
792 $key = substr($key, 10);
793 }
794 $value = \get_option('extendify_' . $key, null);
795
796 return new \WP_REST_Response($value);
797 }
798 }
799