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