PluginProbe
Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News / 4.0.7
Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News v4.0.7
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.7, at blocks/includes/class-assets-manager.php

1,934 lines 59.6 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 'savedTemplatesUrl' => admin_url( 'edit.php?post_type=sp_post_carousel&page=pcp_help#savedTemplate' ),
730 )
731 );
732 wp_localize_script(
733 'sp_smart_post_smart_search_script',
734 'sp_smart_search_block_localize',
735 array(
736 'searchRestUrl' => esc_url_raw( rest_url( 'sps-blocks/v2/smart-search' ) ),
737 )
738 );
739 }
740
741 /**
742 * Enqueue combined Google Fonts for better performance.
743 *
744 * @param array $fonts Array of font strings.
745 * @return void
746 */
747 private function enqueue_combined_fonts( $fonts ) {
748 if ( empty( $fonts ) ) {
749 return;
750 }
751
752 // Flatten nested arrays and keep only strings.
753 $fonts = array_filter( array_map( 'strval', (array) $this->flatten_fonts( $fonts ) ) );
754
755 if ( empty( $fonts ) ) {
756 return;
757 }
758
759 $font_families = array();
760 foreach ( array_unique( $fonts ) as $font ) {
761 if ( ! is_string( $font ) || '' === $font ) {
762 continue;
763 }
764
765 $font_parts = explode( ':', $font );
766 $font_name = str_replace( ' ', '+', $font_parts[0] );
767 $font_weights = $font_parts[1] ?? '400';
768 if ( empty( $font_name ) ) {
769 continue;
770 }
771 $font_families[] = $font_name . ':' . $font_weights;
772 }
773
774 if ( ! empty( $font_families ) ) {
775 $font_url = '//fonts.googleapis.com/css?family=' . implode( '|', $font_families );
776 wp_enqueue_style(
777 'sp-smart-post-google-fonts',
778 $font_url,
779 array(),
780 SMART_POST_SHOW_VERSION
781 );
782
783 $this->log_asset_operation( 'enqueue', 'google_fonts', array( 'count' => count( $font_families ) ) );
784 }
785 }
786
787 /**
788 * Get All Reusable IDs content.
789 *
790 * @param int $post_id Post ids.
791 * @return array
792 */
793 public function get_reusable_ids( $post_id ) {
794 $reusable_id = array();
795 if ( $post_id ) {
796 $post = get_post( $post_id );
797 if ( isset( $post->post_content ) ) {
798 if ( has_blocks( $post->post_content ) &&
799 strpos( $post->post_content, 'wp:block' ) &&
800 strpos( $post->post_content, '"ref"' ) !== false
801 ) {
802 $blocks = parse_blocks( $post->post_content );
803 foreach ( $blocks as $key => $value ) {
804 if ( isset( $value['attrs']['ref'] ) ) {
805 $reusable_id[] = $value['attrs']['ref'];
806 }
807 }
808 }
809 }
810 }
811 return $reusable_id;
812 }
813
814 /**
815 * Get All Shortcode IDs content.
816 *
817 * @param int $post_id post id.
818 * @return array
819 */
820 public function get_shortcode_ids( $post_id ) {
821 $shortcode_ids = array();
822
823 $saved_template_count = wp_count_posts( 'sp_post_template' );
824 $total_saved_template = $saved_template_count->publish ?? 0;
825
826 if ( $post_id && 0 !== $total_saved_template ) {
827 $post = get_post( $post_id );
828
829 if ( isset( $post->post_content ) && ! empty( $post->post_content ) ) {
830 // Regular expression to find [smart_post id="1234"].
831 preg_match_all( '/\[smart_post\s+id=["\'](\d+)["\']\]/', $post->post_content, $matches );
832
833 if ( ! empty( $matches[1] ) ) {
834 $shortcode_ids = $matches[1]; // all captured IDs.
835 }
836 }
837 }
838
839 return $shortcode_ids;
840 }
841
842 /**
843 * Get WordPress options starting with a given key prefix,
844 * including the exact match of the prefix itself.
845 *
846 * @param string $prefix The option_name prefix to search for.
847 * @return array Array of options (option_name => option_value).
848 */
849 public function get_options_by_prefix( $prefix ) {
850 global $wpdb;
851
852 // Add % for SQL LIKE, but also allow exact match.
853 $like_key = $wpdb->esc_like( $prefix ) . '%';
854 $results = $wpdb->get_results(
855 $wpdb->prepare(
856 "SELECT option_name, option_value
857 FROM {$wpdb->options}
858 WHERE option_name = %s OR option_name LIKE %s",
859 $prefix,
860 $like_key
861 )
862 );
863 $options = array();
864 if ( $results ) {
865 foreach ( $results as $row ) {
866 $options[ $row->option_name ] = $row->option_value;
867 }
868 }
869
870 return $options;
871 }
872
873 /**
874 * Load frontend dynamic CSS and fonts for widgets, templates, posts, and reusable blocks.
875 */
876 protected function handle_frontend_assets() {
877 global $post, $wp_registered_sidebars;
878 $current_post_id = ! empty( $post->ID ) ? $post->ID : 0;
879 $css = '';
880 $fonts = array();
881 $block_names = array();
882
883 // Handle different asset sources.
884 $this->handle_template_assets( $current_post_id, $css, $fonts, $block_names );
885 $this->handle_post_assets( $current_post_id, $css, $fonts, $block_names );
886 $this->handle_preview_assets( $current_post_id, $css, $fonts, $block_names );
887 // Enqueue collected assets.
888 $this->enqueue_collected_assets( $css, $fonts, $block_names, $current_post_id );
889 }
890
891 /**
892 * Handle template assets.
893 *
894 * @param int $current_post_id Current post ID.
895 * @param string $css CSS accumulator.
896 * @param array $fonts Fonts accumulator.
897 * @param array $block_names Block names accumulator.
898 * @return void
899 */
900 private function handle_template_assets( $current_post_id, &$css, &$fonts, &$block_names ) {
901 $theme_slug = wp_get_theme()->get_stylesheet();
902 $template_contexts = array( 'header', 'footer' );
903
904 // Determine current context.
905 if ( is_home() ) {
906 $template_contexts[] = 'home';
907 } elseif ( is_archive() ) {
908 $template_contexts[] = 'archive';
909 } elseif ( is_single() ) {
910 $template_contexts[] = 'single';
911 } elseif ( is_page() ) {
912 $template_contexts[] = 'page';
913 }
914
915 global $_wp_current_template_content;
916 $template_parts = array();
917 // Ensure variable exists and not empty.
918 if ( ! empty( $_wp_current_template_content ) ) {
919 // Define the parts we want to detect dynamically.
920 $template_parts = array( 'footer' );
921 foreach ( $template_parts as $part ) {
922 if ( strpos( $_wp_current_template_content, '"slug":"' . $part . '"' ) !== false ) {
923 $template_contexts[] = $part;
924 }
925 }
926 if ( ! empty( $_wp_current_template_content ) ) {
927 // Match all occurrences of "slug":"something".
928 preg_match_all( '/"slug":"([^"]+)"/', $_wp_current_template_content, $matches );
929
930 if ( ! empty( $matches[1] ) ) {
931 $template_parts = array_unique( $matches[1] ); // unique parts only.
932 }
933 }
934 }
935
936 // Remove duplicates just in case.
937 $template_contexts = array_merge( $template_contexts, $template_parts );
938 $template_contexts = array_unique( $template_contexts );
939 // Handle custom template.
940 $custom_template_slug = get_page_template_slug( $current_post_id );
941 if ( ! empty( $custom_template_slug ) ) {
942 $this->handle_template_context( $theme_slug, $custom_template_slug, $css, $fonts, $block_names );
943 }
944
945 // Handle standard template contexts.
946 foreach ( $template_contexts as $context ) {
947 $this->handle_template_context( $theme_slug, $context, $css, $fonts, $block_names );
948 }
949 }
950
951 /**
952 * Handle a specific template context.
953 *
954 * @param string $theme_slug Theme slug.
955 * @param string $context Template context.
956 * @param string $css CSS accumulator.
957 * @param array $fonts Fonts accumulator.
958 * @param array $block_names Block names accumulator.
959 * @return void
960 */
961 private function handle_template_context( $theme_slug, $context, &$css, &$fonts, &$block_names ) {
962 $current_block_name = get_option( "_sp_smart_template_block_names{$theme_slug}{$context}", array() );
963 if ( ! empty( $current_block_name ) ) {
964 $block_names[ $context ] = $current_block_name;
965 $this->merge_assets(
966 get_option( "_sp_smart_template_{$theme_slug}{$context}", '' ),
967 get_option( "_sp_smart_fonts_template_{$theme_slug}{$context}", array() ),
968 $css,
969 $fonts
970 );
971 }
972
973 // Handle reusable blocks in templates.
974 $reused_ids = (array) get_option( "_sp_smart_template_reused_blocks_{$theme_slug}{$context}", array() );
975 foreach ( $reused_ids as $id ) {
976 $this->handle_reusable_block_assets( $id, $css, $fonts, $block_names );
977 }
978 }
979
980 /**
981 * Handle post assets.
982 *
983 * @param int $current_post_id Current post ID.
984 * @param string $css CSS accumulator.
985 * @param array $fonts Fonts accumulator.
986 * @param array $block_names Block names accumulator.
987 * @return void
988 */
989 private function handle_post_assets( $current_post_id, &$css, &$fonts, &$block_names ) {
990 if ( ! is_singular() || ! $current_post_id ) {
991 return;
992 }
993 $filename = self::CSS_FILE_PREFIX . "{$current_post_id}.css";
994 $cssfilepath = $this->css_dir . $filename;
995 $cssfileurl = $this->css_url . $filename;
996
997 if ( $this->handle_file_operation( 'exists', $cssfilepath ) ) {
998 $file_time = file_exists( $cssfilepath ) ? filemtime( $cssfilepath ) : SMART_POST_SHOW_VERSION;
999 wp_enqueue_style(
1000 "sp-post-style-{$current_post_id}",
1001 $cssfileurl,
1002 array(),
1003 $file_time
1004 );
1005 $this->merge_assets( '', get_post_meta( $current_post_id, '_sp_smart_fonts', true ), $css, $fonts );
1006 $current_block_name = get_post_meta( $current_post_id, '_sp_smart_block_names', true );
1007 if ( ! empty( $current_block_name ) ) {
1008 $block_names[ $current_post_id ] = $current_block_name;
1009 }
1010 } else {
1011 $current_block_name = get_post_meta( $current_post_id, '_sp_smart_block_names', true );
1012 if ( ! empty( $current_block_name ) ) {
1013 $block_names[ $current_post_id ] = $current_block_name;
1014 $this->merge_assets(
1015 get_post_meta( $current_post_id, '_sp_smart_css', true ),
1016 get_post_meta( $current_post_id, '_sp_smart_fonts', true ),
1017 $css,
1018 $fonts
1019 );
1020 }
1021 }
1022
1023 // Handle reusable blocks in post.
1024 foreach ( $this->get_reusable_ids( $current_post_id ) as $ref_id ) {
1025 $this->handle_reusable_block_assets( $ref_id, $css, $fonts, $block_names );
1026 }
1027
1028 // Handle reusable blocks in post.
1029 foreach ( $this->get_shortcode_ids( $current_post_id ) as $ref_id ) {
1030 $this->handle_reusable_block_assets( $ref_id, $css, $fonts, $block_names );
1031 }
1032 }
1033
1034 /**
1035 * Handle reusable block assets.
1036 *
1037 * @param int $ref_id Reusable block ID.
1038 * @param string $css CSS accumulator.
1039 * @param array $fonts Fonts accumulator.
1040 * @param array $block_names Block names accumulator.
1041 * @return void
1042 */
1043 private function handle_reusable_block_assets( $ref_id, &$css, &$fonts, &$block_names ) {
1044 $current_block_name = get_post_meta( $ref_id, '_sp_smart_block_names', true );
1045 if ( empty( $current_block_name ) ) {
1046 return;
1047 }
1048
1049 $this->merge_assets(
1050 get_post_meta( $ref_id, '_sp_smart_css', true ),
1051 get_post_meta( $ref_id, '_sp_smart_fonts', true ),
1052 $css,
1053 $fonts
1054 );
1055 $block_names[ $ref_id ] = $current_block_name;
1056 }
1057
1058 /**
1059 * Handle preview assets.
1060 *
1061 * @param int $current_post_id Current post ID.
1062 * @param string $css CSS accumulator.
1063 * @param array $fonts Fonts accumulator.
1064 * @param array $block_names Block names accumulator.
1065 * @return void
1066 */
1067 private function handle_preview_assets( $current_post_id, &$css, &$fonts, &$block_names ) {
1068 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1069 if ( isset( $_GET['preview_id'] ) && isset( $_GET['preview_nonce'] ) ) {
1070 $preview_id = absint( $_GET['preview_id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1071 $preview_nonce = sanitize_text_field( wp_unslash( $_GET['preview_nonce'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1072 // Verify nonce for security.
1073 if ( ! wp_verify_nonce( $preview_nonce, 'post_preview_' . $preview_id ) ) {
1074 return;
1075 }
1076 $this->merge_assets(
1077 get_transient( '_sps_preview_' . $current_post_id ),
1078 get_transient( '_sps_preview_fonts_' . $current_post_id ),
1079 $css,
1080 $fonts
1081 );
1082 $block_names['preview'] = get_transient( '_sps_preview_block_name' . $current_post_id );
1083 }
1084 }
1085
1086 /**
1087 * Enqueue collected assets.
1088 *
1089 * @param string $css CSS content.
1090 * @param array $fonts Fonts array.
1091 * @param array $block_names Block names.
1092 * @param int $current_post_id Current post ID.
1093 * @return void
1094 */
1095 private function enqueue_collected_assets( $css, $fonts, $block_names, $current_post_id ) {
1096 // Enqueue inline CSS.
1097 if ( ! empty( $css ) ) {
1098 wp_add_inline_style( 'sp_smart_post_blocks_css', wp_strip_all_tags( $css ) );
1099 }
1100 // Merge with global Google Fonts.
1101 if ( ! empty( $this->google_fonts ) ) {
1102 $fonts = ! empty( $fonts ) ? array_merge( (array) $fonts, (array) $this->google_fonts ) : $this->google_fonts;
1103 }
1104 // Enqueue Google Fonts.
1105 $this->enqueue_combined_fonts( $fonts );
1106 // Enqueue combined block CSS.
1107 $this->enqueue_combined_block_css( $block_names, $current_post_id );
1108 }
1109
1110 /**
1111 * Merge CSS and fonts into accumulators.
1112 *
1113 * @param string $css_source CSS source.
1114 * @param array $fonts_source Fonts source.
1115 * @param string $css CSS accumulator.
1116 * @param array $fonts Fonts accumulator.
1117 * @return void
1118 */
1119 private function merge_assets( $css_source, $fonts_source, &$css, &$fonts ) {
1120 if ( ! empty( $css_source ) ) {
1121 $css .= $css_source;
1122 }
1123 if ( ! empty( $fonts_source ) && is_array( $fonts_source ) ) {
1124 $fonts = array_merge( $fonts, $fonts_source );
1125 }
1126 }
1127
1128 /**
1129 * Enqueue combined block CSS.
1130 *
1131 * @param array $block_names Block names.
1132 * @param int $current_post_id Current post ID.
1133 * @return void
1134 */
1135 public function enqueue_combined_block_css( $block_names, $current_post_id = 0 ) {
1136 if ( empty( $block_names ) ) {
1137 return;
1138 }
1139
1140 $cssfilepath_static = $this->css_dir . self::STATIC_CSS_DIR;
1141
1142 // Ensure static directory exists.
1143 if ( ! $this->handle_file_operation( 'exists', $cssfilepath_static ) ) {
1144 $this->handle_file_operation( 'mkdir', $cssfilepath_static );
1145 }
1146
1147 $loaded_blocks = array();
1148 foreach ( $block_names as $key => $block_part ) {
1149 if ( empty( $block_part ) ) {
1150 continue;
1151 }
1152
1153 $filename = self::CSS_FILE_PREFIX . "{$key}.css";
1154 $cssfilepath = $cssfilepath_static . $filename;
1155 $cssfileurl = $this->css_url . self::STATIC_CSS_DIR . $filename;
1156
1157 if ( ! $this->handle_file_operation( 'exists', $cssfilepath ) ) {
1158 $css = '';
1159 $block_part = $this->flatten_fonts( $block_part );
1160 $targets = array(
1161 'post-slider',
1162 'post-slider-two',
1163 'post-timeline-three',
1164 'thumbnail-slider',
1165 'thumbnail-slider-two',
1166 );
1167 $list_block_targets = array(
1168 'post-list-two',
1169 'post-list-three',
1170 );
1171
1172 if ( array_intersect( $targets, $block_part ) ) {
1173 $block_part[] = 'post-carousel';
1174 }
1175 if ( in_array( 'post-timeline-two', $block_part, true ) ) {
1176 $block_part[] = 'post-timeline-one';
1177 }
1178 if ( array_intersect( $list_block_targets, $block_part ) ) {
1179 $block_part[] = 'post-list-one';
1180 }
1181
1182 foreach ( $block_part as $block ) {
1183 if ( in_array( $block, $loaded_blocks, true ) ) {
1184 continue;
1185 }
1186
1187 $loaded_blocks[] = $block;
1188 $style_file = SP_PC_PATH . "blocks/includes/$block/style.css";
1189 if ( $this->handle_file_operation( 'exists', $style_file ) ) {
1190 $this->init_filesystem();
1191 if ( $this->wp_filesystem ) {
1192 $file_content = $this->wp_filesystem->get_contents( $style_file );
1193 if ( false !== $file_content ) {
1194 $css .= $file_content;
1195 }
1196 }
1197 }
1198 }
1199
1200 if ( ! empty( $css ) ) {
1201 $this->handle_file_operation( 'write', $cssfilepath, $css );
1202 }
1203 }
1204
1205 // Enqueue combined CSS if file exists.
1206 if ( $this->handle_file_operation( 'exists', $cssfilepath ) ) {
1207 $file_time = file_exists( $cssfilepath ) ? filemtime( $cssfilepath ) : SMART_POST_SHOW_VERSION;
1208 wp_enqueue_style(
1209 'sp-smart-blocks-combined-' . $key,
1210 $cssfileurl,
1211 array(),
1212 $file_time
1213 );
1214 }
1215 }
1216 }
1217
1218 /**
1219 * Helper: Flatten nested font arrays.
1220 */
1221 /**
1222 * Flatten nested font values (arrays, nested arrays, scalar values) into a flat array of strings.
1223 *
1224 * @param array $fonts Nested font values.
1225 * @return string[]
1226 */
1227 private function flatten_fonts( $fonts ) {
1228 $result = array();
1229 $stack = (array) $fonts;
1230
1231 while ( ! empty( $stack ) ) {
1232 $item = array_shift( $stack );
1233
1234 if ( is_array( $item ) ) {
1235 // push each element back onto stack for processing.
1236 foreach ( $item as $v ) {
1237 $stack[] = $v;
1238 }
1239 continue;
1240 }
1241
1242 // Keep scalars (string / number) only.
1243 if ( is_scalar( $item ) ) {
1244 $result[] = (string) $item;
1245 }
1246 }
1247
1248 return $result;
1249 }
1250
1251 /**
1252 * Get active blocks for the current post.
1253 *
1254 * @param int $post_id Current post ID.
1255 * @param array $our_blocks Registered blocks.
1256 * @return array
1257 */
1258 public static function get_active_blocks( $post_id, $our_blocks ) {
1259 $active_blocks = array();
1260 $content = get_post_field( 'post_content', $post_id );
1261 foreach ( $our_blocks as $block ) {
1262 if ( has_block( $block, $content ) ) {
1263 $active_blocks[] = $block;
1264 }
1265 }
1266
1267 return $active_blocks;
1268 }
1269
1270 /**
1271 * Save block CSS to a file.
1272 *
1273 * @param int $post_id Post ID.
1274 * @param string $css CSS content.
1275 * @return bool
1276 */
1277 public function save_block_css( $post_id, $css ) {
1278 // Validate post ID.
1279 if ( ! is_numeric( $post_id ) || $post_id <= 0 ) {
1280 $this->log_asset_operation(
1281 'error',
1282 'save_css',
1283 array(
1284 'post_id' => $post_id,
1285 'error' => 'Invalid post ID',
1286 )
1287 );
1288 return false;
1289 }
1290
1291 // Ensure CSS directory exists.
1292 if ( ! $this->handle_file_operation( 'exists', $this->css_dir ) ) {
1293 $this->handle_file_operation( 'mkdir', $this->css_dir );
1294 }
1295
1296 $filename = self::CSS_FILE_PREFIX . "{$post_id}.css";
1297 $filepath = $this->css_dir . $filename;
1298
1299 // Validate filename for security.
1300 if ( ! $this->validate_css_path( $filename ) ) {
1301 $this->log_asset_operation(
1302 'error',
1303 'save_css',
1304 array(
1305 'filename' => $filename,
1306 'error' => 'Invalid filename',
1307 )
1308 );
1309 return false;
1310 }
1311
1312 // Save CSS to file.
1313 $result = $this->handle_file_operation( 'write', $filepath, $css );
1314 $this->log_asset_operation(
1315 'save',
1316 'css_file',
1317 array(
1318 'post_id' => $post_id,
1319 'success' => $result,
1320 )
1321 );
1322
1323 return $result;
1324 }
1325
1326 /**
1327 * Delete block CSS file.
1328 *
1329 * @param int $post_id Post ID.
1330 * @param \WP_Post $post Post object (optional, for hook compatibility).
1331 * @return void
1332 */
1333 public function delete_block_css( $post_id, $post = null ) {
1334 $filename = self::CSS_FILE_PREFIX . "{$post_id}.css";
1335 $filepath = $this->css_dir . $filename;
1336
1337 // Validate filename for security.
1338 if ( $this->handle_file_operation( 'exists', $filepath ) ) {
1339 $this->handle_file_operation( 'delete', $filepath );
1340 $this->log_asset_operation( 'delete', 'css_file', array( 'post_id' => $post_id ) );
1341 }
1342 $this->delete_block_static_css( $filename );
1343 delete_post_meta( $post_id, '_sp_smart_post_google_fonts' );
1344 }
1345 /**
1346 * Delete static block CSS file.
1347 *
1348 * @param string $filename File name.
1349 * @return void
1350 */
1351 public function delete_block_static_css( $filename ) {
1352 $filepath = $this->css_dir . self::STATIC_CSS_DIR . $filename;
1353
1354 // Validate filename for security.
1355 if ( $this->handle_file_operation( 'exists', $filepath ) ) {
1356 $this->handle_file_operation( 'delete', $filepath );
1357 $this->log_asset_operation( 'delete', 'static_css_file', array( 'filename' => $filename ) );
1358 }
1359 }
1360 /**
1361 * Check permission.
1362 *
1363 * @param mixed $post_id Post ID.
1364 * @param mixed $cap Capability.
1365 * @return bool
1366 */
1367 public function permission_check( $post_id = false, $cap = '' ) {
1368 $cap = $cap ? $cap : 'edit_others_posts';
1369 $is_passed = false;
1370 if ( $post_id ) {
1371 $post_author = (int) get_post_field( 'post_author', $post_id );
1372 $is_passed = (int) get_current_user_id() === $post_author;
1373 }
1374 return $is_passed || current_user_can( $cap );
1375 }
1376 /**
1377 * Save block CSS via REST API.
1378 *
1379 * @param \WP_REST_Request $request Full data about the request.
1380 * @return \WP_REST_Response
1381 *
1382 * @throws \Exception If CSS file saving fails due to file permission issues.
1383 */
1384 public function save_block_content_css( $request ) {
1385 $params = $request->get_params();
1386 // Sanitize all parameters.
1387 $sanitized_params = $this->sanitize_block_params( $params );
1388 extract( $sanitized_params ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
1389
1390 $capability = 'publish_posts';
1391 if ( ! $this->permission_check( is_numeric( $post_id ) ? $post_id : false, $capability ) ) {
1392 $this->log_asset_operation(
1393 'error',
1394 'permission_check',
1395 array(
1396 'post_id' => $post_id,
1397 'capability' => $capability,
1398 )
1399 );
1400 return new \WP_REST_Response(
1401 array(
1402 'success' => false,
1403 'message' => __( 'Insufficient permissions.', 'post-carousel' ),
1404 ),
1405 403
1406 );
1407 }
1408 // Handle widget CSS storage.
1409 if ( ! empty( $widget_id ) ) {
1410 $storage_key = '_sp_smart_widget_' . $widget_id;
1411 $font_key = '_sp_smart_fonts_widget_' . $widget_id;
1412 $block_names_key = '_sp_smart_widget_block_name' . $widget_id;
1413 $this->delete_block_static_css( self::CSS_FILE_PREFIX . "{$widget_id}.css" );
1414 if ( $has_block ) {
1415 update_option( $storage_key, $block_css );
1416 update_option( $font_key, $fonts );
1417 update_option( $block_names_key, $block_names );
1418 $this->log_asset_operation( 'save', 'widget_css', array( 'widget_id' => $widget_id ) );
1419 } else {
1420 delete_option( $storage_key );
1421 delete_option( $font_key );
1422 delete_option( $block_names_key );
1423 $this->log_asset_operation( 'delete', 'widget_css', array( 'widget_id' => $widget_id ) );
1424 }
1425 return new \WP_REST_Response(
1426 array(
1427 'success' => true,
1428 'message' => __( 'Widget CSS saved successfully.', 'post-carousel' ),
1429 )
1430 );
1431 }
1432 // Handle template and template part CSS storage.
1433 if ( ( 'wp_template' === $post_id || 'wp_template_part' === $post_id ) && 'wp_block' !== $block_type ) {
1434 $storage_key = '_sp_smart_template_' . $theme . $slug;
1435 $font_key = '_sp_smart_fonts_template_' . $theme . $slug;
1436 $block_names_key = '_sp_smart_template_block_names' . $theme . $slug;
1437 $this->delete_block_static_css( self::CSS_FILE_PREFIX . "{$slug}.css" );
1438 if ( ! $has_refs ) {
1439 delete_option( '_sp_smart_template_reused_blocks_' . $theme . $slug );
1440 }
1441 if ( $has_block ) {
1442 update_option( $storage_key, $block_css );
1443 update_option( $font_key, $fonts );
1444 update_option( $block_names_key, $block_names );
1445 $this->log_asset_operation(
1446 'save',
1447 'template_css',
1448 array(
1449 'theme' => $theme,
1450 'slug' => $slug,
1451 )
1452 );
1453 } else {
1454 delete_option( $storage_key );
1455 delete_option( $font_key );
1456 delete_option( $block_names_key );
1457 $this->log_asset_operation(
1458 'delete',
1459 'template_css',
1460 array(
1461 'theme' => $theme,
1462 'slug' => $slug,
1463 )
1464 );
1465 }
1466 return new \WP_REST_Response(
1467 array(
1468 'success' => $block_type,
1469 'message' => __( 'Template CSS saved successfully.', 'post-carousel' ),
1470 )
1471 );
1472 }
1473
1474 // Handle regular posts and custom post types.
1475 if ( ! empty( $block_css ) ) {
1476 // Handle preview mode.
1477 if ( $is_preview ) {
1478 set_transient( '_sps_preview_' . $post_id, $block_css, self::PREVIEW_TRANSIENT_EXPIRATION );
1479 set_transient( '_sps_preview_fonts_' . $post_id, $fonts, self::PREVIEW_TRANSIENT_EXPIRATION );
1480 set_transient( '_sps_preview_block_name' . $post_id, $block_names, self::PREVIEW_TRANSIENT_EXPIRATION );
1481 $this->log_asset_operation( 'preview', 'css_saved', array( 'post_id' => $post_id ) );
1482 return new \WP_REST_Response(
1483 array(
1484 'success' => true,
1485 'message' => __( 'Preview CSS saved temporarily.', 'post-carousel' ),
1486 )
1487 );
1488 }
1489 if ( 'wp_block' === $block_type ) {
1490 if ( 'wp_template' === $post_id || 'wp_template_part' === $post_id ) {
1491 $wp_block_used_key = '_sp_smart_template_reused_blocks_' . $theme . $slug;
1492 // Always cast to array when reading.
1493 $old_reuseable_block = get_option( $wp_block_used_key, array() );
1494 if ( ! is_array( $old_reuseable_block ) ) {
1495 $old_reuseable_block = array();
1496 }
1497
1498 // Add the new ID.
1499 $old_reuseable_block[] = $block_ref_id;
1500
1501 // Keep only items that exist in $reusable_block_ids.
1502 $old_reuseable_block = array_intersect( $reusable_block_ids, $old_reuseable_block );
1503
1504 // Remove duplicates and reindex.
1505 $old_reuseable_block = array_values( array_unique( $old_reuseable_block ) );
1506
1507 // Always update as array.
1508 update_option( $wp_block_used_key, $old_reuseable_block );
1509 }
1510 $post_id = $block_ref_id;
1511 }
1512 // Validate post ID.
1513 if ( ! is_numeric( $post_id ) ) {
1514 return new \WP_REST_Response(
1515 array(
1516 'success' => false,
1517 'message' => __( 'Invalid post ID.', 'post-carousel' ),
1518 ),
1519 400
1520 );
1521 }
1522
1523 // Save CSS to file.
1524 try {
1525 if ( 'wp_block' === $block_type ) {
1526 // Handle reusable blocks - save meta data only.
1527 update_post_meta( $block_ref_id, '_sps_smart_active', 'yes' );
1528 update_post_meta( $block_ref_id, '_sp_smart_css', $block_css );
1529 update_post_meta( $block_ref_id, '_sp_smart_fonts', $fonts );
1530 update_post_meta( $block_ref_id, '_sp_smart_block_names', $block_names );
1531 $this->log_asset_operation( 'save', 'reusable_block', array( 'block_ref_id' => $block_ref_id ) );
1532 $result = true;
1533 } else {
1534 $result = $this->save_block_css( $post_id, $block_css );
1535 $this->delete_block_static_css( self::CSS_FILE_PREFIX . "{$post_id}.css" );
1536 if ( $result ) {
1537 update_post_meta( $post_id, '_sps_smart_active', 'yes' );
1538 update_post_meta( $post_id, '_sp_smart_css', $block_css );
1539 update_post_meta( $post_id, '_sp_smart_fonts', $fonts );
1540 update_post_meta( $post_id, '_sp_smart_block_names', $block_names );
1541 $this->log_asset_operation( 'save', 'post_css', array( 'post_id' => $post_id ) );
1542 return new \WP_REST_Response(
1543 array(
1544 'success' => true,
1545 'message' => __( 'CSS file saved successfully.', 'post-carousel' ),
1546 )
1547 );
1548 } else {
1549 throw new \Exception( __( 'CSS could not be saved due to file permission issues.', 'post-carousel' ) );
1550 }
1551 }
1552 } catch ( \Exception $e ) {
1553 return new \WP_REST_Response(
1554 array(
1555 'success' => false,
1556 'message' => $e->getMessage(),
1557 ),
1558 500
1559 );
1560 }
1561 } else {
1562 if ( 'wp_block' === $block_type ) {
1563 $post_id = $block_ref_id;
1564 }
1565 try {
1566 delete_post_meta( $post_id, '_sps_smart_active' );
1567 delete_post_meta( $post_id, '_sp_smart_css' );
1568 delete_post_meta( $post_id, '_sp_smart_fonts' );
1569 delete_post_meta( $post_id, '_sp_smart_block_names' );
1570 $this->delete_block_static_css( self::CSS_FILE_PREFIX . "{$post_id}.css" );
1571 $filename = self::CSS_FILE_PREFIX . "{$post_id}.css";
1572 $filepath = $this->css_dir . $filename;
1573 if ( $this->handle_file_operation( 'exists', $filepath ) ) {
1574 $this->handle_file_operation( 'delete', $filepath );
1575 }
1576 $this->log_asset_operation( 'delete', 'post_css', array( 'post_id' => $post_id ) );
1577 return new \WP_REST_Response(
1578 array(
1579 'success' => true,
1580 'message' => __( 'CSS file deleted successfully.', 'post-carousel' ),
1581 )
1582 );
1583 } catch ( \Exception $e ) {
1584 return new \WP_REST_Response(
1585 array(
1586 'success' => false,
1587 'message' => $e->getMessage(),
1588 ),
1589 500
1590 );
1591 }
1592 }
1593 }
1594
1595 /*
1596 ---------------------------------------------------------------------------------
1597 | Bulk CSS Operations
1598 -------------------------------------------------------------------------------
1599 * This section handles bulk operations for CSS files associated with Smart Post blocks.
1600 * It includes functions to regenerate, clear, and export CSS files for multiple posts.
1601 */
1602 /**
1603 * Regenerate CSS files for all posts with Smart Post blocks.
1604 *
1605 * @param array $post_ids Optional array of specific post IDs to regenerate.
1606 * @return array Results array with success/failure counts.
1607 */
1608 public function regenerate_all_css_files( $post_ids = array() ) {
1609 $this->log_asset_operation( 'start', 'bulk_regeneration', array( 'post_count' => count( $post_ids ) ) );
1610
1611 $results = array(
1612 'success' => 0,
1613 'failed' => 0,
1614 'errors' => array(),
1615 );
1616
1617 // If no specific post IDs provided, get all posts with Smart Post blocks.
1618 if ( empty( $post_ids ) ) {
1619 $post_ids = $this->get_posts_with_smart_blocks();
1620 }
1621
1622 foreach ( $post_ids as $post_id ) {
1623 try {
1624 $css_content = get_post_meta( $post_id, '_sp_smart_css', true );
1625 if ( ! empty( $css_content ) ) {
1626 $success = $this->save_block_css( $post_id, $css_content );
1627 if ( $success ) {
1628 ++$results['success'];
1629 $this->log_asset_operation(
1630 'regenerate',
1631 'css_file',
1632 array(
1633 'post_id' => $post_id,
1634 'success' => true,
1635 )
1636 );
1637 } else {
1638 ++$results['failed'];
1639 $results['errors'][] = sprintf( 'Failed to regenerate CSS for post ID: %d', $post_id );
1640 }
1641 }
1642 } catch ( \Exception $e ) {
1643 ++$results['failed'];
1644 $results['errors'][] = sprintf( 'Error regenerating CSS for post ID %d: %s', $post_id, $e->getMessage() );
1645 $this->log_asset_operation(
1646 'error',
1647 'css_regeneration',
1648 array(
1649 'post_id' => $post_id,
1650 'error' => $e->getMessage(),
1651 )
1652 );
1653 }
1654 }
1655
1656 $this->log_asset_operation( 'complete', 'bulk_regeneration', $results );
1657 return $results;
1658 }
1659
1660 /**
1661 * Get all posts that have Smart Post blocks.
1662 *
1663 * @return array Array of post IDs.
1664 */
1665 public function get_posts_with_smart_blocks() {
1666 global $wpdb;
1667
1668 $post_ids = $wpdb->get_col(
1669 $wpdb->prepare(
1670 "SELECT DISTINCT post_id
1671 FROM {$wpdb->postmeta}
1672 WHERE meta_key = %s AND meta_value = %s",
1673 '_sps_smart_active',
1674 'yes'
1675 )
1676 );
1677
1678 return array_map( 'absint', $post_ids );
1679 }
1680
1681 /**
1682 * Clear all generated CSS files.
1683 *
1684 * @param bool $include_static Whether to include static CSS files.
1685 * @return array Results array.
1686 */
1687 public function clear_all_css_files( $include_static = true ) {
1688 $this->log_asset_operation( 'start', 'bulk_cleanup', array( 'include_static' => $include_static ) );
1689
1690 $results = array(
1691 'deleted' => 0,
1692 'failed' => 0,
1693 'errors' => array(),
1694 );
1695
1696 // Clear main CSS files.
1697 $css_files = glob( $this->css_dir . self::CSS_FILE_PREFIX . '*.css' );
1698 if ( $css_files ) {
1699 foreach ( $css_files as $file ) {
1700 if ( $this->handle_file_operation( 'delete', $file ) ) {
1701 ++$results['deleted'];
1702 } else {
1703 ++$results['failed'];
1704 $results['errors'][] = sprintf( 'Failed to delete: %s', basename( $file ) );
1705 }
1706 }
1707 }
1708
1709 // Clear static CSS files if requested.
1710 if ( $include_static ) {
1711 $static_dir = $this->css_dir . self::STATIC_CSS_DIR;
1712 if ( $this->handle_file_operation( 'exists', $static_dir ) ) {
1713 $static_files = glob( $static_dir . self::CSS_FILE_PREFIX . '*.css' );
1714 if ( $static_files ) {
1715 foreach ( $static_files as $file ) {
1716 if ( $this->handle_file_operation( 'delete', $file ) ) {
1717 ++$results['deleted'];
1718 } else {
1719 ++$results['failed'];
1720 $results['errors'][] = sprintf( 'Failed to delete static file: %s', basename( $file ) );
1721 }
1722 }
1723 }
1724 }
1725 }
1726
1727 $this->log_asset_operation( 'complete', 'bulk_cleanup', $results );
1728 return $results;
1729 }
1730
1731 /**
1732 * Export CSS data for backup/migration.
1733 *
1734 * @param array $post_ids Optional array of specific post IDs to export.
1735 * @return array Export data array.
1736 */
1737 public function export_css_data( $post_ids = array() ) {
1738 $this->log_asset_operation( 'start', 'css_export', array( 'post_count' => count( $post_ids ) ) );
1739
1740 $export_data = array(
1741 'version' => SMART_POST_SHOW_VERSION,
1742 'export_date' => current_time( 'mysql' ),
1743 'posts' => array(),
1744 'widgets' => array(),
1745 'templates' => array(),
1746 );
1747
1748 // Export post CSS data.
1749 if ( empty( $post_ids ) ) {
1750 $post_ids = $this->get_posts_with_smart_blocks();
1751 }
1752
1753 foreach ( $post_ids as $post_id ) {
1754 $post_data = array(
1755 'post_id' => $post_id,
1756 'css' => get_post_meta( $post_id, '_sp_smart_css', true ),
1757 'fonts' => get_post_meta( $post_id, '_sp_smart_fonts', true ),
1758 'block_names' => get_post_meta( $post_id, '_sp_smart_block_names', true ),
1759 'is_active' => get_post_meta( $post_id, '_sps_smart_active', true ),
1760 );
1761 $export_data['posts'][] = $post_data;
1762 }
1763
1764 // Export widget CSS data.
1765 $widget_options = $this->get_options_by_prefix( '_sp_smart_widget_' );
1766 foreach ( $widget_options as $option_name => $option_value ) {
1767 $export_data['widgets'][ $option_name ] = $option_value;
1768 }
1769
1770 // Export template CSS data.
1771 $template_options = $this->get_options_by_prefix( '_sp_smart_template_' );
1772 foreach ( $template_options as $option_name => $option_value ) {
1773 $export_data['templates'][ $option_name ] = $option_value;
1774 }
1775
1776 $this->log_asset_operation( 'complete', 'css_export', array( 'posts' => count( $export_data['posts'] ) ) );
1777 return $export_data;
1778 }
1779
1780 /**
1781 * Import CSS data from backup/migration.
1782 *
1783 * @param array $import_data Import data array.
1784 * @param bool $regenerate_files Whether to regenerate CSS files.
1785 * @return array Import results.
1786 */
1787 public function import_css_data( $import_data, $regenerate_files = true ) {
1788 $this->log_asset_operation( 'start', 'css_import', array( 'regenerate_files' => $regenerate_files ) );
1789
1790 $results = array(
1791 'posts_imported' => 0,
1792 'widgets_imported' => 0,
1793 'templates_imported' => 0,
1794 'files_regenerated' => 0,
1795 'errors' => array(),
1796 );
1797
1798 try {
1799 // Import post data.
1800 if ( isset( $import_data['posts'] ) && is_array( $import_data['posts'] ) ) {
1801 foreach ( $import_data['posts'] as $post_data ) {
1802 if ( isset( $post_data['post_id'] ) ) {
1803 update_post_meta( $post_data['post_id'], '_sp_smart_css', $post_data['css'] ?? '' );
1804 update_post_meta( $post_data['post_id'], '_sp_smart_fonts', $post_data['fonts'] ?? array() );
1805 update_post_meta( $post_data['post_id'], '_sp_smart_block_names', $post_data['block_names'] ?? array() );
1806 update_post_meta( $post_data['post_id'], '_sps_smart_active', $post_data['is_active'] ?? 'yes' );
1807
1808 if ( $regenerate_files && ! empty( $post_data['css'] ) ) {
1809 $this->save_block_css( $post_data['post_id'], $post_data['css'] );
1810 ++$results['files_regenerated'];
1811 }
1812 ++$results['posts_imported'];
1813 }
1814 }
1815 }
1816
1817 // Import widget data.
1818 if ( isset( $import_data['widgets'] ) && is_array( $import_data['widgets'] ) ) {
1819 foreach ( $import_data['widgets'] as $option_name => $option_value ) {
1820 update_option( $option_name, $option_value );
1821 ++$results['widgets_imported'];
1822 }
1823 }
1824
1825 // Import template data.
1826 if ( isset( $import_data['templates'] ) && is_array( $import_data['templates'] ) ) {
1827 foreach ( $import_data['templates'] as $option_name => $option_value ) {
1828 update_option( $option_name, $option_value );
1829 ++$results['templates_imported'];
1830 }
1831 }
1832 } catch ( \Exception $e ) {
1833 $results['errors'][] = $e->getMessage();
1834 $this->log_asset_operation( 'error', 'css_import', array( 'error' => $e->getMessage() ) );
1835 }
1836
1837 $this->log_asset_operation( 'complete', 'css_import', $results );
1838 return $results;
1839 }
1840
1841 /**
1842 * Handle plugin update/migration.
1843 *
1844 * @param string $old_version Previous version.
1845 * @param string $new_version New version.
1846 * @return array Migration results.
1847 */
1848 public function handle_plugin_update( $old_version, $new_version ) {
1849 $this->log_asset_operation(
1850 'start',
1851 'plugin_update',
1852 array(
1853 'old_version' => $old_version,
1854 'new_version' => $new_version,
1855 )
1856 );
1857
1858 $results = array(
1859 'css_files_regenerated' => 0,
1860 'static_files_cleared' => 0,
1861 'errors' => array(),
1862 );
1863
1864 try {
1865 // Clear old static CSS files to force regeneration.
1866 $static_dir = $this->css_dir . self::STATIC_CSS_DIR;
1867 if ( $this->handle_file_operation( 'exists', $static_dir ) ) {
1868 $static_files = glob( $static_dir . '*.css' );
1869 if ( $static_files ) {
1870 foreach ( $static_files as $file ) {
1871 $this->handle_file_operation( 'delete', $file );
1872 ++$results['static_files_cleared'];
1873 }
1874 }
1875 }
1876
1877 // Regenerate CSS files for all active posts.
1878 $regeneration_results = $this->regenerate_all_css_files();
1879 $results['css_files_regenerated'] = $regeneration_results['success'];
1880
1881 } catch ( \Exception $e ) {
1882 $results['errors'][] = $e->getMessage();
1883 $this->log_asset_operation( 'error', 'plugin_update', array( 'error' => $e->getMessage() ) );
1884 }
1885
1886 $this->log_asset_operation( 'complete', 'plugin_update', $results );
1887 return $results;
1888 }
1889
1890 /**
1891 * Get CSS file statistics.
1892 *
1893 * @return array Statistics array.
1894 */
1895 public function get_css_statistics() {
1896 $stats = array(
1897 'total_css_files' => 0,
1898 'total_static_files' => 0,
1899 'total_size' => 0,
1900 'oldest_file' => null,
1901 'newest_file' => null,
1902 );
1903
1904 // Count main CSS files.
1905 $css_files = glob( $this->css_dir . self::CSS_FILE_PREFIX . '*.css' );
1906 if ( $css_files ) {
1907 $stats['total_css_files'] = count( $css_files );
1908 foreach ( $css_files as $file ) {
1909 $file_size = filesize( $file );
1910 $stats['total_size'] += $file_size;
1911
1912 $file_time = filemtime( $file );
1913 if ( ! $stats['oldest_file'] || $file_time < $stats['oldest_file'] ) {
1914 $stats['oldest_file'] = $file_time;
1915 }
1916 if ( ! $stats['newest_file'] || $file_time > $stats['newest_file'] ) {
1917 $stats['newest_file'] = $file_time;
1918 }
1919 }
1920 }
1921
1922 // Count static CSS files.
1923 $static_dir = $this->css_dir . self::STATIC_CSS_DIR;
1924 if ( $this->handle_file_operation( 'exists', $static_dir ) ) {
1925 $static_files = glob( $static_dir . '*.css' );
1926 if ( $static_files ) {
1927 $stats['total_static_files'] = count( $static_files );
1928 }
1929 }
1930
1931 return $stats;
1932 }
1933 }
1934