PluginProbe
WindPress – Tailwind CSS integration for WordPress / 3.2.84
WindPress – Tailwind CSS integration for WordPress v3.2.84
3.2.88 3.2.87 3.2.86 3.2.85 3.2.84 3.2.83 3.2.82 3.2.81 trunk 3.0.0 3.0.1 3.0.10 3.0.11 3.0.12 3.0.13 3.0.14 3.0.15 3.0.16 3.0.17 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 All 142 releases
windpress / src / Integration / Gutenberg / CommonBlock.php

CommonBlock.php in WindPress – Tailwind CSS integration for WordPress 3.2.84, at src/Integration/Gutenberg/CommonBlock.php

240 lines 9.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /*
4 * This file is part of the WindPress package.
5 *
6 * (c) Joshua Gugun Siagian <suabahasa@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11 declare (strict_types=1);
12 namespace WindPress\WindPress\Integration\Gutenberg;
13
14 use WindPressDeps\enshrined\svgSanitize\Sanitizer;
15 use WIND_PRESS;
16 use WindPress\WindPress\Utils\Config;
17 use WindPress\WindPress\Utils\Vite;
18 /**
19 * Common Block - A generic block that can be any HTML element.
20 *
21 * @author Joshua Gugun Siagian <suabahasa@gmail.com>
22 *
23 * TODO:
24 * - clean up the logging and unecessary comments from the current staged git changes
25 * - clean up the code from the current staged git changes
26 */
27 class CommonBlock
28 {
29 /**
30 * Stores the instance, implementing a Singleton pattern.
31 */
32 private static self $instance;
33 /**
34 * The Singleton's constructor should always be private to prevent direct
35 * construction calls with the `new` operator.
36 */
37 private function __construct()
38 {
39 // Only register if Common Block is enabled
40 if (Config::get('integration.gutenberg.settings.common_block', \true)) {
41 add_action('init', fn() => $this->register_block());
42 add_filter('block_default_classname', fn($class, $block_name) => $this->remove_default_class($class, $block_name), 10, 2);
43 add_filter('f!windpress/core/runtime:enqueue_play_modules.loaded_modules', fn($modules) => $this->enqueue_isolate_styles($modules));
44 }
45 }
46 /**
47 * Singletons should not be cloneable.
48 */
49 private function __clone()
50 {
51 }
52 /**
53 * Singletons should not be restorable from strings.
54 *
55 * @throws \Exception Cannot unserialize a singleton.
56 */
57 public function __wakeup()
58 {
59 throw new \Exception('Cannot unserialize a singleton.');
60 }
61 /**
62 * This is the static method that controls the access to the singleton
63 * instance. On the first run, it creates a singleton object and places it
64 * into the static property. On subsequent runs, it returns the client existing
65 * object stored in the static property.
66 */
67 public static function get_instance(): self
68 {
69 if (!isset(self::$instance)) {
70 self::$instance = new self();
71 }
72 return self::$instance;
73 }
74 /**
75 * Enqueue the isolate-styles module
76 * This module prevents Gutenberg styles from affecting Common Blocks
77 *
78 * @param array $modules List of loaded modules
79 * @return array Modified list of modules
80 */
81 public function enqueue_isolate_styles($modules)
82 {
83 Vite::assets()->enqueue('assets/integration/gutenberg/common-block/isolate-styles.js', ['handle' => WIND_PRESS::WP_OPTION . ':gutenberg-editor-isolate-styles', 'in_footer' => \true]);
84 $modules[] = WIND_PRESS::WP_OPTION . ':gutenberg-editor-isolate-styles';
85 return $modules;
86 }
87 /**
88 * Register the Common Block
89 */
90 public function register_block()
91 {
92 // Register block with render callback
93 // The block is registered via JavaScript (index.jsx) with edit and save functions
94 // We only add the render_callback here to override the default rendering
95 register_block_type('windpress/common-block', ['render_callback' => fn($attributes, $content, $block) => $this->render_callback($attributes, $content, $block)]);
96 }
97 /**
98 * Remove WordPress default block class to output clean HTML
99 *
100 * @param string $class The default block class
101 * @param string $block_name The block name
102 * @return string
103 */
104 public function remove_default_class($class, $block_name)
105 {
106 if ($block_name === 'windpress/common-block') {
107 return '';
108 }
109 return $class;
110 }
111 /**
112 * Render callback for the block
113 *
114 * @param array $attributes Block attributes
115 * @param string $content Block inner content
116 * @param object $block Block object
117 * @return string
118 */
119 public function render_callback($attributes, $content, $block)
120 {
121 $tag_name = $attributes['tagName'] ?? 'div';
122 $content_type = $attributes['contentType'] ?? 'blocks';
123 $global_attrs = $attributes['globalAttrs'] ?? [];
124 $text_content = $attributes['content'] ?? '';
125 $self_closing = $attributes['selfClosing'] ?? \false;
126 $class_name = $attributes['className'] ?? '';
127 // Handle cb-text-node (wrapper for TEXT_NODE in html2blocks parser)
128 // Just output the text content without wrapper tags
129 if ($tag_name === 'cb-text-node') {
130 return $text_content;
131 }
132 // Sanitize tag name (allow only alphanumeric, dash, and custom elements with dash)
133 $tag_name = preg_replace('/[^a-zA-Z0-9\-]/', '', $tag_name);
134 // Build attributes array
135 $attrs = [];
136 // Add className if present
137 // Decode HTML entities to preserve arbitrary variants like [&>img]:rounded-lg
138 // Apply decoding twice to handle double-encoded entities (e.g., &amp;amp; -> &amp; -> &)
139 if (!empty($class_name)) {
140 $decoded_class = html_entity_decode($class_name, \ENT_QUOTES | \ENT_HTML5, 'UTF-8');
141 $decoded_class = html_entity_decode($decoded_class, \ENT_QUOTES | \ENT_HTML5, 'UTF-8');
142 $attrs['class'] = $decoded_class;
143 }
144 // Add globalAttrs
145 foreach ($global_attrs as $attr_name => $attr_value) {
146 // Skip 'class' as it's handled by className
147 if ($attr_name === 'class') {
148 continue;
149 }
150 // Sanitize attribute name (only alphanumeric, dash, underscore, at, and colon)
151 // $attr_name = sanitize_key($attr_name);
152 $attr_name = preg_replace('/[^a-zA-Z0-9\-\_\:\@]/', '', $attr_name);
153 // Sanitize attribute value based on type
154 if ($attr_name === 'href' || $attr_name === 'src') {
155 $attrs[$attr_name] = esc_url($attr_value);
156 } else {
157 $attrs[$attr_name] = esc_attr($attr_value);
158 }
159 }
160 // Build attribute string
161 $attr_string = '';
162 foreach ($attrs as $attr_name => $attr_value) {
163 $attr_string .= sprintf(' %s="%s"', $attr_name, $attr_value);
164 }
165 // Determine final content based on content type
166 $final_content = '';
167 if ($content_type === 'blocks') {
168 // Use inner blocks content
169 $final_content = $content;
170 } elseif ($content_type === 'text') {
171 // Use text content (already sanitized by WordPress)
172 $final_content = $text_content;
173 } elseif ($content_type === 'html') {
174 // For SVG elements, use SVG sanitizer
175 if ($tag_name === 'svg') {
176 // Build complete SVG with wrapper for sanitization
177 $svg_wrapper = sprintf('<%s%s>%s</%s>', $tag_name, $attr_string, $text_content, $tag_name);
178 $sanitizer = new Sanitizer();
179 $sanitizer->removeRemoteReferences(\true);
180 // Remove external references for security
181 $sanitized_svg = $sanitizer->sanitize($svg_wrapper);
182 // If sanitization failed, use empty string
183 if ($sanitized_svg === \false) {
184 $final_content = '';
185 } else {
186 // Extract inner content from sanitized SVG
187 // Remove the outer <svg> wrapper that we added
188 $dom = new \DOMDocument();
189 @$dom->loadXML($sanitized_svg);
190 $svg_element = $dom->getElementsByTagName('svg')->item(0);
191 if ($svg_element) {
192 // Get inner HTML of the SVG element
193 $inner_html = '';
194 foreach ($svg_element->childNodes as $child) {
195 $inner_html .= $dom->saveXML($child);
196 }
197 $final_content = $inner_html;
198 } else {
199 $final_content = '';
200 }
201 }
202 } else {
203 // Use wp_kses_post for other HTML content
204 $final_content = wp_kses_post($text_content);
205 }
206 }
207 // 'empty' content type has no content
208 // Render the element
209 if ($self_closing || in_array($tag_name, ['img', 'input', 'br', 'hr', 'meta', 'link'])) {
210 $html = sprintf('<%s%s />', esc_attr($tag_name), $attr_string);
211 } else {
212 $html = sprintf('<%s%s>%s</%s>', esc_attr($tag_name), $attr_string, $final_content, esc_attr($tag_name));
213 }
214 /**
215 * Filter the rendered HTML before returning.
216 *
217 * This allows custom render logic to be applied, such as:
218 * - Template engine compilation (Twig, Blade, Latte, etc.)
219 * - Additional processing or transformations
220 * - Custom HTML sanitization
221 *
222 * Example usage with Picowind's render_string():
223 *
224 * add_filter('f!windpress/integration/gutenberg/common_block:render', function($html, $attributes, $content) {
225 * if (function_exists('\Picowind\render_string') && function_exists('\Picowind\context')) {
226 * $context = \Picowind\context();
227 * return \Picowind\render_string($html, $context, 'twig', false);
228 * }
229 * return $html;
230 * }, 10, 3);
231 *
232 * @param string $html The rendered HTML
233 * @param array $attributes Block attributes
234 * @param string $content Inner blocks content
235 * @return string Modified HTML
236 */
237 return apply_filters('f!windpress/integration/gutenberg/common_block:render', $html, $attributes, $content);
238 }
239 }
240