setup_directories(); $this->init_hooks(); } /** * Get the single instance of the Dynamic_Style class. * * @since 1.0.0 * @access public * * @return Dynamic_Style The single instance. */ public static function get_instance() { if ( is_null( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } /** * Setup CSS directories for storing generated files. * * Creates the directory structure if it doesn't exist. * * @since 1.0.0 * @access private * * @return void */ private function setup_directories() { $upload_dir = wp_upload_dir(); $this->css_dir = trailingslashit( $upload_dir['basedir'] ) . 'acumenpopup/css'; $this->css_url = trailingslashit( $upload_dir['baseurl'] ) . 'acumenpopup/css'; if ( ! file_exists( $this->css_dir ) ) { wp_mkdir_p( $this->css_dir ); } } /** * Initialize WordPress hooks. * * @since 1.0.0 * @access private * * @return void */ private function init_hooks() { add_filter( 'render_block', array( $this, 'collect_dynamic_styles' ), 10, 2 ); add_action( 'wp_footer', array( $this, 'generate_css_file' ), 10 ); add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_dynamic_styles' ) ); } /** * Collect dynamic styles from rendered blocks. * * Filters each block during rendering to collect any blockStyle attributes. * * @since 1.0.0 * @access public * * @param string $block_content The block content. * @param array $block The block data including attributes. * * @return string The unmodified block content. */ public function collect_dynamic_styles( $block_content, $block ) { // Only process Acumenpopup blocks. if ( ! isset( $block['blockName'] ) || false === strpos( $block['blockName'], 'acumenpopup/' ) ) { return $block_content; } /** * Action hook for rendering Acumenpopup blocks. * * Allows other components to hook into block rendering. * * @since 1.0.0 * * @param array $block The block data. */ do_action( 'acumenpopup_render_block', $block ); // Collect block styles. if ( isset( $block['attrs']['blockStyle'] ) && ! empty( $block['attrs']['blockStyle'] ) ) { $block_id = isset( $block['attrs']['blockId'] ) ? sanitize_key( $block['attrs']['blockId'] ) : 'acumenpopup-block-' . md5( maybe_serialize( $block['attrs'] ) ); self::$styles[ $block_id ] = $this->sanitize_custom_css( $block['attrs']['blockStyle'] ); } return $block_content; } /** * Securely sanitize custom CSS by stripping HTML and dangerous vectors. * * @since 1.0.0 * @access private * * @param string $css Unsanitized CSS string. * * @return string Sanitized CSS string. */ private function sanitize_custom_css( $css ) { // Validate UTF-8 encoding $css = wp_check_invalid_utf8( $css ); if ( empty( $css ) ) { return ''; } // Normalize whitespace to prevent obfuscation tricks $css = preg_replace( '/\s+/', ' ', $css ); // Remove CSS comments (can hide payloads: /* expression */background:url() */) $css = preg_replace( '!/\*.*?\*/!s', '', $css ); // Remove backslash escapes used to bypass keyword filters (e.g. \65 xpression) $css = preg_replace( '/\\\\[0-9a-fA-F]{0,6}\s?/', '', $css ); // Block dangerous CSS functions and protocols // Covers: expression(), url(), javascript:, vbscript:, data:, behavior if ( preg_match( '/expression\s*\( | url\s*\( | javascript\s*: | vbscript\s*: | data\s*: | @import | behavior\s*: | -moz-binding\s*: | content\s*:/ix', $css ) ) { return ''; } // Block HTML tags that could escape the