PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / blocks / bridge.php

bridge.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/blocks/bridge.php

304 lines 10.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Shortcode → Block bridge (StoreEngine core).
4 *
5 * StoreEngine owns the bridge: a generic `storeengine/shortcode` block that
6 * renders ANY registered shortcode with real editor controls, available with
7 * just StoreEngine active (no page-builder dependency). aBlocks, when installed,
8 * layers styled native blocks on top and offers a one-click Convert — so the
9 * block doubles as a promotion for aBlocks.
10 *
11 * Plugins register their shortcodes via storeengine_register_shortcode_block() or the
12 * `storeengine_shortcode_block_registry` filter.
13 *
14 * @package StoreEngine\Blocks
15 */
16
17 namespace StoreEngine\Blocks {
18
19 if ( ! defined( 'ABSPATH' ) ) {
20 exit;
21 }
22
23 class Bridge {
24
25 const BLOCK = 'storeengine/shortcode';
26 const HANDLE = 'storeengine-blocks-editor';
27 const JS_GLOBAL = 'StoreEngineShortcodeBlock';
28
29 public static function init() {
30 $self = new self();
31
32 add_action( 'init', [ $self, 'register_block' ], 20 );
33 add_action( 'enqueue_block_editor_assets', [ $self, 'enqueue_editor' ] );
34 add_action( 'enqueue_block_assets', [ $self, 'enqueue_preview_styles' ] );
35 add_action( 'rest_api_init', [ $self, 'register_rest' ] );
36 }
37
38 /**
39 * Load StoreEngine's front-end stylesheet into the block editor canvas so
40 * the server-rendered shortcode preview looks the same as the front end.
41 *
42 * The editor canvas is an iframe; only `enqueue_block_assets` reaches inside
43 * it (`enqueue_block_editor_assets` targets the outer editor chrome). We gate
44 * on is_admin() so this fires only in the editor — the front end already
45 * loads these styles on the store pages that need them. Independent of
46 * aBlocks: the core block must preview correctly with just StoreEngine active.
47 */
48 public function enqueue_preview_styles() {
49 if ( ! is_admin() ) {
50 return;
51 }
52
53 $assets = new \StoreEngine\Assets();
54
55 wp_enqueue_style(
56 'storeengine-frontend-icon',
57 STOREENGINE_ASSETS_URI . 'library/icons/storeengine-icons.css',
58 [ 'wp-components' ],
59 filemtime( STOREENGINE_ASSETS_DIR_PATH . 'library/icons/storeengine-icons.css' ),
60 'all'
61 );
62
63 wp_enqueue_style(
64 'storeengine-frontend-style',
65 STOREENGINE_ASSETS_URI . 'build/frontend.css',
66 [],
67 filemtime( STOREENGINE_ASSETS_DIR_PATH . 'build/frontend.css' ),
68 'all'
69 );
70 wp_add_inline_style( 'storeengine-frontend-style', $assets->get_dynamic_css() );
71 }
72
73 /**
74 * Build a `storeengine/shortcode` block comment for seeding into
75 * post_content — the recommended replacement for a raw `[shortcode]` or
76 * legacy `<!-- wp:shortcode -->[…]` block. Renders identically on the front
77 * end while giving editor controls and one-click Convert to aBlocks.
78 *
79 * @param string $id Descriptor id (owner/tag), e.g. "storeengine/storeengine_dashboard".
80 * @param array $atts Optional shortcode attributes.
81 * @param string $content Optional inner content for content-supporting shortcodes.
82 */
83 public static function block( string $id, array $atts = [], string $content = '' ): string {
84 $data = [ 'shortcode' => $id ];
85 if ( ! empty( $atts ) ) {
86 $data['atts'] = $atts;
87 }
88 if ( '' !== $content ) {
89 $data['content'] = $content;
90 }
91
92 return '<!-- wp:' . self::BLOCK . ' ' . wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) . ' /-->';
93 }
94
95 /* -------------------------------------------------------------- */
96 /* Block registration + render */
97 /* -------------------------------------------------------------- */
98
99 public function register_block() {
100 // Register the editor script handle so block.json's editorScript can
101 // enqueue it (and we can localize the registry onto it).
102 $asset_file = STOREENGINE_ASSETS_DIR_PATH . 'build/blocks.' . STOREENGINE_VERSION . '.js';
103 $asset_meta = STOREENGINE_ASSETS_DIR_PATH . 'build/blocks.' . STOREENGINE_VERSION . '.asset.php';
104 $deps = file_exists( $asset_meta )
105 ? ( include $asset_meta )['dependencies']
106 : [ 'wp-blocks', 'wp-block-editor', 'wp-components', 'wp-element', 'wp-i18n', 'wp-server-side-render' ];
107
108 wp_register_script(
109 self::HANDLE,
110 STOREENGINE_ASSETS_URI . 'build/blocks.' . STOREENGINE_VERSION . '.js',
111 $deps,
112 file_exists( $asset_file ) ? filemtime( $asset_file ) : STOREENGINE_VERSION,
113 true
114 );
115
116 // Editor styles for the block (promo card, picker, field labels).
117 $style_file = STOREENGINE_ASSETS_DIR_PATH . 'build/blocks.css';
118 if ( file_exists( $style_file ) ) {
119 wp_register_style(
120 self::HANDLE,
121 STOREENGINE_ASSETS_URI . 'build/blocks.css',
122 [ 'wp-components' ],
123 filemtime( $style_file )
124 );
125 }
126
127 register_block_type(
128 STOREENGINE_INCLUDES_DIR_PATH . 'blocks/block.json',
129 [ 'render_callback' => [ $this, 'render' ] ]
130 );
131 }
132
133 /**
134 * Server render: rebuild a minimal shortcode (only non-default atts, each
135 * coerced by its sanitize hint) and delegate to do_shortcode.
136 */
137 public function render( $attributes, $content = '' ): string {
138 $id = isset( $attributes['shortcode'] ) ? (string) $attributes['shortcode'] : '';
139 if ( '' === $id ) {
140 return '';
141 }
142
143 $descriptor = Registry::instance()->get( $id );
144 if ( $descriptor ) {
145 return do_shortcode( $this->build_shortcode( $descriptor, $attributes, (string) $content ) );
146 }
147
148 // No descriptor registered — still render the bare shortcode from the
149 // tag embedded in the id (everything after "owner/"), so the block works
150 // for any shortcode, described or not.
151 $tag = false !== strpos( $id, '/' ) ? substr( $id, strpos( $id, '/' ) + 1 ) : $id;
152 $tag = preg_replace( '/[^a-z0-9_]/', '', $tag );
153 if ( '' === $tag || ! shortcode_exists( $tag ) ) {
154 return '';
155 }
156
157 return do_shortcode( '[' . $tag . ']' );
158 }
159
160 protected function build_shortcode( array $descriptor, array $attributes, string $inner ): string {
161 $tag = $descriptor['tag'];
162 $atts_in = ( isset( $attributes['atts'] ) && is_array( $attributes['atts'] ) ) ? $attributes['atts'] : [];
163
164 $pairs = [];
165 foreach ( $descriptor['attributes'] as $attr ) {
166 $name = $attr['name'];
167 if ( ! array_key_exists( $name, $atts_in ) ) {
168 continue;
169 }
170 $value = $atts_in[ $name ];
171 if ( (string) $value === (string) $attr['default'] || '' === (string) $value ) {
172 continue;
173 }
174 $clean = self::sanitize( $value, $attr['sanitize'] );
175 if ( '' === $clean ) {
176 continue;
177 }
178 $pairs[] = $name . '="' . esc_attr( $clean ) . '"';
179 }
180 $attr_str = $pairs ? ' ' . implode( ' ', $pairs ) : '';
181
182 if ( empty( $descriptor['content']['supported'] ) ) {
183 return '[' . $tag . $attr_str . ']';
184 }
185 $body = ( ( $descriptor['content']['mode'] ?? 'plain' ) === 'innerblocks' )
186 ? $inner
187 : ( isset( $attributes['content'] ) ? (string) $attributes['content'] : '' );
188
189 return '[' . $tag . $attr_str . ']' . $body . '[/' . $tag . ']';
190 }
191
192 /* -------------------------------------------------------------- */
193 /* Editor exposure (registry + aBlocks promotion state) */
194 /* -------------------------------------------------------------- */
195
196 public function enqueue_editor() {
197 if ( ! wp_script_is( self::HANDLE, 'registered' ) ) {
198 $this->register_block();
199 }
200 wp_enqueue_script( self::HANDLE );
201 if ( wp_style_is( self::HANDLE, 'registered' ) ) {
202 wp_enqueue_style( self::HANDLE );
203 }
204
205 $ablocks_active = function_exists( 'is_plugin_active' )
206 ? is_plugin_active( 'ablocks/ablocks.php' )
207 : in_array( 'ablocks/ablocks.php', (array) get_option( 'active_plugins', [] ), true );
208
209 $payload = [
210 'schema_version' => Registry::SCHEMA_VERSION,
211 'types' => Registry::TYPES,
212 'shortcodes' => Registry::instance()->all(),
213 'ablocks' => [
214 'active' => (bool) $ablocks_active,
215 'install_url' => self::ablocks_install_url(),
216 ],
217 ];
218
219 wp_add_inline_script(
220 self::HANDLE,
221 'window.' . self::JS_GLOBAL . ' = ' . wp_json_encode( $payload ) . ';',
222 'before'
223 );
224 }
225
226 protected static function ablocks_install_url(): string {
227 if ( current_user_can( 'install_plugins' ) ) {
228 return wp_nonce_url(
229 self_admin_url( 'update.php?action=install-plugin&plugin=ablocks' ),
230 'install-plugin_ablocks'
231 );
232 }
233
234 return 'https://wordpress.org/plugins/ablocks/';
235 }
236
237 public function register_rest() {
238 register_rest_route(
239 'shortcode-block/v1',
240 '/registry',
241 [
242 'methods' => 'GET',
243 'permission_callback' => static function () {
244 return current_user_can( 'edit_posts' );
245 },
246 'callback' => static function () {
247 return rest_ensure_response( [
248 'schema_version' => Registry::SCHEMA_VERSION,
249 'types' => Registry::TYPES,
250 'shortcodes' => Registry::instance()->all(),
251 ] );
252 },
253 ]
254 );
255 }
256
257 /**
258 * Coerce a stored attribute value using the descriptor's sanitize hint,
259 * before it goes into the shortcode string.
260 *
261 * @param mixed $value
262 */
263 public static function sanitize( $value, string $hint ): string {
264 switch ( $hint ) {
265 case 'int':
266 return (string) intval( $value );
267 case 'key':
268 return sanitize_key( is_scalar( $value ) ? (string) $value : '' );
269 case 'url':
270 return esc_url_raw( is_scalar( $value ) ? (string) $value : '' );
271 case 'color':
272 $value = is_scalar( $value ) ? (string) $value : '';
273 return sanitize_hex_color( $value ) ?: preg_replace( '/[^a-zA-Z0-9#(),.%\s-]/', '', $value );
274 case 'csv':
275 $parts = array_map( 'trim', explode( ',', is_scalar( $value ) ? (string) $value : '' ) );
276 $parts = array_filter( $parts, static fn( $p ) => '' !== $p );
277 return implode( ',', array_map( 'sanitize_text_field', $parts ) );
278 case 'text':
279 default:
280 return sanitize_text_field( is_scalar( $value ) ? (string) $value : '' );
281 }
282 }
283 }
284 }
285
286 namespace {
287
288 use StoreEngine\Blocks\Registry;
289
290 if ( ! function_exists( 'storeengine_register_shortcode_block' ) ) {
291 /**
292 * Register a shortcode as an editor block via the StoreEngine bridge.
293 *
294 * Call on an init-level hook, after the shortcode itself is added. Returns
295 * false (and logs under WP_DEBUG) when the descriptor is malformed.
296 *
297 * @param array $descriptor See the v1 contract.
298 */
299 function storeengine_register_shortcode_block( array $descriptor ): bool {
300 return Registry::instance()->register( $descriptor );
301 }
302 }
303 }
304