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 / Shared / Services / BlockStyleVariations.php

BlockStyleVariations.php in Extendify 3.2.1, at app/Shared/Services/BlockStyleVariations.php

165 lines 5.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Extendify\Shared\Services;
4
5 defined('ABSPATH') || die('No direct access.');
6
7 /**
8 * WordPress sanitizes user global styles against the block style registry, so an
9 * unregistered variation name is dropped from every read of the stored styles
10 * and its CSS never compiles.
11 */
12 class BlockStyleVariations
13 {
14 /**
15 * Variation names per block type, as last written to global styles.
16 *
17 * @var string
18 */
19 // phpcs:ignore PSR12.Properties.ConstantVisibility.NotFound
20 const OPTION = 'extendify_block_style_variations';
21
22 /**
23 * @return void
24 */
25 public static function register()
26 {
27 // Late, so a theme registering the same name keeps its own label.
28 add_action('init', [self::class, 'registerStored'], 99);
29 add_filter('rest_request_before_callbacks', [self::class, 'registerIncoming'], 10, 3);
30 }
31
32 /**
33 * @return void
34 */
35 public static function registerStored()
36 {
37 $stored = get_option(self::OPTION, []);
38 $valid = self::validBlockTypes($stored);
39 if ($valid !== $stored) {
40 update_option(self::OPTION, $valid);
41 }
42
43 self::registerNames($valid);
44 }
45
46 /**
47 * A design flow's write carries the names it needs, so nothing is fetched or
48 * guessed. Runs before the controller sanitizes the incoming JSON.
49 *
50 * @param mixed $response - The REST response, passed through untouched.
51 * @param array $handler - The matched route handler.
52 * @param \WP_REST_Request $request - The incoming request.
53 * @return mixed
54 */
55 public static function registerIncoming($response, $handler, $request)
56 {
57 $isWrite = in_array($request->get_method(), ['POST', 'PUT', 'PATCH'], true);
58 if (!$isWrite || strpos($request->get_route(), '/wp/v2/global-styles') !== 0) {
59 return $response;
60 }
61
62 // This filter runs before the route's permission_callback, so auth is checked here.
63 if (!current_user_can('edit_theme_options')) {
64 return $response;
65 }
66
67 $names = self::namesInWrite($request->get_param('styles'));
68 if (!$names) {
69 return $response;
70 }
71
72 self::registerNames($names);
73 update_option(self::OPTION, self::mergeNames(get_option(self::OPTION, []), $names));
74
75 return $response;
76 }
77
78 /**
79 * @param mixed $styles - The styles node of a global styles write.
80 * @return array<string, array<string>>
81 */
82 private static function namesInWrite($styles)
83 {
84 $blocks = is_array($styles) ? ($styles['blocks'] ?? []) : [];
85 $names = [];
86 foreach ((array) $blocks as $blockType => $block) {
87 if (!self::isValidBlockType($blockType)) {
88 continue;
89 }
90
91 $variations = is_array($block) ? ($block['variations'] ?? []) : [];
92 $ours = array_filter(array_keys((array) $variations), function ($name) {
93 return is_string($name) && strpos($name, 'ext-') === 0;
94 });
95
96 if ($ours) {
97 $names[$blockType] = array_values($ours);
98 }
99 }
100
101 return $names;
102 }
103
104 /**
105 * @param mixed $stored - The names already persisted.
106 * @param array<string, array<string>> $names - The names to add.
107 * @return array<string, array<string>>
108 */
109 private static function mergeNames($stored, $names)
110 {
111 $merged = is_array($stored) ? $stored : [];
112 foreach ($names as $blockType => $blockNames) {
113 $existing = (array) ($merged[$blockType] ?? []);
114 $merged[$blockType] = array_values(array_unique(array_merge($existing, $blockNames)));
115 }
116
117 return $merged;
118 }
119
120 /**
121 * A block-style key is interpolated unescaped into core's inline
122 * registerBlockStyle() script, so only a real "namespace/block" name is
123 * ever stored or registered — anything else is an injection attempt.
124 *
125 * @param mixed $blockType - The array key from a global-styles write.
126 * @return bool
127 */
128 private static function isValidBlockType($blockType)
129 {
130 return is_string($blockType) && preg_match('~^[a-z0-9-]+/[a-z0-9-]+$~', $blockType) === 1;
131 }
132
133 /**
134 * @param mixed $stored - The persisted names per block type.
135 * @return array<string, array<string>>
136 */
137 private static function validBlockTypes($stored)
138 {
139 $names = is_array($stored) ? $stored : [];
140 return array_filter($names, [self::class, 'isValidBlockType'], ARRAY_FILTER_USE_KEY);
141 }
142
143 /**
144 * @param mixed $names - Variation names per block type.
145 * @return void
146 */
147 private static function registerNames($names)
148 {
149 $registry = \WP_Block_Styles_Registry::get_instance();
150 foreach ((array) $names as $blockType => $blockNames) {
151 if (!self::isValidBlockType($blockType)) {
152 continue;
153 }
154
155 foreach ((array) $blockNames as $name) {
156 if (!is_string($name) || $registry->is_registered($blockType, $name)) {
157 continue;
158 }
159
160 \register_block_style($blockType, ['name' => $name, 'label' => $name]);
161 }
162 }
163 }
164 }
165