post_type && $new !== $old ) { delete_transient( 'akram_tb_index' ); Frontend::flush_caches(); } } public static function post_deleted( $id, $post = null ) { if ( $post && 'akram_template' === $post->post_type ) { delete_transient( 'akram_tb_index' ); } } // ------------------------------------------------------------------ // Storage // ------------------------------------------------------------------ public static function get_conditions( $id ) { $c = get_post_meta( $id, '_akram_conditions', true ); return is_array( $c ) ? $c : array(); } public static function set_conditions( $id, array $rules ) { $clean = array(); foreach ( $rules as $r ) { if ( empty( $r['rule'] ) || ! preg_match( '/^[a-z0-9_]+(:[a-z0-9_\-]+)?$/', (string) $r['rule'] ) ) { continue; } $clean[] = array( 'mode' => 'exclude' === ( $r['mode'] ?? '' ) ? 'exclude' : 'include', 'rule' => (string) $r['rule'], ); } update_post_meta( $id, '_akram_conditions', $clean ); delete_transient( 'akram_tb_index' ); Frontend::flush_caches(); } public static function get_settings( $id ) { $s = get_post_meta( $id, '_akram_settings', true ); return is_array( $s ) ? $s : array(); } public static function set_settings( $id, array $settings ) { $clean = array(); array_walk_recursive( $settings, static function () {} ); foreach ( $settings as $k => $v ) { $k = sanitize_key( $k ); if ( is_array( $v ) ) { $clean[ $k ] = array_map( static function ( $x ) { return is_scalar( $x ) ? sanitize_text_field( (string) $x ) : ''; }, $v ); } elseif ( is_bool( $v ) || is_numeric( $v ) ) { $clean[ $k ] = $v; } else { $clean[ $k ] = sanitize_text_field( (string) $v ); } } update_post_meta( $id, '_akram_settings', $clean ); if ( isset( $clean['preview'] ) ) { update_post_meta( $id, '_akram_preview', (int) $clean['preview'] ); } delete_transient( 'akram_tb_index' ); } /** Published templates grouped by type (cached). */ private static function index() { $idx = get_transient( 'akram_tb_index' ); if ( is_array( $idx ) ) { return $idx; } $idx = array(); $ids = get_posts( array( 'post_type' => 'akram_template', 'post_status' => 'publish', 'posts_per_page' => 500, 'fields' => 'ids', 'orderby' => 'modified', 'order' => 'DESC', 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- bounded query on a builder meta key. array( 'key' => '_akram_type', 'value' => array( 'header', 'footer', 'single', 'archive', 'search', '404', 'popup' ), 'compare' => 'IN', ), ), ) ); foreach ( $ids as $id ) { $idx[ Post_Types::template_type( $id ) ][] = array( 'id' => $id, 'c' => self::get_conditions( $id ), ); } set_transient( 'akram_tb_index', $idx, DAY_IN_SECONDS ); return $idx; } // ------------------------------------------------------------------ // Matching // ------------------------------------------------------------------ public static function rule_score( $rule ) { list( $name, $arg ) = array_pad( explode( ':', $rule, 2 ), 2, '' ); switch ( $name ) { case 'entire': return 10; case 'singular': return $arg ? 60 : 40; case 'archive': return 40; case 'post': return self::is_post( (int) $arg ) ? 100 : 0; case 'front': return is_front_page() ? 95 : 0; case 'blog': return is_home() ? 95 : 0; default: return 70; } } public static function matches( $rule ) { list( $name, $arg ) = array_pad( explode( ':', (string) $rule, 2 ), 2, '' ); $qid = get_queried_object_id(); switch ( $name ) { case 'entire': return true; case 'front': return is_front_page(); case 'blog': return is_home(); case 'singular': return is_singular() && ( ! $arg || is_singular( $arg ) ); case 'post': return self::is_post( (int) $arg ); case 'child': return is_singular() && (int) wp_get_post_parent_id( $qid ) === (int) $arg; case 'in_term': $t = get_term( (int) $arg ); return is_singular() && $t && ! is_wp_error( $t ) && has_term( (int) $arg, $t->taxonomy, $qid ); case 'archive': return is_archive() || is_home() && ! is_front_page(); case 'pt_archive': return is_post_type_archive( $arg ) || ( 'post' === $arg && is_home() ) || ( 'product' === $arg && function_exists( 'is_shop' ) && is_shop() ); case 'tax': return 'category' === $arg ? is_category() : ( 'post_tag' === $arg ? is_tag() : is_tax( $arg ) ); case 'term': $t = get_queried_object(); return ( is_category() || is_tag() || is_tax() ) && $t && (int) $t->term_id === (int) $arg; case 'author': return is_author( $arg ? (int) $arg : '' ); case 'date': return is_date(); case 'search': return is_search(); case '404': return is_404(); } return (bool) apply_filters( 'akram_condition_rule', false, $name, $arg ); } private static function is_post( $id ) { if ( ! $id ) { return false; } if ( is_singular() && get_queried_object_id() === $id ) { return true; } return ( is_home() && (int) get_option( 'page_for_posts' ) === $id ); } /** Best matching template id for a location, 0 if none. */ public static function resolve( $type ) { $idx = self::index(); $best = 0; $score = 0; foreach ( (array) ( $idx[ $type ] ?? array() ) as $tpl ) { $s = self::score( $tpl['c'] ); if ( $s > $score ) { $score = $s; $best = (int) $tpl['id']; } } return (int) apply_filters( 'akram_template_for_location', $best, $type ); } /** Highest matching include score, or 0 if excluded/unmatched. */ public static function score( array $conds ) { $s = 0; foreach ( $conds as $c ) { if ( 'exclude' === $c['mode'] && self::matches( $c['rule'] ) ) { return 0; } } foreach ( $conds as $c ) { if ( 'include' === $c['mode'] && self::matches( $c['rule'] ) ) { $s = max( $s, self::rule_score( $c['rule'] ) ); } } return $s; } public static function setup() { if ( is_admin() || null !== self::$resolved ) { return; } self::$resolved = array(); $canvas = Editor::canvas_id(); if ( 'akram-canvas' === get_page_template_slug( get_queried_object_id() ) && is_singular() ) { return; // canvas pages never get header/footer } $canvas_type = $canvas && 'akram_template' === get_post_type( $canvas ) ? Post_Types::template_type( $canvas ) : ''; foreach ( array( 'header', 'footer' ) as $loc ) { if ( $canvas_type === $loc ) { continue; } $id = self::resolve( $loc ); if ( $id ) { self::$resolved[ $loc ] = $id; } } if ( $canvas_type ) { return; // body comes from the editor } $body = ''; if ( is_404() ) { $body = '404'; } elseif ( is_search() ) { $body = self::resolve( 'search' ) ? 'search' : 'archive'; } elseif ( is_singular() ) { $body = 'single'; } elseif ( is_archive() || is_home() ) { $body = 'archive'; } if ( $body ) { $id = self::resolve( $body ); if ( $id && ! ( 'single' === $body && Document::is_built( get_queried_object_id() ) && ! self::explicit_single( $id ) ) ) { self::$resolved['body'] = $id; self::$body_type = $body; } } if ( isset( self::$resolved['header'] ) && ! wp_is_block_theme_safe() ) { add_action( 'get_header', array( __CLASS__, 'replace_header' ), 999 ); } if ( isset( self::$resolved['footer'] ) && ! wp_is_block_theme_safe() ) { add_action( 'get_footer', array( __CLASS__, 'replace_footer' ), 999 ); } if ( ! empty( self::$resolved ) ) { add_action( 'wp_enqueue_scripts', array( __CLASS__, 'head_css' ) ); } } /** A single template only replaces an Akram-built page when it targets that page explicitly. */ private static function explicit_single( $tpl ) { foreach ( self::get_conditions( $tpl ) as $c ) { if ( 'include' === $c['mode'] && 0 === strpos( $c['rule'], 'post:' ) && self::matches( $c['rule'] ) ) { return true; } } return false; } public static function active_templates() { self::setup(); return array_values( array_unique( array_filter( (array) self::$resolved ) ) ); } public static function has( $loc ) { self::setup(); return ! empty( self::$resolved[ $loc ] ); } public static function template_include( $template ) { if ( Editor::canvas_id() && 'akram_template' === get_post_type( Editor::canvas_id() ) ) { return $template; } if ( self::has( 'body' ) ) { return AKRAM_DIR . 'templates/theme.php'; } return $template; } public static function head_css() { $css = '.nx-site-header,.nx-site-footer{position:relative;z-index:50}.nx-site-header.is-overlay{position:absolute;left:0;right:0;top:0;z-index:60}.admin-bar .nx-site-header.is-overlay{top:var(--wp-admin--admin-bar--height,0)}' // The content area fills the theme's wrapper, also when that wrapper is a flex row (Astra, GeneratePress). . '.nx-main{display:block;flex:1 1 100%;width:100%;max-width:100%;min-width:0}'; wp_register_style( 'akram-tb', false, array(), AKRAM_VERSION ); wp_enqueue_style( 'akram-tb' ); wp_add_inline_style( 'akram-tb', $css ); } // ------------------------------------------------------------------ // Output // ------------------------------------------------------------------ public static function header_html() { $id = self::$resolved['header'] ?? 0; if ( ! $id ) { return ''; } $s = self::get_settings( $id ); return ''; } public static function footer_html() { $id = self::$resolved['footer'] ?? 0; return $id ? '' : ''; } /** Classic themes: print our document head + header, then swallow the theme's header.php. */ public static function replace_header( $name ) { if ( self::$header_done ) { return; } self::$header_done = true; self::open_document(); echo self::header_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in Frontend::render_post() (KSES allow-list). $templates = array(); if ( $name ) { $templates[] = "header-{$name}.php"; } $templates[] = 'header.php'; remove_all_actions( 'wp_head' ); remove_all_actions( 'wp_body_open' ); ob_start(); locate_template( $templates, true ); ob_end_clean(); } public static function replace_footer( $name ) { if ( self::$footer_done ) { return; } self::$footer_done = true; echo self::footer_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in Frontend::render_post() (KSES allow-list). wp_footer(); echo "\n\n"; $templates = array(); if ( $name ) { $templates[] = "footer-{$name}.php"; } $templates[] = 'footer.php'; remove_all_actions( 'wp_footer' ); ob_start(); locate_template( $templates, true ); ob_end_clean(); } private static function open_document() { ?> > > ' . esc_html__( 'Skip to content', 'akram' ) . ''; } /** Used by Akram page templates: works for classic and block themes. */ public static function header() { self::setup(); // Akram templates always need the layout rules (wp_head() has not run yet). add_action( 'wp_enqueue_scripts', array( __CLASS__, 'head_css' ) ); if ( ! wp_is_block_theme_safe() ) { self::classic_full_width(); ob_start(); get_header(); $html = (string) ob_get_clean(); echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- the theme's own header.php output, unchanged. self::$main_id = self::skip_target( $html ); return; } self::$header_done = true; // Like core's template canvas, the theme's parts are rendered before wp_head(): blocks such as Navigation // enqueue their styles, scripts and script modules while rendering, and a block theme prints them (and the // import map) in the head. foreach ( array( 'header', 'footer' ) as $part ) { if ( ! self::has( $part ) && function_exists( 'block_template_part' ) ) { ob_start(); block_template_part( $part ); self::$parts[ $part ] = ob_get_clean(); } } self::open_document(); echo '
'; if ( self::has( 'header' ) ) { echo self::header_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in Frontend::render_post() (KSES allow-list). } elseif ( isset( self::$parts['header'] ) ) { echo self::$parts['header']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- block markup rendered by core's block_template_part(). } } /** * Opens the content area of an Akram template. The theme's own header.php (a classic theme without an Akram header) * already has its #content element, so the id is only used when Akram prints the document. */ public static function main_open( $class = '' ) { echo ''; } /** * Classic themes: the id their "Skip to content" link points to, or '' when the header already has that element * (themes such as Hello and Kadence add it in their own page templates, which an Akram template replaces). */ private static function skip_target( $html ) { $id = 'content'; if ( preg_match_all( '/]*>/i', $html, $tags ) ) { foreach ( $tags[0] as $tag ) { if ( false !== stripos( $tag, 'skip-link' ) && preg_match( '/href=["\']#([\w-]+)["\']/', $tag, $m ) ) { $id = $m[1]; break; } } } return preg_match( '/\bid=["\']' . preg_quote( $id, '/' ) . '["\']/', $html ) ? '' : $id; } /** * Classic themes print their own content wrappers in header.php. Themes that offer a full-width, no-sidebar * layout are asked for it, so Akram pages use the whole width as they do in the editor. */ private static function classic_full_width() { // Astra: "Full Width / Stretched" content layout, no sidebar. add_filter( 'astra_page_layout', array( __CLASS__, 'no_sidebar' ) ); add_filter( 'astra_get_content_layout', array( __CLASS__, 'astra_stretched' ) ); // GeneratePress: no sidebar and its full-width content container. add_filter( 'generate_sidebar_layout', array( __CLASS__, 'no_sidebar' ) ); if ( 'generatepress' === get_template() ) { add_filter( 'body_class', array( __CLASS__, 'generatepress_full_width' ) ); } } public static function no_sidebar() { return 'no-sidebar'; } public static function astra_stretched() { return 'page-builder'; } public static function generatepress_full_width( $classes ) { $classes[] = 'full-width-content'; return $classes; } public static function footer() { if ( ! wp_is_block_theme_safe() ) { get_footer(); return; } self::$footer_done = true; if ( self::has( 'footer' ) ) { echo self::footer_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in Frontend::render_post() (KSES allow-list). } elseif ( isset( self::$parts['footer'] ) ) { echo self::$parts['footer']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- block markup rendered by core's block_template_part(). } elseif ( function_exists( 'block_template_part' ) ) { block_template_part( 'footer' ); } echo '
'; wp_footer(); echo "\n\n"; } /** Block themes: replace header/footer template parts. */ public static function block_parts( $content, $block ) { if ( 'core/template-part' !== ( $block['blockName'] ?? '' ) || is_admin() ) { return $content; } $slug = (string) ( $block['attrs']['slug'] ?? '' ); $area = (string) ( $block['attrs']['area'] ?? $block['attrs']['tagName'] ?? '' ); if ( ( 'header' === $area || false !== strpos( $slug, 'header' ) ) && self::has( 'header' ) ) { return self::header_html(); } if ( ( 'footer' === $area || false !== strpos( $slug, 'footer' ) ) && self::has( 'footer' ) ) { return self::footer_html(); } return $content; } public static function render_location( $type ) { $id = self::$resolved['body'] ?? 0; if ( ! $id ) { return; } if ( 'single' === $type && have_posts() ) { the_post(); } echo Frontend::render_post( $id ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in Frontend::render_post() (KSES allow-list). if ( 'single' === $type ) { rewind_posts(); } } } /** wp_is_block_theme() exists since 5.9. */ function wp_is_block_theme_safe() { return function_exists( 'wp_is_block_theme' ) && wp_is_block_theme(); }