'entire_site' | 'page' | 'post_type' | 'archive' | '404', * 'value' => '' (for entire_site/404) | comma-separated page IDs | post type slug | archive type * ] * * @package AdMoreWidgetsSuite * @since 1.0.0 */ if ( ! defined( 'ABSPATH' ) ) exit; class EPPRO_Theme_Builder_Conditions { /** Transient key for all published template IDs. */ private const TRANSIENT_IDS = 'eppro_hf_template_ids'; /** @var array All published templates for this request (loaded once). */ private static array $tpl_cache = []; /** @var bool Whether the one-shot query has run. */ private static bool $tpl_loaded = false; /** * Delete the cached template IDs transient. * Call this whenever a template is created, saved, trashed, or deleted. */ public static function flush_template_ids_cache(): void { delete_transient( self::TRANSIENT_IDS ); self::$tpl_loaded = false; self::$tpl_cache = []; } /** * Load ALL published templates in a single DB query and prime the * post-meta object cache so subsequent get_post_meta() calls are free. * Called once per request; subsequent calls are no-ops. * * Uses a transient to cache the list of published template IDs across * requests, avoiding a full post_type + post_status table scan every load. */ private static function load_all(): void { if ( self::$tpl_loaded ) return; self::$tpl_loaded = true; // Persistent cache: IDs of all published templates $tpl_ids = get_transient( self::TRANSIENT_IDS ); if ( false === $tpl_ids ) { $raw_ids = get_posts( [ 'post_type' => EPPRO_Theme_Builder_CPT::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => -1, 'no_found_rows' => true, 'fields' => 'ids', 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ] ); $tpl_ids = array_map( 'intval', $raw_ids ); set_transient( self::TRANSIENT_IDS, $tpl_ids, 12 * HOUR_IN_SECONDS ); } if ( empty( $tpl_ids ) ) { self::$tpl_cache = []; return; } self::$tpl_cache = get_posts( [ 'post_type' => EPPRO_Theme_Builder_CPT::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => -1, 'post__in' => $tpl_ids, 'no_found_rows' => true, 'update_post_meta_cache' => true, // Batch-primes the meta object cache 'update_post_term_cache' => false, 'orderby' => 'title', 'order' => 'ASC', ] ); } /** * Find the best matching template ID for a given type on the current page. * * Priority order (first match wins): * 1. Specific page IDs / taxonomy / 404 * 2. Post type match * 3. Modifier conditions (user_role / device / url_contains) * 4. Archive pages * 5. Entire site (lowest priority fallback) * * @param string $type 'header'|'footer'|'section' * @return int Template post ID, or 0 if none match. */ public static function get_matching_template( string $type ): int { // An empty type string would match templates with no _eppro_hf_type set. if ( '' === $type ) return 0; self::load_all(); // Filter in PHP — no extra DB hit (meta already in WP object cache) $templates = array_filter( self::$tpl_cache, fn( $tpl ) => get_post_meta( $tpl->ID, '_eppro_hf_type', true ) === $type ); if ( empty( $templates ) ) return 0; // Each slot stores [ 'id' => int, 'priority' => int ] so the // highest-priority template wins when multiple templates match. $specific_match = [ 'id' => 0, 'priority' => 0 ]; $posttype_match = [ 'id' => 0, 'priority' => 0 ]; $modifier_match = [ 'id' => 0, 'priority' => 0 ]; // user_role / device / url_contains $archive_match = [ 'id' => 0, 'priority' => 0 ]; $fallback_match = [ 'id' => 0, 'priority' => 0 ]; foreach ( $templates as $tpl ) { $conditions = get_post_meta( $tpl->ID, '_eppro_hf_conditions', true ); if ( ! is_array( $conditions ) || empty( $conditions ) ) continue; $priority = (int) ( get_post_meta( $tpl->ID, '_eppro_hf_priority', true ) ?: 10 ); foreach ( $conditions as $cond ) { $cond_type = $cond['type'] ?? ''; $cond_value = $cond['value'] ?? ''; switch ( $cond_type ) { case 'entire_site': if ( $priority > $fallback_match['priority'] ) { $fallback_match = [ 'id' => $tpl->ID, 'priority' => $priority ]; } break; case 'page': if ( self::matches_pages( $cond_value ) && $priority > $specific_match['priority'] ) { $specific_match = [ 'id' => $tpl->ID, 'priority' => $priority ]; } break; case 'post_type': if ( self::matches_post_type( $cond_value ) && $priority > $posttype_match['priority'] ) { $posttype_match = [ 'id' => $tpl->ID, 'priority' => $priority ]; } break; case 'archive': if ( self::matches_archive( $cond_value ) && $priority > $archive_match['priority'] ) { $archive_match = [ 'id' => $tpl->ID, 'priority' => $priority ]; } break; case '404': if ( is_404() && $priority > $specific_match['priority'] ) { $specific_match = [ 'id' => $tpl->ID, 'priority' => $priority ]; } break; case 'taxonomy': if ( self::matches_taxonomy( $cond_value ) && $priority > $specific_match['priority'] ) { $specific_match = [ 'id' => $tpl->ID, 'priority' => $priority ]; } break; case 'user_role': if ( self::matches_user_role( $cond_value ) && $priority > $modifier_match['priority'] ) { $modifier_match = [ 'id' => $tpl->ID, 'priority' => $priority ]; } break; case 'device': if ( self::matches_device( $cond_value ) && $priority > $modifier_match['priority'] ) { $modifier_match = [ 'id' => $tpl->ID, 'priority' => $priority ]; } break; case 'url_contains': if ( self::matches_url_contains( $cond_value ) && $priority > $modifier_match['priority'] ) { $modifier_match = [ 'id' => $tpl->ID, 'priority' => $priority ]; } break; } } } // Return by specificity tier, highest-priority template wins within each tier. return $specific_match['id'] ?: $posttype_match['id'] ?: $modifier_match['id'] ?: $archive_match['id'] ?: $fallback_match['id']; } /** * Check if current page is in the comma-separated list of page/post IDs. */ private static function matches_pages( string $value ): bool { if ( ! is_singular() ) return false; $ids = array_map( 'absint', array_filter( explode( ',', $value ) ) ); return in_array( get_queried_object_id(), $ids, true ); } /** * Check if current page belongs to a given post type (singular or archive). */ private static function matches_post_type( string $value ): bool { $pt = sanitize_key( $value ); if ( is_singular( $pt ) ) return true; if ( is_post_type_archive( $pt ) ) return true; return false; } /** * Check archive type. * * @param string $value 'all'|'category'|'tag'|'author'|'date'|'search' */ private static function matches_archive( string $value ): bool { switch ( $value ) { case 'all': return is_archive() || is_search(); case 'category': return is_category(); case 'tag': return is_tag(); case 'author': return is_author(); case 'date': return is_date(); case 'search': return is_search(); default: return false; } } /** * Check taxonomy (category, tag, or custom taxonomy term). * * @param string $value Taxonomy slug or term slug (e.g., 'category:news' or 'post_tag:tech') */ private static function matches_taxonomy( string $value ): bool { if ( empty( $value ) ) return false; // Format: taxonomy:term (e.g., 'category:news' or just 'news' for category) if ( strpos( $value, ':' ) !== false ) { list( $taxonomy, $term ) = explode( ':', $value, 2 ); return is_tax( $taxonomy, $term ) || is_category( $term ) || is_tag( $term ); } // Single value - check if it's a category or tag slug return is_category( $value ) || is_tag( $value ); } /** * Check user role. * * @param string $value Comma-separated roles (e.g., 'administrator,editor') */ private static function matches_user_role( string $value ): bool { if ( ! is_user_logged_in() ) { return 'logged_out' === $value; } if ( empty( $value ) ) return true; $user = wp_get_current_user(); $allowed_roles = array_map( 'trim', explode( ',', $value ) ); return ! empty( array_intersect( $allowed_roles, $user->roles ) ); } /** * Check device type. * * @param string $value 'desktop'|'tablet'|'mobile' */ private static function matches_device( string $value ): bool { $ua = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) ); $is_mobile = wp_is_mobile(); $is_tablet = $is_mobile && preg_match( '/tablet|ipad|playbook|silk/i', $ua ); $is_mobile = $is_mobile && ! $is_tablet; $is_desktop = ! $is_mobile && ! $is_tablet; switch ( $value ) { case 'desktop': return $is_desktop; case 'tablet': return $is_tablet; case 'mobile': return $is_mobile; default: return true; } } /** * Check if URL contains a string. * * @param string $value Needle to search in URL */ private static function matches_url_contains( string $value ): bool { if ( empty( $value ) ) return false; $current_url = home_url( esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ) ) ); return stripos( $current_url, $value ) !== false; } /** * Return condition type labels for the admin UI. */ public static function get_condition_types(): array { return [ 'entire_site' => __( 'Entire Site', 'admore-widgets-suite' ), 'page' => __( 'Specific Pages', 'admore-widgets-suite' ), 'post_type' => __( 'Post Type', 'admore-widgets-suite' ), 'archive' => __( 'Archive Pages', 'admore-widgets-suite' ), 'taxonomy' => __( 'Taxonomy (Category/Tag)', 'admore-widgets-suite' ), 'user_role' => __( 'User Role', 'admore-widgets-suite' ), 'device' => __( 'Device Type', 'admore-widgets-suite' ), 'url_contains' => __( 'URL Contains', 'admore-widgets-suite' ), '404' => __( '404 Page', 'admore-widgets-suite' ), ]; } }