PluginProbe
Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News / 4.0.2
Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News v4.0.2
4.0.8 4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 2.3.5 2.3.6 2.4.0 2.4.1 2.4.10 2.4.11 2.4.12 2.4.13 2.4.14 2.4.15 2.4.16 2.4.17 2.4.18 2.4.19 2.4.2 2.4.20 2.4.21 All 88 releases
post-carousel / blocks / includes / class-assets-manager.php

class-assets-manager.php in Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News 4.0.2, at blocks/includes/class-assets-manager.php

1,933 lines 59.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles all asset loading for the Smart Post Show plugin.
4 *
5 * @package Smart_Post_Show
6 */
7
8 namespace SmartPostShow\Blocks;
9
10 use SmartPostShow\Blocks\Helper;
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit; // Exit if accessed directly.
14 }
15
16 /**
17 * Assets_Manager class.
18 */
19 class Assets_Manager {
20
21 /**
22 * Preview transient expiration time.
23 */
24 const PREVIEW_TRANSIENT_EXPIRATION = HOUR_IN_SECONDS;
25
26 /**
27 * CSS file prefix.
28 */
29 const CSS_FILE_PREFIX = 'sp-post-';
30
31 /**
32 * Static CSS directory.
33 */
34 const STATIC_CSS_DIR = 'static/';
35
36 /**
37 * Registered styles.
38 *
39 * @var array
40 */
41 private $styles = array();
42
43 /**
44 * Registered scripts.
45 *
46 * @var array
47 */
48 private $scripts = array();
49
50 /**
51 * Block slugs
52 *
53 * @var array
54 */
55 private $block_slugs = array();
56
57 /**
58 * Block slugs
59 *
60 * @var array
61 */
62 private $google_fonts = array();
63
64 /**
65 * CSS directory path
66 *
67 * @var string
68 */
69 private $css_dir;
70
71 /**
72 * CSS directory URL
73 *
74 * @var string
75 */
76 private $css_url;
77
78 /**
79 * Debug mode flag.
80 *
81 * @var bool
82 */
83 private $sps_debug = false;
84
85 /**
86 * File system instance.
87 *
88 * @var \WP_Filesystem_Base
89 */
90 private $wp_filesystem;
91
92 /**
93 * Assets_Manager constructor.
94 *
95 * @param array $slugs Blocks slugs.
96 */
97 public function __construct( $slugs ) {
98 $this->block_slugs = $slugs;
99 $upload_dir = wp_upload_dir();
100 $this->css_dir = trailingslashit( $upload_dir['basedir'] ) . 'smart-post-show/';
101 $this->css_url = trailingslashit( $upload_dir['baseurl'] ) . 'smart-post-show/';
102
103 // Initialize file system.
104 $this->init_filesystem();
105
106 add_action( 'after_delete_post', array( $this, 'delete_block_css' ), 10, 2 );
107 add_action( 'enqueue_block_assets', array( $this, 'enqueue_assets' ) );
108
109 add_action(
110 'rest_api_init',
111 function () {
112 register_rest_route(
113 'sp-smart-post/v2',
114 '/smart-save-block-css',
115 array(
116 array(
117 'methods' => 'POST',
118 'callback' => array( $this, 'save_block_content_css' ),
119 'permission_callback' => function () {
120 return current_user_can( 'publish_posts' );
121 },
122 'args' => array(),
123 ),
124 )
125 );
126 }
127 );
128 add_filter( 'render_block', array( $this, 'render_block_callback' ), 10, 2 );
129 add_filter(
130 'dynamic_sidebar_params',
131 function ( $params ) {
132 global $sp_current_page_widgets;
133 $widget_id = $params[0]['widget_id'] ?? '';
134 if ( strpos( $widget_id, 'block-' ) === 0 ) {
135 $sp_current_page_widgets[] = array(
136 'sidebar_id' => $params[0]['id'],
137 'widget_id' => $params[0]['widget_id'],
138 'widget_name' => $params[0]['widget_name'],
139 );
140 }
141 return $params;
142 }
143 );
144 add_action( 'dynamic_sidebar_after', array( $this, 'generate_widget_dynamic_css' ) );
145 }
146 /**
147 * Generates dynamic CSS for widgets in non-block themes.
148 *
149 * @return void
150 */
151 public function generate_widget_dynamic_css() {
152 // Only for classic themes.
153 if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) {
154 return;
155 }
156 global $sp_current_page_widgets, $wp_registered_sidebars;
157 if ( empty( $wp_registered_sidebars ) || empty( $sp_current_page_widgets ) ) {
158 return;
159 }
160 $widget_blocks = get_option( 'widget_block', array() );
161 $google_fonts_list = array();
162 $css = '';
163 $block_names = array();
164 foreach ( $sp_current_page_widgets as $widget ) {
165 // Ensure widget ID exists.
166 if ( empty( $widget['widget_id'] ) ) {
167 continue;
168 }
169 $widget_id = $widget['widget_id'];
170 // Only block widgets (block-*).
171 if ( strpos( $widget_id, 'block-' ) !== 0 ) {
172 continue;
173 }
174 // Extract instance number from widget ID.
175 if ( ! preg_match( '/-(\d+)$/', $widget_id, $matches ) ) {
176 continue;
177 }
178 $instance = (int) $matches[1];
179 // Validate widget content exists.
180 if ( empty( $widget_blocks[ $instance ]['content'] ) ) {
181 continue;
182 }
183 $current_block_name = get_option( '_sp_smart_widget_block_name' . $widget_id, array() );
184
185 if ( empty( $current_block_name ) ) {
186 continue;
187 }
188 $this->merge_assets(
189 get_option( '_sp_smart_widget_' . $widget_id, '' ),
190 get_option( '_sp_smart_fonts_widget_' . $widget_id, array() ),
191 $css,
192 $google_fonts_list
193 );
194 $block_names[] = $current_block_name;
195
196 }
197 // Output inline CSS.
198 if ( ! empty( $css ) ) {
199 wp_add_inline_style( 'sp_smart_post_blocks_css', wp_strip_all_tags( $css ) );
200 }
201 $this->enqueue_widget_block_css( $block_names );
202 if ( ! empty( $google_fonts_list ) ) {
203 $font_families = array();
204 foreach ( array_unique( $google_fonts_list ) as $font ) {
205 if ( ! is_string( $font ) || '' === $font ) {
206 continue;
207 }
208 $font_parts = explode( ':', $font );
209 $font_name = str_replace( ' ', '+', $font_parts[0] );
210 $font_weights = $font_parts[1] ?? '400';
211 if ( empty( $font_name ) ) {
212 continue;
213 }
214 $font_families[] = $font_name . ':' . $font_weights;
215 }
216 // $google_fonts_list = array_unique( $google_fonts_list );
217 wp_enqueue_style( 'sp_smart_post-widget_google-fonts', 'https://fonts.googleapis.com/css?family=' . implode( '|', $font_families ), array(), SMART_POST_SHOW_VERSION, 'all' );
218 }
219 }
220 /**
221 * Enqueue combined CSS for widget blocks.
222 *
223 * @param array $block_names Block names array.
224 * @return void
225 */
226 protected function enqueue_widget_block_css( $block_names ) {
227
228 $cssfilepath_static = $this->css_dir . self::STATIC_CSS_DIR;
229
230 // Ensure static directory exists.
231 if ( ! $this->handle_file_operation( 'exists', $cssfilepath_static ) ) {
232 $this->handle_file_operation( 'mkdir', $cssfilepath_static );
233 }
234 $filename = self::CSS_FILE_PREFIX . 'widget.css';
235 $cssfilepath = $cssfilepath_static . $filename;
236 $cssfileurl = $this->css_url . self::STATIC_CSS_DIR . $filename;
237 $file_exists = $this->handle_file_operation( 'exists', $cssfilepath );
238 if ( empty( $block_names ) ) {
239 if ( $file_exists ) {
240 // If CSS is empty and file exists, delete it.
241 $this->handle_file_operation( 'delete', $cssfilepath );
242 delete_option( '_sp_smart_widget_static_css_hash' );
243 }
244 return;
245 }
246 $block_names = $this->flatten_fonts( $block_names );
247 $block_names = array_unique( $block_names );
248 // Normalize block names (apply same transformations as during CSS generation).
249 $normalized_blocks = $this->normalize_widget_blocks( $block_names );
250 sort( $normalized_blocks ); // Sort for consistent comparison.
251 $normalized_blocks_hash = md5( wp_json_encode( $normalized_blocks ) );
252
253 // Get stored block hash for comparison.
254 $stored_blocks_hash = get_option( '_sp_smart_widget_static_css_hash', '' );
255
256 // Regenerate CSS if file doesn't exist or blocks have changed.
257 $needs_regeneration = ! $file_exists || $normalized_blocks_hash !== $stored_blocks_hash;
258
259 if ( $needs_regeneration ) {
260 $loaded_blocks = array();
261 $css = '';
262
263 foreach ( $block_names as $block ) {
264 if ( empty( $block ) ) {
265 continue;
266 }
267
268 $targets = array(
269 'post-slider',
270 'post-slider-two',
271 'post-timeline-three',
272 'thumbnail-slider',
273 'thumbnail-slider-two',
274 );
275 $list_block_targets = array(
276 'post-list-two',
277 'post-list-three',
278 );
279
280 if ( in_array( $block, $targets ) ) {
281 $block = 'post-carousel';
282 }
283 if ( 'post-timeline-two' == $block ) {
284 $block = 'post-timeline-one';
285 }
286 if ( in_array( $block, $list_block_targets ) ) {
287 $block = 'post-list-one';
288 }
289 if ( in_array( $block, $loaded_blocks, true ) ) {
290 continue;
291 }
292 $loaded_blocks[] = $block;
293 $style_file = SP_PC_PATH . "blocks/includes/$block/style.css";
294 if ( $this->handle_file_operation( 'exists', $style_file ) ) {
295 $this->init_filesystem();
296 if ( $this->wp_filesystem ) {
297 $file_content = $this->wp_filesystem->get_contents( $style_file );
298 if ( false !== $file_content ) {
299 $css .= $file_content;
300 }
301 }
302 }
303 }
304 // Write CSS file if we have content, or delete if empty.
305 if ( ! empty( $css ) ) {
306 $this->handle_file_operation( 'write', $cssfilepath, $css );
307 // Store the hash of blocks used to generate this CSS.
308 update_option( '_sp_smart_widget_static_css_hash', $normalized_blocks_hash );
309 } elseif ( $file_exists ) {
310 // If CSS is empty and file exists, delete it.
311 $this->handle_file_operation( 'delete', $cssfilepath );
312 delete_option( '_sp_smart_widget_static_css_hash' );
313 }
314 }
315
316 // Enqueue combined CSS if file exists.
317 if ( $this->handle_file_operation( 'exists', $cssfilepath ) ) {
318 $file_time = file_exists( $cssfilepath ) ? filemtime( $cssfilepath ) : SMART_POST_SHOW_VERSION;
319 wp_enqueue_style(
320 'sp-smart-blocks-combined-widget',
321 $cssfileurl,
322 array(),
323 $file_time
324 );
325 }
326 }
327
328 /**
329 * Normalize widget block names by applying the same transformations used during CSS generation.
330 *
331 * @param array $block_names Array of block names.
332 * @return array Normalized block names.
333 */
334 private function normalize_widget_blocks( $block_names ) {
335 $normalized = array();
336 $targets = array(
337 'post-slider',
338 'post-slider-two',
339 'post-timeline-three',
340 'thumbnail-slider',
341 'thumbnail-slider-two',
342 );
343 $list_block_targets = array(
344 'post-list-two',
345 'post-list-three',
346 );
347
348 foreach ( $block_names as $block ) {
349 if ( empty( $block ) ) {
350 continue;
351 }
352
353 $normalized_block = $block;
354
355 if ( in_array( $block, $targets ) ) {
356 $normalized_block = 'post-carousel';
357 } elseif ( 'post-timeline-two' == $block ) {
358 $normalized_block = 'post-timeline-one';
359 } elseif ( in_array( $block, $list_block_targets ) ) {
360 $normalized_block = 'post-list-one';
361 }
362
363 if ( ! in_array( $normalized_block, $normalized, true ) ) {
364 $normalized[] = $normalized_block;
365 }
366 }
367
368 return $normalized;
369 }
370 /**
371 * Render block callback to enqueue block-specific CSS and fonts.
372 *
373 * @param string $block_content The rendered block content.
374 * @param array $block The block data array.
375 * @return string The rendered block content.
376 */
377 public function render_block_callback( $block_content, $block ) {
378 if ( is_singular() ) {
379 return $block_content;
380 }
381 if ( ! is_admin() && isset( $block['blockName'] ) && strpos( $block['blockName'], 'sp-smart-post-show/' ) === 0 ) {
382 // Get post ID - works for both singular and archive/blog pages.
383 global $post;
384 $current_post_id = get_the_ID();
385 // Fallback to global $post for archive/blog contexts.
386 if ( ! $current_post_id && ! empty( $post->ID ) ) {
387 $current_post_id = $post->ID;
388 }
389 // Skip if no valid post ID found.
390 if ( ! $current_post_id || ! is_numeric( $current_post_id ) ) {
391 return $block_content;
392 }
393 $current_post_id = absint( $current_post_id );
394 $cssfilepath_static = $this->css_dir . self::STATIC_CSS_DIR;
395 $static_filename = self::CSS_FILE_PREFIX . "{$current_post_id}.css";
396 $static_cssfilepath = $cssfilepath_static . $static_filename;
397 $static_cssfileurl = $this->css_url . self::STATIC_CSS_DIR . $static_filename;
398 // Enqueue combined CSS if file exists.
399 if ( $this->handle_file_operation( 'exists', $static_cssfilepath ) ) {
400 $file_time = file_exists( $static_cssfilepath ) ? filemtime( $static_cssfilepath ) : SMART_POST_SHOW_VERSION;
401 wp_enqueue_style(
402 'sp-smart-blocks-combined-' . $current_post_id,
403 $static_cssfileurl,
404 array( 'sp_smart_post_blocks_css' ),
405 $file_time
406 );
407 }
408 $filename = self::CSS_FILE_PREFIX . "{$current_post_id}.css";
409 $cssfilepath = $this->css_dir . $filename;
410 $cssfileurl = $this->css_url . $filename;
411 $css = '';
412 $fonts = array();
413 $block_names = array();
414 if ( $this->handle_file_operation( 'exists', $cssfilepath ) ) {
415 if ( wp_style_is( "sp-post-style-{$current_post_id}", 'enqueued' ) ) {
416 return $block_content;
417 }
418 $file_time = file_exists( $cssfilepath ) ? filemtime( $cssfilepath ) : SMART_POST_SHOW_VERSION;
419 wp_enqueue_style(
420 "sp-post-style-{$current_post_id}",
421 $cssfileurl,
422 array(),
423 $file_time
424 );
425 $this->merge_assets( '', get_post_meta( $current_post_id, '_sp_smart_fonts', true ), $css, $fonts );
426 } else {
427 $current_block_name = get_post_meta( $current_post_id, '_sp_smart_block_names', true );
428 if ( ! empty( $current_block_name ) ) {
429 $block_names[ $current_post_id ] = $current_block_name;
430 $css = get_post_meta( $current_post_id, '_sp_smart_css', true );
431 if ( ! empty( $css ) ) {
432 wp_add_inline_style( 'sp_smart_post_blocks_css', wp_strip_all_tags( $css ) );
433 }
434 }
435 }
436 }
437 return $block_content;
438 }
439 /**
440 * Initialize WordPress file system.
441 *
442 * @return void
443 */
444 private function init_filesystem() {
445 if ( ! $this->wp_filesystem ) {
446 require_once ABSPATH . 'wp-admin/includes/file.php';
447 WP_Filesystem();
448 global $wp_filesystem;
449 $this->wp_filesystem = $wp_filesystem;
450 }
451 }
452
453 /**
454 * Log asset operations for debugging.
455 *
456 * @param string $operation Operation type.
457 * @param string $context Context information.
458 * @param array $details Additional details.
459 * @return void
460 */
461 private function log_asset_operation( $operation, $context, $details = array() ) {
462 if ( defined( 'WP_DEBUG' ) && WP_DEBUG && $this->sps_debug ) {
463 error_log(
464 sprintf(
465 'Smart Post Show Assets: %s - %s - %s',
466 $operation,
467 $context,
468 wp_json_encode( $details )
469 )
470 );
471 }
472 }
473
474 /**
475 * Handle file system operations with error handling.
476 *
477 * @param string $operation Operation type (write, delete, exists).
478 * @param string $filepath File path.
479 * @param string $content Content for write operations.
480 * @return mixed
481 *
482 * @throws \InvalidArgumentException If the operation type is invalid.
483 */
484 private function handle_file_operation( $operation, $filepath, $content = '' ) {
485 try {
486 $this->init_filesystem();
487
488 switch ( $operation ) {
489 case 'write':
490 $result = $this->wp_filesystem->put_contents( $filepath, $content );
491 $this->log_asset_operation(
492 'write',
493 'css_file',
494 array(
495 'path' => $filepath,
496 'success' => $result,
497 )
498 );
499 return $result;
500
501 case 'delete':
502 $result = $this->wp_filesystem->delete( $filepath );
503 $this->log_asset_operation(
504 'delete',
505 'css_file',
506 array(
507 'path' => $filepath,
508 'success' => $result,
509 )
510 );
511 return $result;
512
513 case 'exists':
514 return $this->wp_filesystem->exists( $filepath );
515
516 case 'mkdir':
517 return $this->wp_filesystem->mkdir( $filepath );
518
519 default:
520 throw new \InvalidArgumentException( 'Invalid file operation: ' . $operation );
521 }
522 } catch ( \Exception $e ) {
523 $this->log_asset_operation(
524 'error',
525 'file_operation',
526 array(
527 'operation' => $operation,
528 'error' => $e->getMessage(),
529 )
530 );
531 return false;
532 }
533 }
534
535 /**
536 * Validate CSS file path for security.
537 *
538 * @param string $filename Filename to validate.
539 * @return bool
540 */
541 private function validate_css_path( $filename ) {
542 $filename = sanitize_file_name( $filename );
543 return preg_match( '/^' . self::CSS_FILE_PREFIX . '\d+\.css$/', $filename );
544 }
545
546 /**
547 * Sanitize block parameters for security.
548 *
549 * @param array $params Raw parameters.
550 * @return array Sanitized parameters.
551 */
552 private function sanitize_block_params( $params ) {
553 return array(
554 'post_id' => sanitize_text_field( $params['post_id'] ?? 0 ),
555 'slug' => sanitize_text_field( $params['slug'] ?? '' ),
556 'widget_id' => sanitize_text_field( $params['widget_id'] ?? '' ),
557 'theme' => sanitize_text_field( $params['theme'] ?? '' ),
558 'block_css' => wp_strip_all_tags( $params['block_css'] ?? '' ),
559 'fonts' => array_map( 'sanitize_text_field', (array) ( $params['fonts'] ?? array() ) ),
560 'is_preview' => rest_sanitize_boolean( $params['preview'] ?? false ),
561 'has_refs' => rest_sanitize_boolean( $params['has_refs'] ?? false ),
562 'has_block' => rest_sanitize_boolean( $params['has_block'] ?? false ),
563 'block_ref_id' => sanitize_text_field( $params['ref_id'] ?? 0 ),
564 'block_type' => sanitize_text_field( $params['block_type'] ?? '' ),
565 'block_names' => array_map( 'sanitize_text_field', (array) ( $params['block_names'] ?? array() ) ),
566 'reusable_block_ids' => array_map( 'sanitize_text_field', (array) ( $params['reusable_block_ids'] ?? array() ) ),
567 );
568 }
569
570 /**
571 * Enqueue script and style for the blocks.
572 *
573 * @return void
574 */
575 public function enqueue_assets() {
576 $this->add_global_css();
577 $this->register_styles();
578 $this->register_scripts();
579 $this->localize_scripts();
580 if ( ! is_admin() ) {
581 $this->handle_frontend_assets();
582 }
583 }
584
585 /**
586 * Register all styles.
587 *
588 * @return void
589 */
590 protected function register_styles() {
591 $styles = array(
592 'sp_smart_post_blocks_editor_style' => 'blocks/build/editor/index.css',
593 'sp_smart_post_blocks_social_icons_style' => 'blocks/assets/css/icons.min.css',
594 'sp_smart_post_blocks_css' => 'blocks/build/editor/style-index.css',
595 // 'sp_smart_post_blocks_style' => 'blocks/assets/css/style.css',
596 // 'sp_smart_post_builder_style' => 'blocks/assets/css/builder.css',
597 );
598
599 foreach ( $styles as $handle => $path ) {
600 wp_register_style(
601 $handle,
602 SP_PC_URL . $path,
603 array(),
604 SMART_POST_SHOW_VERSION,
605 'all'
606 );
607 }
608 }
609
610 /**
611 * Register all scripts.
612 *
613 * @return void
614 */
615 protected function register_scripts() {
616 $asset_file = SP_PC_PATH . 'blocks/build/editor/index.asset.php';
617 $asset = file_exists( $asset_file )
618 ? require $asset_file
619 : array(
620 'dependencies' => array(),
621 'version' => SMART_POST_SHOW_VERSION,
622 );
623
624 if ( is_admin() ) {
625 wp_register_script(
626 'sp_smart_post_blocks_index_js',
627 SP_PC_URL . 'blocks/build/editor/index.js',
628 array(),
629 SMART_POST_SHOW_VERSION,
630 true
631 );
632 }
633
634 $scripts = array(
635 'sp_smart_post_blocks_script_js' => 'blocks/assets/js/script.min.js',
636 'sp_smart_post_news_ticker_script' => 'blocks/assets/js/news-ticker.min.js',
637 'sp_video_and_gallery_slider' => 'blocks/assets/js/video-and-gallery-slider.min.js',
638 'sp-smart-post-back-to-top' => 'blocks/assets/js/back-to-top.min.js',
639 'sp_smart_post_smart_toc_script' => 'blocks/assets/js/table-of-content.min.js',
640 'sp_smart_post_build_script' => 'blocks/assets/js/builder-scripts.js',
641 );
642
643 foreach ( $scripts as $handle => $path ) {
644 wp_register_script(
645 $handle,
646 SP_PC_URL . $path,
647 array(),
648 SMART_POST_SHOW_VERSION,
649 true
650 );
651 }
652 }
653
654 /**
655 * Adds global CSS styles to the page.
656 *
657 * @return void
658 */
659 private function add_global_css() {
660 $global_style = get_option( 'sp_smart_post_global_settings', array() );
661 $custom_css = $global_style['customCss'] ?? '';
662 $typography_css = ! empty( $global_style['typography']['typographyCss'] ) ? $global_style['typography']['typographyCss'] : ':root { --sp-smart-font-size-heading-1: 44px; --sp-smart-font-size-heading-2: 32px; --sp-smart-font-size-heading-3: 24px; --sp-smart-font-size-heading-4: 22px; --sp-smart-font-size-heading-5: 20px; --sp-smart-font-size-heading-6: 18px; --sp-smart-font-size-body-1: 18px; --sp-smart-font-size-body-2: 16px; --sp-smart-font-size-body-3: 14px; --sp-smart-font-size-body-4: 12px; --sp-smart-font-size-button-1: 18px; --sp-smart-font-size-button-2: 16px;}';
663 $this->google_fonts = $global_style['typography']['fontList'] ?? '';
664 $root_css = ! empty( $global_style['rootcss'] ) ? $global_style['rootcss'] : ':root{ --sp-smart-breakpoint-tablet: 1023px; --sp-smart-breakpoint-mobile: 767px; --smart-post-light-text: #FAFAFA; --smart-post-background: #FFFFFF; --smart-post-primary-light: #EBEBEB; --smart-post-primary: #999999; --smart-post-primary-dark: #1D1D1D; --smart-post-secondary: #0054FB; --smart-post-dark-2-text: #3E3E3E; --smart-post-dark-text: #0A0A0A; --smart-post-black: #000000;} :root { --smart-post-shadow-subtle-1dp: 0px 1px 2px 0px rgba(0, 0, 0, 0.12); --smart-post-shadow-light-2dp: 0px 2px 4px 0px rgba(0, 0, 0, 0.14); --smart-post-shadow-medium-4dp: 0px 4px 6px 0px rgba(0, 0, 0, 0.16); --smart-post-shadow-strong-8dp: 0px 8px 18px 0px rgba(0, 0, 0, 0.18); --smart-post-shadow-deep-12dp: 0px 12px 17px 0px rgba(0, 0, 0, 0.20); --smart-post-shadow-sharp-4dp: 4px 4px 0px 0px rgba(0, 0, 0, 0.25);}';
665 $shadow_css = $global_style['shadow']['shadowRootCSS'] ?? ':root { --smart-post-shadow-subtle-1dp: 0px 1px 2px 0px rgba(0, 0, 0, 0.12); --smart-post-shadow-light-2dp: 0px 2px 4px 0px rgba(0, 0, 0, 0.14); --smart-post-shadow-medium-4dp: 0px 4px 6px 0px rgba(0, 0, 0, 0.16); --smart-post-shadow-strong-8dp: 0px 8px 18px 0px rgba(0, 0, 0, 0.18); --smart-post-shadow-deep-12dp: 0px 12px 17px 0px rgba(0, 0, 0, 0.20); --smart-post-shadow-sharp-4dp: 4px 4px 0px 0px rgba(0, 0, 0, 0.25);}';
666 $inline_css = wp_strip_all_tags( $typography_css . $root_css . $shadow_css . $custom_css );
667
668 wp_register_style( 'sp-smart-post-global-root', false, array(), SMART_POST_SHOW_VERSION );
669 wp_add_inline_style( 'sp-smart-post-global-root', $inline_css );
670 wp_enqueue_style( 'sp-smart-post-global-root' );
671 }
672
673 /**
674 * Localize scripts with common data.
675 *
676 * @return void
677 */
678 protected function localize_scripts() {
679
680 $ajax_url = is_admin() ? 'sp_smart_post_blocks_index_js' : 'sp_smart_post_blocks_script_js';
681 global $wp;
682 $current_url = is_home() ? home_url() : get_the_permalink();
683 $actives_blocks = Helper::object_to_array( get_option( 'sp-pcp-blocks-setting-options' ) );
684 $always_active_blocks = array(
685 array(
686 'block_name' => 'smart-post-parent',
687 'show' => true,
688 ),
689 array(
690 'block_name' => 'live-filter',
691 'show' => true,
692 ),
693 array(
694 'block_name' => 'search-filter',
695 'show' => true,
696 ),
697 array(
698 'block_name' => 'sort-filter',
699 'show' => true,
700 ),
701 array(
702 'block_name' => 'taxonomy-filter',
703 'show' => true,
704 ),
705 array(
706 'block_name' => 'pagination',
707 'show' => true,
708 ),
709 );
710 $actives_blocks = array_merge( $actives_blocks, $always_active_blocks );
711 wp_localize_script(
712 $ajax_url,
713 'sp_smart_post_block_localize',
714 array(
715 'ajaxUrl' => esc_url_raw( admin_url( 'admin-ajax.php' ) ),
716 'restUrl' => esc_url_raw( rest_url( 'sps-blocks/v2/filter' ) ),
717 'searchRestUrl' => esc_url_raw( rest_url( 'sps-blocks/v2/smart-search' ) ),
718 'ajaxNonce' => wp_create_nonce( 'sp_smart_post_block_nonce' ),
719 'permalink_structure' => $current_url,
720 'pluginUrl' => SP_PC_URL,
721 'uploadFile' => wp_upload_dir(),
722 'integrationOptions' => Helper::get_integration_options(),
723 'placeholderImg' => SP_PC_URL,
724 'getBlockOptions' => $actives_blocks,
725 'cssPath' => $this->css_url,
726 'adminUrl' => admin_url(),
727 'isPro' => true,
728 'modulesOptions' => Helper::get_modules_show_hide(),
729 )
730 );
731 wp_localize_script(
732 'sp_smart_post_smart_search_script',
733 'sp_smart_search_block_localize',
734 array(
735 'searchRestUrl' => esc_url_raw( rest_url( 'sps-blocks/v2/smart-search' ) ),
736 )
737 );
738 }
739
740 /**
741 * Enqueue combined Google Fonts for better performance.
742 *
743 * @param array $fonts Array of font strings.
744 * @return void
745 */
746 private function enqueue_combined_fonts( $fonts ) {
747 if ( empty( $fonts ) ) {
748 return;
749 }
750
751 // Flatten nested arrays and keep only strings.
752 $fonts = array_filter( array_map( 'strval', (array) $this->flatten_fonts( $fonts ) ) );
753
754 if ( empty( $fonts ) ) {
755 return;
756 }
757
758 $font_families = array();
759 foreach ( array_unique( $fonts ) as $font ) {
760 if ( ! is_string( $font ) || '' === $font ) {
761 continue;
762 }
763
764 $font_parts = explode( ':', $font );
765 $font_name = str_replace( ' ', '+', $font_parts[0] );
766 $font_weights = $font_parts[1] ?? '400';
767 if ( empty( $font_name ) ) {
768 continue;
769 }
770 $font_families[] = $font_name . ':' . $font_weights;
771 }
772
773 if ( ! empty( $font_families ) ) {
774 $font_url = '//fonts.googleapis.com/css?family=' . implode( '|', $font_families );
775 wp_enqueue_style(
776 'sp-smart-post-google-fonts',
777 $font_url,
778 array(),
779 SMART_POST_SHOW_VERSION
780 );
781
782 $this->log_asset_operation( 'enqueue', 'google_fonts', array( 'count' => count( $font_families ) ) );
783 }
784 }
785
786 /**
787 * Get All Reusable IDs content.
788 *
789 * @param int $post_id Post ids.
790 * @return array
791 */
792 public function get_reusable_ids( $post_id ) {
793 $reusable_id = array();
794 if ( $post_id ) {
795 $post = get_post( $post_id );
796 if ( isset( $post->post_content ) ) {
797 if ( has_blocks( $post->post_content ) &&
798 strpos( $post->post_content, 'wp:block' ) &&
799 strpos( $post->post_content, '"ref"' ) !== false
800 ) {
801 $blocks = parse_blocks( $post->post_content );
802 foreach ( $blocks as $key => $value ) {
803 if ( isset( $value['attrs']['ref'] ) ) {
804 $reusable_id[] = $value['attrs']['ref'];
805 }
806 }
807 }
808 }
809 }
810 return $reusable_id;
811 }
812
813 /**
814 * Get All Shortcode IDs content.
815 *
816 * @param int $post_id post id.
817 * @return array
818 */
819 public function get_shortcode_ids( $post_id ) {
820 $shortcode_ids = array();
821
822 $saved_template_count = wp_count_posts( 'sp_post_template' );
823 $total_saved_template = $saved_template_count->publish ?? 0;
824
825 if ( $post_id && 0 !== $total_saved_template ) {
826 $post = get_post( $post_id );
827
828 if ( isset( $post->post_content ) && ! empty( $post->post_content ) ) {
829 // Regular expression to find [smart_post id="1234"].
830 preg_match_all( '/\[smart_post\s+id=["\'](\d+)["\']\]/', $post->post_content, $matches );
831
832 if ( ! empty( $matches[1] ) ) {
833 $shortcode_ids = $matches[1]; // all captured IDs.
834 }
835 }
836 }
837
838 return $shortcode_ids;
839 }
840
841 /**
842 * Get WordPress options starting with a given key prefix,
843 * including the exact match of the prefix itself.
844 *
845 * @param string $prefix The option_name prefix to search for.
846 * @return array Array of options (option_name => option_value).
847 */
848 public function get_options_by_prefix( $prefix ) {
849 global $wpdb;
850
851 // Add % for SQL LIKE, but also allow exact match.
852 $like_key = $wpdb->esc_like( $prefix ) . '%';
853 $results = $wpdb->get_results(
854 $wpdb->prepare(
855 "SELECT option_name, option_value
856 FROM {$wpdb->options}
857 WHERE option_name = %s OR option_name LIKE %s",
858 $prefix,
859 $like_key
860 )
861 );
862 $options = array();
863 if ( $results ) {
864 foreach ( $results as $row ) {
865 $options[ $row->option_name ] = $row->option_value;
866 }
867 }
868
869 return $options;
870 }
871
872 /**
873 * Load frontend dynamic CSS and fonts for widgets, templates, posts, and reusable blocks.
874 */
875 protected function handle_frontend_assets() {
876 global $post, $wp_registered_sidebars;
877 $current_post_id = ! empty( $post->ID ) ? $post->ID : 0;
878 $css = '';
879 $fonts = array();
880 $block_names = array();
881
882 // Handle different asset sources.
883 $this->handle_template_assets( $current_post_id, $css, $fonts, $block_names );
884 $this->handle_post_assets( $current_post_id, $css, $fonts, $block_names );
885 $this->handle_preview_assets( $current_post_id, $css, $fonts, $block_names );
886 // Enqueue collected assets.
887 $this->enqueue_collected_assets( $css, $fonts, $block_names, $current_post_id );
888 }
889
890 /**
891 * Handle template assets.
892 *
893 * @param int $current_post_id Current post ID.
894 * @param string $css CSS accumulator.
895 * @param array $fonts Fonts accumulator.
896 * @param array $block_names Block names accumulator.
897 * @return void
898 */
899 private function handle_template_assets( $current_post_id, &$css, &$fonts, &$block_names ) {
900 $theme_slug = wp_get_theme()->get_stylesheet();
901 $template_contexts = array( 'header', 'footer' );
902
903 // Determine current context.
904 if ( is_home() ) {
905 $template_contexts[] = 'home';
906 } elseif ( is_archive() ) {
907 $template_contexts[] = 'archive';
908 } elseif ( is_single() ) {
909 $template_contexts[] = 'single';
910 } elseif ( is_page() ) {
911 $template_contexts[] = 'page';
912 }
913
914 global $_wp_current_template_content;
915 $template_parts = array();
916 // Ensure variable exists and not empty.
917 if ( ! empty( $_wp_current_template_content ) ) {
918 // Define the parts we want to detect dynamically.
919 $template_parts = array( 'footer' );
920 foreach ( $template_parts as $part ) {
921 if ( strpos( $_wp_current_template_content, '"slug":"' . $part . '"' ) !== false ) {
922 $template_contexts[] = $part;
923 }
924 }
925 if ( ! empty( $_wp_current_template_content ) ) {
926 // Match all occurrences of "slug":"something".
927 preg_match_all( '/"slug":"([^"]+)"/', $_wp_current_template_content, $matches );
928
929 if ( ! empty( $matches[1] ) ) {
930 $template_parts = array_unique( $matches[1] ); // unique parts only.
931 }
932 }
933 }
934
935 // Remove duplicates just in case.
936 $template_contexts = array_merge( $template_contexts, $template_parts );
937 $template_contexts = array_unique( $template_contexts );
938 // Handle custom template.
939 $custom_template_slug = get_page_template_slug( $current_post_id );
940 if ( ! empty( $custom_template_slug ) ) {
941 $this->handle_template_context( $theme_slug, $custom_template_slug, $css, $fonts, $block_names );
942 }
943
944 // Handle standard template contexts.
945 foreach ( $template_contexts as $context ) {
946 $this->handle_template_context( $theme_slug, $context, $css, $fonts, $block_names );
947 }
948 }
949
950 /**
951 * Handle a specific template context.
952 *
953 * @param string $theme_slug Theme slug.
954 * @param string $context Template context.
955 * @param string $css CSS accumulator.
956 * @param array $fonts Fonts accumulator.
957 * @param array $block_names Block names accumulator.
958 * @return void
959 */
960 private function handle_template_context( $theme_slug, $context, &$css, &$fonts, &$block_names ) {
961 $current_block_name = get_option( "_sp_smart_template_block_names{$theme_slug}{$context}", array() );
962 if ( ! empty( $current_block_name ) ) {
963 $block_names[ $context ] = $current_block_name;
964 $this->merge_assets(
965 get_option( "_sp_smart_template_{$theme_slug}{$context}", '' ),
966 get_option( "_sp_smart_fonts_template_{$theme_slug}{$context}", array() ),
967 $css,
968 $fonts
969 );
970 }
971
972 // Handle reusable blocks in templates.
973 $reused_ids = (array) get_option( "_sp_smart_template_reused_blocks_{$theme_slug}{$context}", array() );
974 foreach ( $reused_ids as $id ) {
975 $this->handle_reusable_block_assets( $id, $css, $fonts, $block_names );
976 }
977 }
978
979 /**
980 * Handle post assets.
981 *
982 * @param int $current_post_id Current post ID.
983 * @param string $css CSS accumulator.
984 * @param array $fonts Fonts accumulator.
985 * @param array $block_names Block names accumulator.
986 * @return void
987 */
988 private function handle_post_assets( $current_post_id, &$css, &$fonts, &$block_names ) {
989 if ( ! is_singular() || ! $current_post_id ) {
990 return;
991 }
992 $filename = self::CSS_FILE_PREFIX . "{$current_post_id}.css";
993 $cssfilepath = $this->css_dir . $filename;
994 $cssfileurl = $this->css_url . $filename;
995
996 if ( $this->handle_file_operation( 'exists', $cssfilepath ) ) {
997 $file_time = file_exists( $cssfilepath ) ? filemtime( $cssfilepath ) : SMART_POST_SHOW_VERSION;
998 wp_enqueue_style(
999 "sp-post-style-{$current_post_id}",
1000 $cssfileurl,
1001 array(),
1002 $file_time
1003 );
1004 $this->merge_assets( '', get_post_meta( $current_post_id, '_sp_smart_fonts', true ), $css, $fonts );
1005 $current_block_name = get_post_meta( $current_post_id, '_sp_smart_block_names', true );
1006 if ( ! empty( $current_block_name ) ) {
1007 $block_names[ $current_post_id ] = $current_block_name;
1008 }
1009 } else {
1010 $current_block_name = get_post_meta( $current_post_id, '_sp_smart_block_names', true );
1011 if ( ! empty( $current_block_name ) ) {
1012 $block_names[ $current_post_id ] = $current_block_name;
1013 $this->merge_assets(
1014 get_post_meta( $current_post_id, '_sp_smart_css', true ),
1015 get_post_meta( $current_post_id, '_sp_smart_fonts', true ),
1016 $css,
1017 $fonts
1018 );
1019 }
1020 }
1021
1022 // Handle reusable blocks in post.
1023 foreach ( $this->get_reusable_ids( $current_post_id ) as $ref_id ) {
1024 $this->handle_reusable_block_assets( $ref_id, $css, $fonts, $block_names );
1025 }
1026
1027 // Handle reusable blocks in post.
1028 foreach ( $this->get_shortcode_ids( $current_post_id ) as $ref_id ) {
1029 $this->handle_reusable_block_assets( $ref_id, $css, $fonts, $block_names );
1030 }
1031 }
1032
1033 /**
1034 * Handle reusable block assets.
1035 *
1036 * @param int $ref_id Reusable block ID.
1037 * @param string $css CSS accumulator.
1038 * @param array $fonts Fonts accumulator.
1039 * @param array $block_names Block names accumulator.
1040 * @return void
1041 */
1042 private function handle_reusable_block_assets( $ref_id, &$css, &$fonts, &$block_names ) {
1043 $current_block_name = get_post_meta( $ref_id, '_sp_smart_block_names', true );
1044 if ( empty( $current_block_name ) ) {
1045 return;
1046 }
1047
1048 $this->merge_assets(
1049 get_post_meta( $ref_id, '_sp_smart_css', true ),
1050 get_post_meta( $ref_id, '_sp_smart_fonts', true ),
1051 $css,
1052 $fonts
1053 );
1054 $block_names[ $ref_id ] = $current_block_name;
1055 }
1056
1057 /**
1058 * Handle preview assets.
1059 *
1060 * @param int $current_post_id Current post ID.
1061 * @param string $css CSS accumulator.
1062 * @param array $fonts Fonts accumulator.
1063 * @param array $block_names Block names accumulator.
1064 * @return void
1065 */
1066 private function handle_preview_assets( $current_post_id, &$css, &$fonts, &$block_names ) {
1067 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1068 if ( isset( $_GET['preview_id'] ) && isset( $_GET['preview_nonce'] ) ) {
1069 $preview_id = absint( $_GET['preview_id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1070 $preview_nonce = sanitize_text_field( wp_unslash( $_GET['preview_nonce'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1071 // Verify nonce for security.
1072 if ( ! wp_verify_nonce( $preview_nonce, 'post_preview_' . $preview_id ) ) {
1073 return;
1074 }
1075 $this->merge_assets(
1076 get_transient( '_sps_preview_' . $current_post_id ),
1077 get_transient( '_sps_preview_fonts_' . $current_post_id ),
1078 $css,
1079 $fonts
1080 );
1081 $block_names['preview'] = get_transient( '_sps_preview_block_name' . $current_post_id );
1082 }
1083 }
1084
1085 /**
1086 * Enqueue collected assets.
1087 *
1088 * @param string $css CSS content.
1089 * @param array $fonts Fonts array.
1090 * @param array $block_names Block names.
1091 * @param int $current_post_id Current post ID.
1092 * @return void
1093 */
1094 private function enqueue_collected_assets( $css, $fonts, $block_names, $current_post_id ) {
1095 // Enqueue inline CSS.
1096 if ( ! empty( $css ) ) {
1097 wp_add_inline_style( 'sp_smart_post_blocks_css', wp_strip_all_tags( $css ) );
1098 }
1099 // Merge with global Google Fonts.
1100 if ( ! empty( $this->google_fonts ) ) {
1101 $fonts = ! empty( $fonts ) ? array_merge( (array) $fonts, (array) $this->google_fonts ) : $this->google_fonts;
1102 }
1103 // Enqueue Google Fonts.
1104 $this->enqueue_combined_fonts( $fonts );
1105 // Enqueue combined block CSS.
1106 $this->enqueue_combined_block_css( $block_names, $current_post_id );
1107 }
1108
1109 /**
1110 * Merge CSS and fonts into accumulators.
1111 *
1112 * @param string $css_source CSS source.
1113 * @param array $fonts_source Fonts source.
1114 * @param string $css CSS accumulator.
1115 * @param array $fonts Fonts accumulator.
1116 * @return void
1117 */
1118 private function merge_assets( $css_source, $fonts_source, &$css, &$fonts ) {
1119 if ( ! empty( $css_source ) ) {
1120 $css .= $css_source;
1121 }
1122 if ( ! empty( $fonts_source ) && is_array( $fonts_source ) ) {
1123 $fonts = array_merge( $fonts, $fonts_source );
1124 }
1125 }
1126
1127 /**
1128 * Enqueue combined block CSS.
1129 *
1130 * @param array $block_names Block names.
1131 * @param int $current_post_id Current post ID.
1132 * @return void
1133 */
1134 public function enqueue_combined_block_css( $block_names, $current_post_id = 0 ) {
1135 if ( empty( $block_names ) ) {
1136 return;
1137 }
1138
1139 $cssfilepath_static = $this->css_dir . self::STATIC_CSS_DIR;
1140
1141 // Ensure static directory exists.
1142 if ( ! $this->handle_file_operation( 'exists', $cssfilepath_static ) ) {
1143 $this->handle_file_operation( 'mkdir', $cssfilepath_static );
1144 }
1145
1146 $loaded_blocks = array();
1147 foreach ( $block_names as $key => $block_part ) {
1148 if ( empty( $block_part ) ) {
1149 continue;
1150 }
1151
1152 $filename = self::CSS_FILE_PREFIX . "{$key}.css";
1153 $cssfilepath = $cssfilepath_static . $filename;
1154 $cssfileurl = $this->css_url . self::STATIC_CSS_DIR . $filename;
1155
1156 if ( ! $this->handle_file_operation( 'exists', $cssfilepath ) ) {
1157 $css = '';
1158 $block_part = $this->flatten_fonts( $block_part );
1159 $targets = array(
1160 'post-slider',
1161 'post-slider-two',
1162 'post-timeline-three',
1163 'thumbnail-slider',
1164 'thumbnail-slider-two',
1165 );
1166 $list_block_targets = array(
1167 'post-list-two',
1168 'post-list-three',
1169 );
1170
1171 if ( array_intersect( $targets, $block_part ) ) {
1172 $block_part[] = 'post-carousel';
1173 }
1174 if ( in_array( 'post-timeline-two', $block_part, true ) ) {
1175 $block_part[] = 'post-timeline-one';
1176 }
1177 if ( array_intersect( $list_block_targets, $block_part ) ) {
1178 $block_part[] = 'post-list-one';
1179 }
1180
1181 foreach ( $block_part as $block ) {
1182 if ( in_array( $block, $loaded_blocks, true ) ) {
1183 continue;
1184 }
1185
1186 $loaded_blocks[] = $block;
1187 $style_file = SP_PC_PATH . "blocks/includes/$block/style.css";
1188 if ( $this->handle_file_operation( 'exists', $style_file ) ) {
1189 $this->init_filesystem();
1190 if ( $this->wp_filesystem ) {
1191 $file_content = $this->wp_filesystem->get_contents( $style_file );
1192 if ( false !== $file_content ) {
1193 $css .= $file_content;
1194 }
1195 }
1196 }
1197 }
1198
1199 if ( ! empty( $css ) ) {
1200 $this->handle_file_operation( 'write', $cssfilepath, $css );
1201 }
1202 }
1203
1204 // Enqueue combined CSS if file exists.
1205 if ( $this->handle_file_operation( 'exists', $cssfilepath ) ) {
1206 $file_time = file_exists( $cssfilepath ) ? filemtime( $cssfilepath ) : SMART_POST_SHOW_VERSION;
1207 wp_enqueue_style(
1208 'sp-smart-blocks-combined-' . $key,
1209 $cssfileurl,
1210 array(),
1211 $file_time
1212 );
1213 }
1214 }
1215 }
1216
1217 /**
1218 * Helper: Flatten nested font arrays.
1219 */
1220 /**
1221 * Flatten nested font values (arrays, nested arrays, scalar values) into a flat array of strings.
1222 *
1223 * @param array $fonts Nested font values.
1224 * @return string[]
1225 */
1226 private function flatten_fonts( $fonts ) {
1227 $result = array();
1228 $stack = (array) $fonts;
1229
1230 while ( ! empty( $stack ) ) {
1231 $item = array_shift( $stack );
1232
1233 if ( is_array( $item ) ) {
1234 // push each element back onto stack for processing.
1235 foreach ( $item as $v ) {
1236 $stack[] = $v;
1237 }
1238 continue;
1239 }
1240
1241 // Keep scalars (string / number) only.
1242 if ( is_scalar( $item ) ) {
1243 $result[] = (string) $item;
1244 }
1245 }
1246
1247 return $result;
1248 }
1249
1250 /**
1251 * Get active blocks for the current post.
1252 *
1253 * @param int $post_id Current post ID.
1254 * @param array $our_blocks Registered blocks.
1255 * @return array
1256 */
1257 public static function get_active_blocks( $post_id, $our_blocks ) {
1258 $active_blocks = array();
1259 $content = get_post_field( 'post_content', $post_id );
1260 foreach ( $our_blocks as $block ) {
1261 if ( has_block( $block, $content ) ) {
1262 $active_blocks[] = $block;
1263 }
1264 }
1265
1266 return $active_blocks;
1267 }
1268
1269 /**
1270 * Save block CSS to a file.
1271 *
1272 * @param int $post_id Post ID.
1273 * @param string $css CSS content.
1274 * @return bool
1275 */
1276 public function save_block_css( $post_id, $css ) {
1277 // Validate post ID.
1278 if ( ! is_numeric( $post_id ) || $post_id <= 0 ) {
1279 $this->log_asset_operation(
1280 'error',
1281 'save_css',
1282 array(
1283 'post_id' => $post_id,
1284 'error' => 'Invalid post ID',
1285 )
1286 );
1287 return false;
1288 }
1289
1290 // Ensure CSS directory exists.
1291 if ( ! $this->handle_file_operation( 'exists', $this->css_dir ) ) {
1292 $this->handle_file_operation( 'mkdir', $this->css_dir );
1293 }
1294
1295 $filename = self::CSS_FILE_PREFIX . "{$post_id}.css";
1296 $filepath = $this->css_dir . $filename;
1297
1298 // Validate filename for security.
1299 if ( ! $this->validate_css_path( $filename ) ) {
1300 $this->log_asset_operation(
1301 'error',
1302 'save_css',
1303 array(
1304 'filename' => $filename,
1305 'error' => 'Invalid filename',
1306 )
1307 );
1308 return false;
1309 }
1310
1311 // Save CSS to file.
1312 $result = $this->handle_file_operation( 'write', $filepath, $css );
1313 $this->log_asset_operation(
1314 'save',
1315 'css_file',
1316 array(
1317 'post_id' => $post_id,
1318 'success' => $result,
1319 )
1320 );
1321
1322 return $result;
1323 }
1324
1325 /**
1326 * Delete block CSS file.
1327 *
1328 * @param int $post_id Post ID.
1329 * @param \WP_Post $post Post object (optional, for hook compatibility).
1330 * @return void
1331 */
1332 public function delete_block_css( $post_id, $post = null ) {
1333 $filename = self::CSS_FILE_PREFIX . "{$post_id}.css";
1334 $filepath = $this->css_dir . $filename;
1335
1336 // Validate filename for security.
1337 if ( $this->handle_file_operation( 'exists', $filepath ) ) {
1338 $this->handle_file_operation( 'delete', $filepath );
1339 $this->log_asset_operation( 'delete', 'css_file', array( 'post_id' => $post_id ) );
1340 }
1341 $this->delete_block_static_css( $filename );
1342 delete_post_meta( $post_id, '_sp_smart_post_google_fonts' );
1343 }
1344 /**
1345 * Delete static block CSS file.
1346 *
1347 * @param string $filename File name.
1348 * @return void
1349 */
1350 public function delete_block_static_css( $filename ) {
1351 $filepath = $this->css_dir . self::STATIC_CSS_DIR . $filename;
1352
1353 // Validate filename for security.
1354 if ( $this->handle_file_operation( 'exists', $filepath ) ) {
1355 $this->handle_file_operation( 'delete', $filepath );
1356 $this->log_asset_operation( 'delete', 'static_css_file', array( 'filename' => $filename ) );
1357 }
1358 }
1359 /**
1360 * Check permission.
1361 *
1362 * @param mixed $post_id Post ID.
1363 * @param mixed $cap Capability.
1364 * @return bool
1365 */
1366 public function permission_check( $post_id = false, $cap = '' ) {
1367 $cap = $cap ? $cap : 'edit_others_posts';
1368 $is_passed = false;
1369 if ( $post_id ) {
1370 $post_author = (int) get_post_field( 'post_author', $post_id );
1371 $is_passed = (int) get_current_user_id() === $post_author;
1372 }
1373 return $is_passed || current_user_can( $cap );
1374 }
1375 /**
1376 * Save block CSS via REST API.
1377 *
1378 * @param \WP_REST_Request $request Full data about the request.
1379 * @return \WP_REST_Response
1380 *
1381 * @throws \Exception If CSS file saving fails due to file permission issues.
1382 */
1383 public function save_block_content_css( $request ) {
1384 $params = $request->get_params();
1385 // Sanitize all parameters.
1386 $sanitized_params = $this->sanitize_block_params( $params );
1387 extract( $sanitized_params ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
1388
1389 $capability = 'publish_posts';
1390 if ( ! $this->permission_check( is_numeric( $post_id ) ? $post_id : false, $capability ) ) {
1391 $this->log_asset_operation(
1392 'error',
1393 'permission_check',
1394 array(
1395 'post_id' => $post_id,
1396 'capability' => $capability,
1397 )
1398 );
1399 return new \WP_REST_Response(
1400 array(
1401 'success' => false,
1402 'message' => __( 'Insufficient permissions.', 'post-carousel' ),
1403 ),
1404 403
1405 );
1406 }
1407 // Handle widget CSS storage.
1408 if ( ! empty( $widget_id ) ) {
1409 $storage_key = '_sp_smart_widget_' . $widget_id;
1410 $font_key = '_sp_smart_fonts_widget_' . $widget_id;
1411 $block_names_key = '_sp_smart_widget_block_name' . $widget_id;
1412 $this->delete_block_static_css( self::CSS_FILE_PREFIX . "{$widget_id}.css" );
1413 if ( $has_block ) {
1414 update_option( $storage_key, $block_css );
1415 update_option( $font_key, $fonts );
1416 update_option( $block_names_key, $block_names );
1417 $this->log_asset_operation( 'save', 'widget_css', array( 'widget_id' => $widget_id ) );
1418 } else {
1419 delete_option( $storage_key );
1420 delete_option( $font_key );
1421 delete_option( $block_names_key );
1422 $this->log_asset_operation( 'delete', 'widget_css', array( 'widget_id' => $widget_id ) );
1423 }
1424 return new \WP_REST_Response(
1425 array(
1426 'success' => true,
1427 'message' => __( 'Widget CSS saved successfully.', 'post-carousel' ),
1428 )
1429 );
1430 }
1431 // Handle template and template part CSS storage.
1432 if ( ( 'wp_template' === $post_id || 'wp_template_part' === $post_id ) && 'wp_block' !== $block_type ) {
1433 $storage_key = '_sp_smart_template_' . $theme . $slug;
1434 $font_key = '_sp_smart_fonts_template_' . $theme . $slug;
1435 $block_names_key = '_sp_smart_template_block_names' . $theme . $slug;
1436 $this->delete_block_static_css( self::CSS_FILE_PREFIX . "{$slug}.css" );
1437 if ( ! $has_refs ) {
1438 delete_option( '_sp_smart_template_reused_blocks_' . $theme . $slug );
1439 }
1440 if ( $has_block ) {
1441 update_option( $storage_key, $block_css );
1442 update_option( $font_key, $fonts );
1443 update_option( $block_names_key, $block_names );
1444 $this->log_asset_operation(
1445 'save',
1446 'template_css',
1447 array(
1448 'theme' => $theme,
1449 'slug' => $slug,
1450 )
1451 );
1452 } else {
1453 delete_option( $storage_key );
1454 delete_option( $font_key );
1455 delete_option( $block_names_key );
1456 $this->log_asset_operation(
1457 'delete',
1458 'template_css',
1459 array(
1460 'theme' => $theme,
1461 'slug' => $slug,
1462 )
1463 );
1464 }
1465 return new \WP_REST_Response(
1466 array(
1467 'success' => $block_type,
1468 'message' => __( 'Template CSS saved successfully.', 'post-carousel' ),
1469 )
1470 );
1471 }
1472
1473 // Handle regular posts and custom post types.
1474 if ( ! empty( $block_css ) ) {
1475 // Handle preview mode.
1476 if ( $is_preview ) {
1477 set_transient( '_sps_preview_' . $post_id, $block_css, self::PREVIEW_TRANSIENT_EXPIRATION );
1478 set_transient( '_sps_preview_fonts_' . $post_id, $fonts, self::PREVIEW_TRANSIENT_EXPIRATION );
1479 set_transient( '_sps_preview_block_name' . $post_id, $block_names, self::PREVIEW_TRANSIENT_EXPIRATION );
1480 $this->log_asset_operation( 'preview', 'css_saved', array( 'post_id' => $post_id ) );
1481 return new \WP_REST_Response(
1482 array(
1483 'success' => true,
1484 'message' => __( 'Preview CSS saved temporarily.', 'post-carousel' ),
1485 )
1486 );
1487 }
1488 if ( 'wp_block' === $block_type ) {
1489 if ( 'wp_template' === $post_id || 'wp_template_part' === $post_id ) {
1490 $wp_block_used_key = '_sp_smart_template_reused_blocks_' . $theme . $slug;
1491 // Always cast to array when reading.
1492 $old_reuseable_block = get_option( $wp_block_used_key, array() );
1493 if ( ! is_array( $old_reuseable_block ) ) {
1494 $old_reuseable_block = array();
1495 }
1496
1497 // Add the new ID.
1498 $old_reuseable_block[] = $block_ref_id;
1499
1500 // Keep only items that exist in $reusable_block_ids.
1501 $old_reuseable_block = array_intersect( $reusable_block_ids, $old_reuseable_block );
1502
1503 // Remove duplicates and reindex.
1504 $old_reuseable_block = array_values( array_unique( $old_reuseable_block ) );
1505
1506 // Always update as array.
1507 update_option( $wp_block_used_key, $old_reuseable_block );
1508 }
1509 $post_id = $block_ref_id;
1510 }
1511 // Validate post ID.
1512 if ( ! is_numeric( $post_id ) ) {
1513 return new \WP_REST_Response(
1514 array(
1515 'success' => false,
1516 'message' => __( 'Invalid post ID.', 'post-carousel' ),
1517 ),
1518 400
1519 );
1520 }
1521
1522 // Save CSS to file.
1523 try {
1524 if ( 'wp_block' === $block_type ) {
1525 // Handle reusable blocks - save meta data only.
1526 update_post_meta( $block_ref_id, '_sps_smart_active', 'yes' );
1527 update_post_meta( $block_ref_id, '_sp_smart_css', $block_css );
1528 update_post_meta( $block_ref_id, '_sp_smart_fonts', $fonts );
1529 update_post_meta( $block_ref_id, '_sp_smart_block_names', $block_names );
1530 $this->log_asset_operation( 'save', 'reusable_block', array( 'block_ref_id' => $block_ref_id ) );
1531 $result = true;
1532 } else {
1533 $result = $this->save_block_css( $post_id, $block_css );
1534 $this->delete_block_static_css( self::CSS_FILE_PREFIX . "{$post_id}.css" );
1535 if ( $result ) {
1536 update_post_meta( $post_id, '_sps_smart_active', 'yes' );
1537 update_post_meta( $post_id, '_sp_smart_css', $block_css );
1538 update_post_meta( $post_id, '_sp_smart_fonts', $fonts );
1539 update_post_meta( $post_id, '_sp_smart_block_names', $block_names );
1540 $this->log_asset_operation( 'save', 'post_css', array( 'post_id' => $post_id ) );
1541 return new \WP_REST_Response(
1542 array(
1543 'success' => true,
1544 'message' => __( 'CSS file saved successfully.', 'post-carousel' ),
1545 )
1546 );
1547 } else {
1548 throw new \Exception( __( 'CSS could not be saved due to file permission issues.', 'post-carousel' ) );
1549 }
1550 }
1551 } catch ( \Exception $e ) {
1552 return new \WP_REST_Response(
1553 array(
1554 'success' => false,
1555 'message' => $e->getMessage(),
1556 ),
1557 500
1558 );
1559 }
1560 } else {
1561 if ( 'wp_block' === $block_type ) {
1562 $post_id = $block_ref_id;
1563 }
1564 try {
1565 delete_post_meta( $post_id, '_sps_smart_active' );
1566 delete_post_meta( $post_id, '_sp_smart_css' );
1567 delete_post_meta( $post_id, '_sp_smart_fonts' );
1568 delete_post_meta( $post_id, '_sp_smart_block_names' );
1569 $this->delete_block_static_css( self::CSS_FILE_PREFIX . "{$post_id}.css" );
1570 $filename = self::CSS_FILE_PREFIX . "{$post_id}.css";
1571 $filepath = $this->css_dir . $filename;
1572 if ( $this->handle_file_operation( 'exists', $filepath ) ) {
1573 $this->handle_file_operation( 'delete', $filepath );
1574 }
1575 $this->log_asset_operation( 'delete', 'post_css', array( 'post_id' => $post_id ) );
1576 return new \WP_REST_Response(
1577 array(
1578 'success' => true,
1579 'message' => __( 'CSS file deleted successfully.', 'post-carousel' ),
1580 )
1581 );
1582 } catch ( \Exception $e ) {
1583 return new \WP_REST_Response(
1584 array(
1585 'success' => false,
1586 'message' => $e->getMessage(),
1587 ),
1588 500
1589 );
1590 }
1591 }
1592 }
1593
1594 /*
1595 ---------------------------------------------------------------------------------
1596 | Bulk CSS Operations
1597 -------------------------------------------------------------------------------
1598 * This section handles bulk operations for CSS files associated with Smart Post blocks.
1599 * It includes functions to regenerate, clear, and export CSS files for multiple posts.
1600 */
1601 /**
1602 * Regenerate CSS files for all posts with Smart Post blocks.
1603 *
1604 * @param array $post_ids Optional array of specific post IDs to regenerate.
1605 * @return array Results array with success/failure counts.
1606 */
1607 public function regenerate_all_css_files( $post_ids = array() ) {
1608 $this->log_asset_operation( 'start', 'bulk_regeneration', array( 'post_count' => count( $post_ids ) ) );
1609
1610 $results = array(
1611 'success' => 0,
1612 'failed' => 0,
1613 'errors' => array(),
1614 );
1615
1616 // If no specific post IDs provided, get all posts with Smart Post blocks.
1617 if ( empty( $post_ids ) ) {
1618 $post_ids = $this->get_posts_with_smart_blocks();
1619 }
1620
1621 foreach ( $post_ids as $post_id ) {
1622 try {
1623 $css_content = get_post_meta( $post_id, '_sp_smart_css', true );
1624 if ( ! empty( $css_content ) ) {
1625 $success = $this->save_block_css( $post_id, $css_content );
1626 if ( $success ) {
1627 ++$results['success'];
1628 $this->log_asset_operation(
1629 'regenerate',
1630 'css_file',
1631 array(
1632 'post_id' => $post_id,
1633 'success' => true,
1634 )
1635 );
1636 } else {
1637 ++$results['failed'];
1638 $results['errors'][] = sprintf( 'Failed to regenerate CSS for post ID: %d', $post_id );
1639 }
1640 }
1641 } catch ( \Exception $e ) {
1642 ++$results['failed'];
1643 $results['errors'][] = sprintf( 'Error regenerating CSS for post ID %d: %s', $post_id, $e->getMessage() );
1644 $this->log_asset_operation(
1645 'error',
1646 'css_regeneration',
1647 array(
1648 'post_id' => $post_id,
1649 'error' => $e->getMessage(),
1650 )
1651 );
1652 }
1653 }
1654
1655 $this->log_asset_operation( 'complete', 'bulk_regeneration', $results );
1656 return $results;
1657 }
1658
1659 /**
1660 * Get all posts that have Smart Post blocks.
1661 *
1662 * @return array Array of post IDs.
1663 */
1664 public function get_posts_with_smart_blocks() {
1665 global $wpdb;
1666
1667 $post_ids = $wpdb->get_col(
1668 $wpdb->prepare(
1669 "SELECT DISTINCT post_id
1670 FROM {$wpdb->postmeta}
1671 WHERE meta_key = %s AND meta_value = %s",
1672 '_sps_smart_active',
1673 'yes'
1674 )
1675 );
1676
1677 return array_map( 'absint', $post_ids );
1678 }
1679
1680 /**
1681 * Clear all generated CSS files.
1682 *
1683 * @param bool $include_static Whether to include static CSS files.
1684 * @return array Results array.
1685 */
1686 public function clear_all_css_files( $include_static = true ) {
1687 $this->log_asset_operation( 'start', 'bulk_cleanup', array( 'include_static' => $include_static ) );
1688
1689 $results = array(
1690 'deleted' => 0,
1691 'failed' => 0,
1692 'errors' => array(),
1693 );
1694
1695 // Clear main CSS files.
1696 $css_files = glob( $this->css_dir . self::CSS_FILE_PREFIX . '*.css' );
1697 if ( $css_files ) {
1698 foreach ( $css_files as $file ) {
1699 if ( $this->handle_file_operation( 'delete', $file ) ) {
1700 ++$results['deleted'];
1701 } else {
1702 ++$results['failed'];
1703 $results['errors'][] = sprintf( 'Failed to delete: %s', basename( $file ) );
1704 }
1705 }
1706 }
1707
1708 // Clear static CSS files if requested.
1709 if ( $include_static ) {
1710 $static_dir = $this->css_dir . self::STATIC_CSS_DIR;
1711 if ( $this->handle_file_operation( 'exists', $static_dir ) ) {
1712 $static_files = glob( $static_dir . self::CSS_FILE_PREFIX . '*.css' );
1713 if ( $static_files ) {
1714 foreach ( $static_files as $file ) {
1715 if ( $this->handle_file_operation( 'delete', $file ) ) {
1716 ++$results['deleted'];
1717 } else {
1718 ++$results['failed'];
1719 $results['errors'][] = sprintf( 'Failed to delete static file: %s', basename( $file ) );
1720 }
1721 }
1722 }
1723 }
1724 }
1725
1726 $this->log_asset_operation( 'complete', 'bulk_cleanup', $results );
1727 return $results;
1728 }
1729
1730 /**
1731 * Export CSS data for backup/migration.
1732 *
1733 * @param array $post_ids Optional array of specific post IDs to export.
1734 * @return array Export data array.
1735 */
1736 public function export_css_data( $post_ids = array() ) {
1737 $this->log_asset_operation( 'start', 'css_export', array( 'post_count' => count( $post_ids ) ) );
1738
1739 $export_data = array(
1740 'version' => SMART_POST_SHOW_VERSION,
1741 'export_date' => current_time( 'mysql' ),
1742 'posts' => array(),
1743 'widgets' => array(),
1744 'templates' => array(),
1745 );
1746
1747 // Export post CSS data.
1748 if ( empty( $post_ids ) ) {
1749 $post_ids = $this->get_posts_with_smart_blocks();
1750 }
1751
1752 foreach ( $post_ids as $post_id ) {
1753 $post_data = array(
1754 'post_id' => $post_id,
1755 'css' => get_post_meta( $post_id, '_sp_smart_css', true ),
1756 'fonts' => get_post_meta( $post_id, '_sp_smart_fonts', true ),
1757 'block_names' => get_post_meta( $post_id, '_sp_smart_block_names', true ),
1758 'is_active' => get_post_meta( $post_id, '_sps_smart_active', true ),
1759 );
1760 $export_data['posts'][] = $post_data;
1761 }
1762
1763 // Export widget CSS data.
1764 $widget_options = $this->get_options_by_prefix( '_sp_smart_widget_' );
1765 foreach ( $widget_options as $option_name => $option_value ) {
1766 $export_data['widgets'][ $option_name ] = $option_value;
1767 }
1768
1769 // Export template CSS data.
1770 $template_options = $this->get_options_by_prefix( '_sp_smart_template_' );
1771 foreach ( $template_options as $option_name => $option_value ) {
1772 $export_data['templates'][ $option_name ] = $option_value;
1773 }
1774
1775 $this->log_asset_operation( 'complete', 'css_export', array( 'posts' => count( $export_data['posts'] ) ) );
1776 return $export_data;
1777 }
1778
1779 /**
1780 * Import CSS data from backup/migration.
1781 *
1782 * @param array $import_data Import data array.
1783 * @param bool $regenerate_files Whether to regenerate CSS files.
1784 * @return array Import results.
1785 */
1786 public function import_css_data( $import_data, $regenerate_files = true ) {
1787 $this->log_asset_operation( 'start', 'css_import', array( 'regenerate_files' => $regenerate_files ) );
1788
1789 $results = array(
1790 'posts_imported' => 0,
1791 'widgets_imported' => 0,
1792 'templates_imported' => 0,
1793 'files_regenerated' => 0,
1794 'errors' => array(),
1795 );
1796
1797 try {
1798 // Import post data.
1799 if ( isset( $import_data['posts'] ) && is_array( $import_data['posts'] ) ) {
1800 foreach ( $import_data['posts'] as $post_data ) {
1801 if ( isset( $post_data['post_id'] ) ) {
1802 update_post_meta( $post_data['post_id'], '_sp_smart_css', $post_data['css'] ?? '' );
1803 update_post_meta( $post_data['post_id'], '_sp_smart_fonts', $post_data['fonts'] ?? array() );
1804 update_post_meta( $post_data['post_id'], '_sp_smart_block_names', $post_data['block_names'] ?? array() );
1805 update_post_meta( $post_data['post_id'], '_sps_smart_active', $post_data['is_active'] ?? 'yes' );
1806
1807 if ( $regenerate_files && ! empty( $post_data['css'] ) ) {
1808 $this->save_block_css( $post_data['post_id'], $post_data['css'] );
1809 ++$results['files_regenerated'];
1810 }
1811 ++$results['posts_imported'];
1812 }
1813 }
1814 }
1815
1816 // Import widget data.
1817 if ( isset( $import_data['widgets'] ) && is_array( $import_data['widgets'] ) ) {
1818 foreach ( $import_data['widgets'] as $option_name => $option_value ) {
1819 update_option( $option_name, $option_value );
1820 ++$results['widgets_imported'];
1821 }
1822 }
1823
1824 // Import template data.
1825 if ( isset( $import_data['templates'] ) && is_array( $import_data['templates'] ) ) {
1826 foreach ( $import_data['templates'] as $option_name => $option_value ) {
1827 update_option( $option_name, $option_value );
1828 ++$results['templates_imported'];
1829 }
1830 }
1831 } catch ( \Exception $e ) {
1832 $results['errors'][] = $e->getMessage();
1833 $this->log_asset_operation( 'error', 'css_import', array( 'error' => $e->getMessage() ) );
1834 }
1835
1836 $this->log_asset_operation( 'complete', 'css_import', $results );
1837 return $results;
1838 }
1839
1840 /**
1841 * Handle plugin update/migration.
1842 *
1843 * @param string $old_version Previous version.
1844 * @param string $new_version New version.
1845 * @return array Migration results.
1846 */
1847 public function handle_plugin_update( $old_version, $new_version ) {
1848 $this->log_asset_operation(
1849 'start',
1850 'plugin_update',
1851 array(
1852 'old_version' => $old_version,
1853 'new_version' => $new_version,
1854 )
1855 );
1856
1857 $results = array(
1858 'css_files_regenerated' => 0,
1859 'static_files_cleared' => 0,
1860 'errors' => array(),
1861 );
1862
1863 try {
1864 // Clear old static CSS files to force regeneration.
1865 $static_dir = $this->css_dir . self::STATIC_CSS_DIR;
1866 if ( $this->handle_file_operation( 'exists', $static_dir ) ) {
1867 $static_files = glob( $static_dir . '*.css' );
1868 if ( $static_files ) {
1869 foreach ( $static_files as $file ) {
1870 $this->handle_file_operation( 'delete', $file );
1871 ++$results['static_files_cleared'];
1872 }
1873 }
1874 }
1875
1876 // Regenerate CSS files for all active posts.
1877 $regeneration_results = $this->regenerate_all_css_files();
1878 $results['css_files_regenerated'] = $regeneration_results['success'];
1879
1880 } catch ( \Exception $e ) {
1881 $results['errors'][] = $e->getMessage();
1882 $this->log_asset_operation( 'error', 'plugin_update', array( 'error' => $e->getMessage() ) );
1883 }
1884
1885 $this->log_asset_operation( 'complete', 'plugin_update', $results );
1886 return $results;
1887 }
1888
1889 /**
1890 * Get CSS file statistics.
1891 *
1892 * @return array Statistics array.
1893 */
1894 public function get_css_statistics() {
1895 $stats = array(
1896 'total_css_files' => 0,
1897 'total_static_files' => 0,
1898 'total_size' => 0,
1899 'oldest_file' => null,
1900 'newest_file' => null,
1901 );
1902
1903 // Count main CSS files.
1904 $css_files = glob( $this->css_dir . self::CSS_FILE_PREFIX . '*.css' );
1905 if ( $css_files ) {
1906 $stats['total_css_files'] = count( $css_files );
1907 foreach ( $css_files as $file ) {
1908 $file_size = filesize( $file );
1909 $stats['total_size'] += $file_size;
1910
1911 $file_time = filemtime( $file );
1912 if ( ! $stats['oldest_file'] || $file_time < $stats['oldest_file'] ) {
1913 $stats['oldest_file'] = $file_time;
1914 }
1915 if ( ! $stats['newest_file'] || $file_time > $stats['newest_file'] ) {
1916 $stats['newest_file'] = $file_time;
1917 }
1918 }
1919 }
1920
1921 // Count static CSS files.
1922 $static_dir = $this->css_dir . self::STATIC_CSS_DIR;
1923 if ( $this->handle_file_operation( 'exists', $static_dir ) ) {
1924 $static_files = glob( $static_dir . '*.css' );
1925 if ( $static_files ) {
1926 $stats['total_static_files'] = count( $static_files );
1927 }
1928 }
1929
1930 return $stats;
1931 }
1932 }
1933