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

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