PluginProbe
Content Control – The Ultimate Content Restriction Plugin! Restrict Content, Create Conditional Blocks & More / trunk
Content Control – The Ultimate Content Restriction Plugin! Restrict Content, Create Conditional Blocks & More vtrunk
trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.10 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 All 47 releases
content-control / classes / Controllers / Frontend / Blocks.php

Blocks.php in Content Control – The Ultimate Content Restriction Plugin! Restrict Content, Create Conditional Blocks & More trunk, at classes/Controllers/Frontend/Blocks.php

358 lines 11.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Frontend general setup.
4 *
5 * @copyright (c) 2023, Code Atlantic LLC.
6 * @package ContentControl
7 */
8
9 namespace ContentControl\Controllers\Frontend;
10
11 use ContentControl\Base\Controller;
12
13 use function ContentControl\protection_is_disabled;
14
15 defined( 'ABSPATH' ) || exit;
16
17 /**
18 * Class Frontend
19 */
20 class Blocks extends Controller {
21
22 /**
23 * Initialize Hooks & Filters.
24 *
25 * @return void
26 */
27 public function init() {
28 if ( is_admin() ) {
29 return;
30 }
31
32 add_action( 'wp_loaded', [ $this, 'register_block_attributes' ], 100 );
33 add_filter( 'pre_render_block', [ $this, 'pre_render_block' ], 999, 3 );
34 add_filter( 'render_block', [ $this, 'render_block' ], 10, 2 );
35 add_filter( 'content_control/should_hide_block', [ $this, 'block_user_rules' ], 10, 2 );
36 add_action( 'wp_print_styles', [ $this, 'print_block_styles' ] );
37 }
38
39 /**
40 * Adds custom attributes to allowed block attributes.
41 *
42 * @return void
43 */
44 public function register_block_attributes() {
45 $registered_blocks = \WP_Block_Type_Registry::get_instance()->get_all_registered();
46
47 foreach ( $registered_blocks as $name => $block ) {
48 $block->attributes['contentControls'] = [
49 'type' => 'object',
50 'default' => [
51 'enabled' => false,
52 'rules' => [],
53 ],
54 ];
55 }
56 }
57
58 /**
59 * Check if block has controls enabled.
60 *
61 * @param array<string,mixed> $block Block to be checked.
62 * @return boolean Whether the block has Controls enabled.
63 */
64 public function has_block_controls( $block ) {
65 if ( ! isset( $block['attrs']['contentControls'] ) ) {
66 return false;
67 }
68
69 $controls = wp_parse_args( $block['attrs']['contentControls'], [
70 'enabled' => false,
71 ] );
72
73 return (bool) $controls['enabled'];
74 }
75
76 /**
77 * Get blocks controls if enabled.
78 *
79 * @param array{attrs:array<string,mixed>} $block Block to get controls for.
80 * @return array{enabled:bool,rules:array<string,mixed>}|null Controls if enabled.
81 */
82 public function get_block_controls( $block ) {
83 if ( ! $this->has_block_controls( $block ) ) {
84 return null;
85 }
86
87 /**
88 * Controls for the block.
89 *
90 * @var array{enabled:bool,rules:array<string,mixed>} $controls
91 */
92 $controls = wp_parse_args( $block['attrs']['contentControls'], [
93 'enabled' => false,
94 'rules' => [],
95 ] );
96
97 return $controls;
98 }
99
100 /**
101 * Check block rules to see if it should be hidden from user.
102 *
103 * @param array{attrs:array<string,mixed>} $block Block to get controls for.
104 *
105 * @return boolean Whether the block should be hidden.
106 */
107 public function should_hide_block( $block ) {
108 if ( protection_is_disabled() ) {
109 return false;
110 }
111
112 if ( ! $this->has_block_controls( $block ) ) {
113 return false;
114 }
115
116 $controls = $this->get_block_controls( $block );
117
118 if ( ! $controls['enabled'] ) {
119 return false;
120 }
121
122 /**
123 * Filter whether to hide the block.
124 *
125 * @param bool $should_hide Whether the block should be hidden.
126 * @param array $rules Rules to check.
127 * @param array $block The block being rendered.
128 * @return bool
129 */
130 $should_hide = apply_filters(
131 'content_control/should_hide_block',
132 false,
133 $controls['rules'],
134 $block
135 );
136
137 return $should_hide;
138 }
139
140 /**
141 * Short curcuit block rendering for hidden blocks.
142 *
143 * @param string|null $pre_render The pre-rendered content. Default null.
144 * @param array<string,mixed> $parsed_block The block being rendered.
145 * @param \WP_Block|null $parent_block If this is a nested block, a reference to the parent block.
146 *
147 * @return string|null
148 */
149 public function pre_render_block( $pre_render, $parsed_block, $parent_block ) {
150 return $this->should_hide_block( $parsed_block ) ? '' : $pre_render;
151 }
152
153 /**
154 * Check block rules to see if it should be hidden from user.
155 *
156 * @param bool $should_hide Whether the block should be hidden.
157 * @param array<string,array<string,mixed>|null> $rules Rules to check.
158 * @return bool
159 */
160 public function block_user_rules( $should_hide, $rules ) {
161 $rules = wp_parse_args( $rules, [
162 'user' => null,
163 ] );
164
165 if ( null === $rules['user'] || true === $should_hide ) {
166 return $should_hide;
167 }
168
169 $user_status = ! empty( $rules['user']['userStatus'] ) ? $rules['user']['userStatus'] : false;
170 $role_match = ! empty( $rules['user']['roleMatch'] ) ? $rules['user']['roleMatch'] : 'any';
171 $user_roles = ! empty( $rules['user']['userRoles'] ) ? $rules['user']['userRoles'] : [];
172
173 if ( ! \ContentControl\user_meets_requirements( $user_status, $user_roles, $role_match ) ) {
174 return true;
175 }
176
177 return $should_hide;
178 }
179
180 /**
181 * Get any classes to be added to the outer block element.
182 *
183 * @param array{attrs:array<string,mixed>} $block Block to get controls for.
184 * @return null|string[]
185 */
186 public function get_block_control_classes( $block ) {
187 if ( ! $this->has_block_controls( $block ) ) {
188 return null;
189 }
190
191 $classes = [];
192
193 $controls = $this->get_block_controls( $block );
194
195 if ( isset( $controls['rules']['device'] ) ) {
196 $device_rules = wp_parse_args( $controls['rules']['device'], [
197 'hideOn' => [],
198 ] );
199
200 $hide_on = wp_parse_args( $device_rules['hideOn'], [
201 'mobile' => null,
202 'tablet' => null,
203 'desktop' => null,
204 ] );
205
206 foreach ( $hide_on as $device => $hidden ) {
207 if ( $hidden ) {
208 $classes[] = 'cc-hide-on-' . sanitize_html_class( $device );
209 }
210 }
211 }
212
213 /**
214 * Filter the classes to be added to the block.
215 *
216 * @param array $classes Classes to be added.
217 * @param array $controls Controls for the block.
218 * @param array $block Block to get classes for.
219 *
220 * @return string[]
221 */
222 $classes = apply_filters( 'content_control/get_block_control_classes', $classes, $controls, $block );
223 $classes = array_filter( $classes, 'is_string' );
224 $classes = array_map( 'sanitize_html_class', $classes );
225 $classes = array_filter( $classes );
226
227 return array_unique( $classes );
228 }
229
230 /**
231 * Filter the block attributes, primarily to add classes and control visibility.
232 *
233 * References: https://github.com/WordPress/gutenberg/search?l=PHP&q=%27render_block%27
234 * - https://github.com/WordPress/gutenberg/blob/9aab0c4f60c78d19aae0af3351a2b66f8fa4c162/lib/block-supports/layout.php#L317
235 * - https://github.com/WordPress/gutenberg/blob/e776b4f00f690ce9cf21c027ebf5e7442420d716/lib/block-supports/duotone.php#L503
236 * - https://github.com/WordPress/gutenberg/blob/9aab0c4f60c78d19aae0af3351a2b66f8fa4c162/lib/block-supports/elements.php#L54-L72
237 *
238 * @param string $block_content Blocks rendered html.
239 * @param array<string,mixed> $block Array of block properties.
240 *
241 * @return string
242 */
243 public function render_block( $block_content, $block ) {
244 if ( ! $this->has_block_controls( $block ) ) {
245 return $block_content;
246 }
247
248 // If the block should be hidden, return an empty string.
249 // This catches blocks that should be hidden but were missed by pre_render_block.
250 // This applies to core/navigation-link blocks for example.
251 if ( $this->should_hide_block( $block ) ) {
252 return '';
253 }
254
255 $classes = $this->get_block_control_classes( $block );
256
257 if ( empty( $classes ) ) {
258 return $block_content;
259 }
260
261 // Enqueue the styles.
262 // wp_enqueue_style( 'content-control-block-styles' );.
263
264 $class_name = esc_attr( implode( ' ', $classes ) );
265
266 /** Mimicing WP Cores usage in https://github.com/WordPress/wordpress-develop/blob/trunk/src/wp-includes/block-supports/elements.php#L32 */
267 $html_element_matches = [];
268 preg_match( '/<[^>]+>/', $block_content, $html_element_matches, PREG_OFFSET_CAPTURE );
269 $first_element = $html_element_matches[0][0];
270
271 // If the first HTML element has a class attribute just add the new class
272 // as we do on layout and duotone.
273 if ( strpos( $first_element, 'class="' ) !== false || strpos( $first_element, "class='" ) !== false ) {
274 $content = preg_replace(
275 // Matches $1 & $3 is ' or ", $2 are the existing classes.
276 '/class=([\'"])([a-z0-9-_ ]*)([\'"])/',
277 'class=$1$2 ' . $class_name . '$3',
278 $block_content,
279 1
280 );
281 } else {
282 // If the first HTML element has no class attribute we should inject the attribute before the attribute at the end.
283 $first_element_offset = $html_element_matches[0][1];
284 $content = substr_replace( $block_content, ' class="' . $class_name . '"', $first_element_offset + strlen( $first_element ) - 1, 0 );
285 }
286
287 return $content;
288 }
289
290 /**
291 * Print the styles for the block controls.
292 *
293 * @return void
294 */
295 public function print_block_styles() {
296 $media_queries = $this->container->get_option( 'mediaQueries' );
297
298 if ( ! $media_queries ) {
299 $styles = "@media (max-width: 480px) {\n\t.cc-hide-on-mobile {\n\t\tdisplay: none !important;\n\t}\n}\n";
300 $styles .= "@media (min-width: 481px) and (max-width: 991px) {\n\t.cc-hide-on-tablet {\n\t\tdisplay: none !important;\n\t}\n}\n";
301 $styles .= "@media (min-width: 992px) {\n\t.cc-hide-on-desktop {\n\t\tdisplay: none !important;\n\t}\n}";
302 $this->enqueue_block_styles( $styles );
303 return;
304 }
305
306 $mobile_breakpoint = isset( $media_queries['mobile']['breakpoint'] ) ? absint( $media_queries['mobile']['breakpoint'] ) : 640;
307 $tablet_breakpoint = isset( $media_queries['tablet']['breakpoint'] ) ? absint( $media_queries['tablet']['breakpoint'] ) : 920;
308 $desktop_breakpoint = isset( $media_queries['desktop']['breakpoint'] ) ? absint( $media_queries['desktop']['breakpoint'] ) : 1440;
309
310 $tablet_start = $mobile_breakpoint + 1;
311 $desktop_start = $tablet_breakpoint + 1;
312
313 $styles = [];
314 $styles[] = sprintf( "@media (max-width: %dpx) {\n\t.cc-hide-on-mobile {\n\t\tdisplay: none !important;\n\t}\n}", $mobile_breakpoint );
315 $styles[] = sprintf( "@media (min-width: %dpx) and (max-width: %dpx) {\n\t.cc-hide-on-tablet {\n\t\tdisplay: none !important;\n\t}\n}", $tablet_start, $tablet_breakpoint );
316 $styles[] = sprintf( "@media (min-width: %dpx) and (max-width: %dpx) {\n\t.cc-hide-on-desktop {\n\t\tdisplay: none !important;\n\t}\n}", $desktop_start, $desktop_breakpoint );
317
318 unset( $media_queries['mobile'], $media_queries['tablet'], $media_queries['desktop'] );
319
320 foreach ( $media_queries as $media_query => $media_query_settings ) {
321 if ( ! is_array( $media_query_settings ) || ! isset( $media_query_settings['breakpoint'] ) ) {
322 continue;
323 }
324
325 $breakpoint = absint( $media_query_settings['breakpoint'] );
326 $media_query_class = sanitize_html_class( $media_query );
327
328 if ( empty( $media_query_class ) ) {
329 continue;
330 }
331
332 $style = sprintf( "@media (min-width: %dpx) {\n\t.cc-hide-on-%s {\n\t\tdisplay: none !important;\n\t}\n}", $breakpoint, $media_query_class );
333
334 $styles[] = apply_filters( 'content_control/block_styles', $style, $media_query, $breakpoint );
335 }
336
337 $styles = implode( "\n", $styles );
338
339 $this->enqueue_block_styles( $styles );
340 }
341
342 /**
343 * Enqueue generated block-control styles.
344 *
345 * @param string $styles CSS styles.
346 *
347 * @return void
348 */
349 protected function enqueue_block_styles( $styles ) {
350 $handle = 'content-control-block-styles';
351
352 wp_register_style( $handle, false, [], $this->container->get( 'version' ) );
353 wp_enqueue_style( $handle );
354 // Preserve valid HTML-like CSS syntax; wp_add_inline_style() handles style-tag safety.
355 wp_add_inline_style( $handle, $styles );
356 }
357 }
358