init_hooks(); } /** * Get the single instance of the Campaigns class. * * @since 1.0.0 * @access public * * @return Campaigns The single instance. */ public static function get_instance() { if ( is_null( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } /** * Initialize WordPress hooks. * * @since 1.0.0 * @access private * * @return void */ private function init_hooks() { // Post type and meta registration. add_action( 'init', array( $this, 'register_post_type' ) ); add_action( 'init', array( $this, 'register_post_meta' ) ); // Block editor restrictions. add_action( 'enqueue_block_editor_assets', array( $this, 'restrict_blocks_to_post_type' ) ); // Frontend popup display. add_action( 'wp_footer', array( $this, 'render_popups' ) ); add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_all_frontend_assets' ) ); add_action( 'init', array( $this, 'handle_popup_cookies' ) ); // Admin AJAX actions. add_action( 'wp_ajax_acumenpopup_update_popup_status', array( $this, 'ajax_update_status' ) ); add_action( 'wp_ajax_acumenpopup_duplicate_campaign', array( $this, 'ajax_duplicate_campaign' ) ); // Admin list table customization. add_filter( 'manage_' . $this->post_type . '_posts_columns', array( $this, 'add_custom_columns' ) ); add_action( 'manage_' . $this->post_type . '_posts_custom_column', array( $this, 'render_custom_columns' ), 10, 2 ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) ); } /** * Register the campaigns custom post type. * * @since 1.0.0 * @access public * * @return void */ public function register_post_type() { $labels = array( 'name' => esc_html__( 'Campaigns', 'acumen-popup' ), 'singular_name' => esc_html__( 'Campaign', 'acumen-popup' ), 'menu_name' => esc_html__( 'Campaigns', 'acumen-popup' ), 'name_admin_bar' => esc_html__( 'Campaign', 'acumen-popup' ), 'add_new' => esc_html__( 'Create Campaign', 'acumen-popup' ), 'add_new_item' => esc_html__( 'Create New Campaign', 'acumen-popup' ), 'new_item' => esc_html__( 'New Campaign', 'acumen-popup' ), 'edit_item' => esc_html__( 'Edit Campaign', 'acumen-popup' ), 'view_item' => esc_html__( 'View Campaign', 'acumen-popup' ), 'all_items' => esc_html__( 'All Campaigns', 'acumen-popup' ), 'search_items' => esc_html__( 'Search Campaigns', 'acumen-popup' ), 'parent_item_colon' => esc_html__( 'Parent Campaigns:', 'acumen-popup' ), 'not_found' => esc_html__( 'No campaigns found.', 'acumen-popup' ), 'not_found_in_trash' => esc_html__( 'No campaigns found in Trash.', 'acumen-popup' ), ); $args = array( 'labels' => $labels, 'description' => esc_html__( 'Popup Campaigns for your website', 'acumen-popup' ), 'public' => false, 'publicly_queryable' => false, 'show_ui' => true, 'show_in_menu' => false, 'query_var' => false, 'rewrite' => false, 'capability_type' => 'post', 'has_archive' => false, 'hierarchical' => false, 'menu_position' => 20, 'menu_icon' => 'dashicons-megaphone', 'show_in_rest' => true, 'template' => array( array( 'acumenpopup/popup-builder' ), ), 'template_lock' => 'insert', 'supports' => array( 'title', 'editor', 'author', 'custom-fields' ), ); register_post_type( $this->post_type, $args ); } /** * Register post meta fields for REST API access. * * @since 1.0.0 * @access public * * @return void */ public function register_post_meta() { $meta_fields = array( 'acumenpopup_popup_id' => array( 'type' => 'string', 'default' => '', ), 'acumenpopup_popup_trigger' => array( 'type' => 'string', 'default' => 'onload', ), 'acumenpopup_popup_enable_disable' => array( 'type' => 'boolean', 'default' => true, ), 'acumenpopup_popup_infinite_repeat' => array( 'type' => 'boolean', 'default' => true, ), 'acumenpopup_popup_repeat_num' => array( 'type' => 'number', 'default' => 1, ), ); foreach ( $meta_fields as $key => $config ) { register_post_meta( $this->post_type, $key, array( 'show_in_rest' => true, 'single' => true, 'type' => $config['type'], 'default' => $config['default'], 'auth_callback' => function () { return current_user_can( 'edit_posts' ); }, ) ); } } /** * Restrict Acumenpopup blocks to only appear in campaign post type. * * @since 1.0.0 * @access public * * @return void */ public function restrict_blocks_to_post_type() { $screen = get_current_screen(); if ( ! $screen || ! method_exists( $screen, 'is_block_editor' ) || ! $screen->is_block_editor() ) { return; } // Only restrict blocks on non-campaign post types. if ( $this->post_type === $screen->post_type ) { return; } $restricted_blocks = array( 'acumenpopup/popup-builder', 'acumenpopup/container', 'acumenpopup/advanced-image', 'acumenpopup/paragraph', 'acumenpopup/button', 'acumenpopup/icon', 'acumenpopup/form', 'acumenpopup/text', 'acumenpopup/email', 'acumenpopup/message', ); $script = sprintf( 'wp.domReady(function() { var restrictedBlocks = %s; if (wp.blocks && wp.blocks.unregisterBlockType) { restrictedBlocks.forEach(function(blockName) { if (wp.blocks.getBlockType(blockName)) { wp.blocks.unregisterBlockType(blockName); } }); } });', wp_json_encode( $restricted_blocks ) ); wp_add_inline_script( 'wp-blocks', $script ); } /** * Handle popup cookie logic for repeat limiting. * * @since 1.0.0 * @access public * * @return void */ public function handle_popup_cookies() { if ( is_admin() ) { return; } $popups = $this->get_published_popups(); if ( empty( $popups ) ) { return; } foreach ( $popups as $post_id ) { $this->process_popup_cookie( $post_id ); } } /** * Process cookie for a single popup. * * @since 1.0.0 * @access private * * @param int $post_id The popup post ID. * * @return void */ private function process_popup_cookie( $post_id ) { $popup_id = get_post_meta( $post_id, 'acumenpopup_popup_id', true ); $repeat_num = get_post_meta( $post_id, 'acumenpopup_popup_repeat_num', true ); $infinite_repeat = get_post_meta( $post_id, 'acumenpopup_popup_infinite_repeat', true ); if ( empty( $popup_id ) ) { return; } $repeat_num = $infinite_repeat ? -1 : absint( $repeat_num ); $cookie_name = 'acumenpopup_' . sanitize_key( $popup_id ); // Get current cookie value. $cookie_value = 0; if ( isset( $_COOKIE[ $cookie_name ] ) ) { $cookie_value = absint( sanitize_text_field( wp_unslash( $_COOKIE[ $cookie_name ] ) ) ); } // Increment cookie if within repeat limit. if ( -1 !== $repeat_num && $cookie_value < $repeat_num ) { $cookie_path = defined( 'COOKIEPATH' ) && COOKIEPATH ? COOKIEPATH : '/'; setcookie( $cookie_name, $cookie_value + 1, time() + DAY_IN_SECONDS, $cookie_path ); $_COOKIE[ $cookie_name ] = $cookie_value + 1; } } /** * Render popups on the frontend. * * @since 1.0.0 * @access public * * @return void */ public function render_popups() { if ( is_admin() ) { return; } $popups = $this->get_published_popups( false ); if ( empty( $popups ) ) { return; } foreach ( $popups as $post ) { $this->render_single_popup( $post ); } } /** * Properly enqueue all frontend assets and styles via wp_enqueue_scripts hook. * * @since 1.0.0 * @access public * * @return void */ public function enqueue_all_frontend_assets() { if ( is_admin() ) { return; } $popups = $this->get_published_popups( false ); if ( empty( $popups ) ) { return; } $should_enqueue_core = false; foreach ( $popups as $post ) { $meta = $this->get_popup_meta( $post->ID ); // Skip if disabled or cookie limits reached. if ( ! $meta['enabled'] || ! $this->should_show_popup( $meta ) ) { continue; } $should_enqueue_core = true; // Process and enqueue block styles. $blocks = parse_blocks( $post->post_content ); $styles = $this->collect_block_styles( $blocks ); if ( ! empty( $styles ) ) { $this->add_inline_styles( $styles ); } } if ( $should_enqueue_core ) { $this->enqueue_frontend_assets(); } } /** * Render a single popup. * * @since 1.0.0 * @access private * * @param \WP_Post $post The popup post object. * * @return void */ private function render_single_popup( $post ) { $post_id = $post->ID; $meta = $this->get_popup_meta( $post_id ); // Skip if disabled. if ( ! $meta['enabled'] ) { return; } // Check cookie limits. if ( ! $this->should_show_popup( $meta ) ) { return; } // Output the popup content. // Note: apply_filters is needed for blocks to render properly. echo wp_kses_post( apply_filters( 'the_content', $post->post_content ) ); } /** * Get popup meta data. * * @since 1.0.0 * @access private * * @param int $post_id The popup post ID. * * @return array Popup meta data. */ private function get_popup_meta( $post_id ) { $popup_id = get_post_meta( $post_id, 'acumenpopup_popup_id', true ); $enabled = get_post_meta( $post_id, 'acumenpopup_popup_enable_disable', true ); $infinite_repeat = get_post_meta( $post_id, 'acumenpopup_popup_infinite_repeat', true ); $repeat_num = get_post_meta( $post_id, 'acumenpopup_popup_repeat_num', true ); return array( 'popup_id' => sanitize_key( $popup_id ), 'enabled' => filter_var( $enabled, FILTER_VALIDATE_BOOLEAN ), 'infinite_repeat' => filter_var( $infinite_repeat, FILTER_VALIDATE_BOOLEAN ), 'repeat_num' => $infinite_repeat ? -1 : absint( $repeat_num ), ); } /** * Check if popup should be shown based on cookie limits. * * @since 1.0.0 * @access private * * @param array $meta Popup meta data. * * @return bool True if popup should be shown. */ private function should_show_popup( $meta ) { $cookie_name = 'acumenpopup_' . $meta['popup_id']; $cookie_value = 0; if ( isset( $_COOKIE[ $cookie_name ] ) ) { $cookie_value = absint( sanitize_text_field( wp_unslash( $_COOKIE[ $cookie_name ] ) ) ); } return -1 === $meta['repeat_num'] || $cookie_value < $meta['repeat_num']; } /** * Enqueue frontend assets for popups. * * @since 1.0.0 * @access private * * @return void */ private function enqueue_frontend_assets() { wp_enqueue_style( 'acumenpopup-animations-css', trailingslashit( ACUMENPOPUP_URL_FILE ) . 'assets/css/popup-animations.css', array(), ACUMENPOPUP_VERSION ); wp_enqueue_script( 'acumenpopup-frontend-js', trailingslashit( ACUMENPOPUP_URL_FILE ) . 'assets/js/popup-frontend.js', array(), ACUMENPOPUP_VERSION, true ); wp_enqueue_script( 'acumenpopup-triggers-js', trailingslashit( ACUMENPOPUP_URL_FILE ) . 'assets/js/popup-triggers.js', array(), ACUMENPOPUP_VERSION, true ); } /** * Collect block styles recursively. * * @since 1.0.0 * @access private * * @param array $blocks Array of parsed blocks. * * @return string Combined CSS styles. */ private function collect_block_styles( $blocks ) { $styles = ''; if ( empty( $blocks ) || ! is_array( $blocks ) ) { return $styles; } foreach ( $blocks as $block ) { // Collect blockStyle attribute. if ( isset( $block['attrs']['blockStyle'] ) && ! empty( $block['attrs']['blockStyle'] ) ) { // Sanitize the CSS string to strip any HTML tags or scripts before output. $styles .= $this->sanitize_custom_css( $block['attrs']['blockStyle'] ); } // Process inner blocks. if ( ! empty( $block['innerBlocks'] ) ) { $styles .= $this->collect_block_styles( $block['innerBlocks'] ); } } return $styles; } /** * 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