PluginProbe
Extendify / 3.1.5
Extendify v3.1.5
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 3.1.5, at app/Agent/Controllers/WPController.php

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