__( 'Header', 'nyxonn-builder' ), 'footer' => __( 'Footer', 'nyxonn-builder' ), 'section' => __( 'Section', 'nyxonn-builder' ), 'single' => __( 'Single Post', 'nyxonn-builder' ), 'page' => __( 'Page', 'nyxonn-builder' ), 'archive' => __( 'Archive', 'nyxonn-builder' ), 'category' => __( 'Category', 'nyxonn-builder' ), 'loop' => __( 'Loop', 'nyxonn-builder' ), 'popup' => __( 'Popup', 'nyxonn-builder' ), '404' => __( '404 Page', 'nyxonn-builder' ), ]; } /** * Initialize template manager */ public static function init() { // Builder initializes this class during `init`; register immediately in // that case so the post type exists for Theme Builder routes. if ( did_action( 'init' ) ) { self::register_post_type(); } else { add_action( 'init', [ __CLASS__, 'register_post_type' ] ); } add_action( 'rest_api_init', [ __CLASS__, 'register_rest_routes' ] ); add_action( 'add_meta_boxes', [ __CLASS__, 'add_meta_boxes' ] ); add_action( 'save_post', [ __CLASS__, 'save_meta_boxes' ] ); add_action( 'wp_body_open', [ __CLASS__, 'inject_header' ], 1 ); add_action( 'wp_footer', [ __CLASS__, 'inject_footer' ], 999 ); add_filter( 'pre_render_block', [ __CLASS__, 'suppress_matching_theme_part' ], 10, 2 ); add_filter( 'nyxonn_fx_dynamic_preview_post_id', [ __CLASS__, 'filter_dynamic_preview_post_id' ], 10, 2 ); } /** * Register custom post type for templates */ public static function register_post_type() { register_post_type( Prefix_Migration::LEGACY_POST_TYPE, [ 'public' => false, 'show_ui' => false, 'show_in_rest' => false, 'rewrite' => false, ] ); $labels = [ 'name' => __( 'Nyxonn Builder Templates', 'nyxonn-builder' ), 'singular_name' => __( 'Nyxonn Builder Template', 'nyxonn-builder' ), 'add_new' => __( 'Add New Template', 'nyxonn-builder' ), 'add_new_item' => __( 'Add New Nyxonn Builder Template', 'nyxonn-builder' ), 'edit_item' => __( 'Edit Nyxonn Builder Template', 'nyxonn-builder' ), 'new_item' => __( 'New Nyxonn Builder Template', 'nyxonn-builder' ), 'view_item' => __( 'View Nyxonn Builder Template', 'nyxonn-builder' ), 'search_items' => __( 'Search Templates', 'nyxonn-builder' ), 'not_found' => __( 'No templates found', 'nyxonn-builder' ), 'not_found_in_trash' => __( 'No templates found in Trash', 'nyxonn-builder' ), 'all_items' => __( 'All Templates', 'nyxonn-builder' ), 'menu_name' => __( 'Templates', 'nyxonn-builder' ), ]; $args = [ 'labels' => $labels, 'public' => false, 'show_ui' => true, 'show_in_menu' => false, 'show_in_rest' => true, 'supports' => [ 'title' ], 'capability_type' => 'post', 'map_meta_cap' => true, 'menu_icon' => 'dashicons-layout', 'has_archive' => false, 'rewrite' => false, ]; register_post_type( self::TEMPLATES_POST_TYPE, $args ); } /** * Register REST routes for templates */ public static function register_rest_routes() { // List templates register_rest_route( 'nyxonn-builder/v1', '/templates', [ 'methods' => 'GET', 'callback' => [ __CLASS__, 'rest_list_templates' ], 'permission_callback' => function() { return current_user_can( 'manage_options' ); } ] ); register_rest_route( 'nyxonn-builder/v1', '/theme-content', [ 'methods' => 'GET', 'callback' => [ __CLASS__, 'rest_search_theme_content' ], 'permission_callback' => static function () { return current_user_can( 'manage_options' ) && class_exists( Role_Manager::class ) && Role_Manager::can_use_builder(); }, 'args' => [ 'search' => [ 'sanitize_callback' => 'sanitize_text_field' ], 'type' => [ 'sanitize_callback' => 'sanitize_key' ], 'page' => [ 'sanitize_callback' => 'absint' ], ], ] ); register_rest_route( 'nyxonn-builder/v1', '/section-templates', [ [ 'methods' => 'GET', 'callback' => [ __CLASS__, 'rest_list_section_templates' ], 'permission_callback' => function() { return class_exists( Role_Manager::class ) && Role_Manager::can_use_builder(); }, ], [ 'methods' => 'POST', 'callback' => [ __CLASS__, 'rest_create_section_template' ], 'permission_callback' => [ __CLASS__, 'can_create_published_template' ], ], ] ); register_rest_route( 'nyxonn-builder/v1', '/section-templates/(?P\d+)', [ 'methods' => 'POST', 'callback' => [ __CLASS__, 'rest_update_section_template' ], 'permission_callback' => function( $request ) { $template_id = intval( $request['template_id'] ); return $template_id > 0 && class_exists( Role_Manager::class ) && Role_Manager::can_use_builder( $template_id ) && current_user_can( 'edit_post', $template_id ); }, ] ); register_rest_route( 'nyxonn-builder/v1', '/page-templates', [ [ 'methods' => 'GET', 'callback' => [ __CLASS__, 'rest_list_page_templates' ], 'permission_callback' => function() { return class_exists( Role_Manager::class ) && Role_Manager::can_use_builder(); }, ], [ 'methods' => 'POST', 'callback' => [ __CLASS__, 'rest_create_page_template' ], 'permission_callback' => [ __CLASS__, 'can_create_published_template' ], ], ] ); register_rest_route( 'nyxonn-builder/v1', '/page-templates/(?P\d+)', [ 'methods' => 'POST', 'callback' => [ __CLASS__, 'rest_update_page_template' ], 'permission_callback' => function( $request ) { $template_id = intval( $request['template_id'] ); return $template_id > 0 && class_exists( Role_Manager::class ) && Role_Manager::can_use_builder( $template_id ) && current_user_can( 'edit_post', $template_id ); }, ] ); register_rest_route( 'nyxonn-builder/v1', '/design-templates', [ [ 'methods' => 'GET', 'callback' => [ __CLASS__, 'rest_list_design_templates' ], 'permission_callback' => [ __CLASS__, 'can_use_template_library' ], ], [ 'methods' => 'POST', 'callback' => [ __CLASS__, 'rest_create_design_template' ], 'permission_callback' => [ __CLASS__, 'can_create_published_template' ], ], ] ); register_rest_route( 'nyxonn-builder/v1', '/design-templates/(?P\d+)', [ [ 'methods' => 'GET', 'callback' => [ __CLASS__, 'rest_get_design_template' ], 'permission_callback' => [ __CLASS__, 'can_access_design_template' ], ], [ 'methods' => [ 'POST', 'PATCH' ], 'callback' => [ __CLASS__, 'rest_update_design_template' ], 'permission_callback' => [ __CLASS__, 'can_access_design_template' ], ], [ 'methods' => 'DELETE', 'callback' => [ __CLASS__, 'rest_delete_design_template' ], 'permission_callback' => [ __CLASS__, 'can_delete_design_template' ], ], ] ); } public static function can_use_template_library() { return class_exists( Role_Manager::class ) && Role_Manager::can_use_builder() && current_user_can( 'edit_posts' ); } /** All current create endpoints publish immediately; reject rather than silently downgrade. */ public static function can_create_published_template() { if ( ! class_exists( Role_Manager::class ) || ! Role_Manager::can_use_builder() ) { return new \WP_Error( 'nyxonn_builder_editor_access_required', __( 'Builder access is required to create templates.', 'nyxonn-builder' ), [ 'status' => 403 ] ); } if ( ! current_user_can( 'edit_posts' ) ) { return new \WP_Error( 'nyxonn_builder_tpl_create_forbidden', __( 'You are not allowed to create templates.', 'nyxonn-builder' ), [ 'status' => 403 ] ); } if ( ! current_user_can( 'publish_posts' ) ) { return new \WP_Error( 'nyxonn_builder_tpl_publish_forbidden', __( 'You are not allowed to publish templates.', 'nyxonn-builder' ), [ 'status' => 403 ] ); } return true; } public static function can_access_design_template( $request ) { $template_id = intval( $request['template_id'] ); return self::can_use_template_library() && $template_id > 0 && current_user_can( 'edit_post', $template_id ); } public static function can_delete_design_template( $request ) { $template_id = intval( $request['template_id'] ); return self::can_use_template_library() && $template_id > 0 && current_user_can( 'delete_post', $template_id ); } private static function design_type_from_record( $template_id ) { $type = Prefix_Migration::get_post_meta( $template_id, self::TEMPLATE_TYPE_META, true ); if ( self::PAGE_TEMPLATE_TYPE === $type || ( 'page' === $type && self::PAGE_TEMPLATE_SOURCE === Prefix_Migration::get_post_meta( $template_id, self::TEMPLATE_SOURCE_META, true ) ) ) { return 'page'; } if ( self::SECTION_TEMPLATE_TYPE === $type ) { return 'section'; } if ( self::ROW_TEMPLATE_TYPE === $type ) { return 'row'; } return ''; } private static function storage_type_for_design_type( $type ) { return 'page' === $type ? self::PAGE_TEMPLATE_TYPE : ( 'section' === $type ? self::SECTION_TEMPLATE_TYPE : self::ROW_TEMPLATE_TYPE ); } private static function normalize_design_payload( $type, $payload ) { if ( is_string( $payload ) ) { $payload = json_decode( $payload, true ); } if ( ! is_array( $payload ) ) { return new \WP_Error( 'invalid_template_payload', __( 'Template layout must be structured JSON data.', 'nyxonn-builder' ), [ 'status' => 400 ] ); } if ( class_exists( Dynamic_Content::class ) ) { $payload = Dynamic_Content::sanitize_layout_descriptors( $payload ); } if ( 'page' === $type ) { $layout = array_values( $payload ); $stored_payload = $layout; } elseif ( 'section' === $type ) { $node = isset( $payload[0] ) && is_array( $payload[0] ) ? $payload[0] : $payload; $layout = [ $node ]; $stored_payload = $node; } elseif ( 'row' === $type ) { $node = isset( $payload[0] ) && is_array( $payload[0] ) ? $payload[0] : $payload; $layout = [ [ 'type' => 'section', 'rows' => [ $node ], 'settings' => [] ] ]; $stored_payload = $node; } else { return new \WP_Error( 'invalid_template_type', __( 'Template type must be page, section, or row.', 'nyxonn-builder' ), [ 'status' => 400 ] ); } if ( empty( $layout ) ) { return new \WP_Error( 'empty_template', __( 'The template layout cannot be empty.', 'nyxonn-builder' ), [ 'status' => 400 ] ); } $canonical = Save::canonicalize_layout( $layout ); if ( is_wp_error( $canonical ) ) { $canonical->add_data( [ 'status' => 400 ] ); return $canonical; } $layout = json_decode( $canonical, true ); if ( 'page' === $type ) $stored_payload = $layout; elseif ( 'section' === $type ) $stored_payload = $layout[0]; else $stored_payload = $layout[0]['rows'][0]; return [ 'payload' => $stored_payload, 'layout' => $layout ]; } private static function get_design_payload( $template_id, $type ) { $layout_json = Save::get_layout_for_post( $template_id ); $layout = is_string( $layout_json ) ? json_decode( $layout_json, true ) : []; if ( is_array( $layout ) && $layout ) { if ( 'section' === $type ) return $layout[0] ?? null; if ( 'row' === $type ) return $layout[0]['rows'][0] ?? null; return $layout; } $raw = Prefix_Migration::get_post_meta( $template_id, self::DESIGN_PAYLOAD_META, true ); if ( is_string( $raw ) && '' !== $raw ) { $decoded = json_decode( $raw, true ); if ( JSON_ERROR_NONE === json_last_error() ) { $prepared = self::normalize_design_payload( $type, $decoded ); return is_wp_error( $prepared ) ? null : $prepared['payload']; } } return null; } private static function format_design_template_metadata( $template ) { $type = self::design_type_from_record( $template->ID ); return [ 'id' => intval( $template->ID ), 'title' => $template->post_title, 'type' => $type, 'source' => self::DESIGN_TEMPLATE_SOURCE, 'schemaVersion' => intval( Prefix_Migration::get_post_meta( $template->ID, self::DESIGN_SCHEMA_META, true ) ) ?: Layout_Schema::CURRENT_VERSION, 'created' => $template->post_date_gmt, 'modified' => $template->post_modified_gmt, 'status' => $template->post_status, ]; } public static function rest_list_design_templates( $request ) { $wanted_type = sanitize_key( $request->get_param( 'type' ) ?: '' ); $search = sanitize_text_field( $request->get_param( 'search' ) ?: '' ); $templates = get_posts( [ 'post_type' => self::TEMPLATES_POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => -1, 'orderby' => 'modified', 'order' => 'DESC', 's' => $search, ] ); $items = []; foreach ( $templates as $template ) { $type = self::design_type_from_record( $template->ID ); if ( ! $type || ( $wanted_type && $wanted_type !== $type ) ) continue; $items[] = self::format_design_template_metadata( $template ); } return rest_ensure_response( [ 'items' => $items ] ); } public static function rest_get_design_template( $request ) { $template = get_post( intval( $request['template_id'] ) ); $type = $template && self::TEMPLATES_POST_TYPE === $template->post_type ? self::design_type_from_record( $template->ID ) : ''; if ( ! $type ) return new \WP_Error( 'invalid_design_template', __( 'Saved Builder template not found.', 'nyxonn-builder' ), [ 'status' => 404 ] ); $response = self::format_design_template_metadata( $template ); $response['layout'] = self::get_design_payload( $template->ID, $type ); return rest_ensure_response( $response ); } public static function rest_create_design_template( $request ) { $params = $request->get_json_params(); $title = sanitize_text_field( $params['title'] ?? '' ); $type = sanitize_key( $params['type'] ?? '' ); $schema_version = intval( $params['schemaVersion'] ?? Layout_Schema::CURRENT_VERSION ); if ( '' === $title ) return new \WP_Error( 'missing_template_title', __( 'A template name is required.', 'nyxonn-builder' ), [ 'status' => 400 ] ); if ( Layout_Schema::CURRENT_VERSION !== $schema_version ) return new \WP_Error( 'incompatible_template_schema', __( 'This template schema version is not supported.', 'nyxonn-builder' ), [ 'status' => 400 ] ); $prepared = self::normalize_design_payload( $type, $params['layout'] ?? null ); if ( is_wp_error( $prepared ) ) return $prepared; $template_id = wp_insert_post( [ 'post_type' => self::TEMPLATES_POST_TYPE, 'post_title' => $title, 'post_status' => 'publish' ], true ); if ( is_wp_error( $template_id ) ) return $template_id; update_post_meta( $template_id, self::TEMPLATE_TYPE_META, self::storage_type_for_design_type( $type ) ); update_post_meta( $template_id, self::TEMPLATE_SOURCE_META, self::DESIGN_TEMPLATE_SOURCE ); update_post_meta( $template_id, self::DESIGN_SCHEMA_META, Layout_Schema::CURRENT_VERSION ); update_post_meta( $template_id, self::TEMPLATE_RULES_META, [] ); $payload_json = wp_json_encode( $prepared['payload'] ); update_post_meta( $template_id, self::DESIGN_PAYLOAD_META, wp_slash( $payload_json ) ); $stored = Save::store_canonical_layout( $template_id, $prepared['layout'] ); if ( is_wp_error( $stored ) ) return $stored; $response = self::format_design_template_metadata( get_post( $template_id ) ); $response['layout'] = $prepared['payload']; return rest_ensure_response( $response ); } public static function rest_update_design_template( $request ) { $template_id = intval( $request['template_id'] ); $template = get_post( $template_id ); $type = $template ? self::design_type_from_record( $template_id ) : ''; if ( ! $type ) return new \WP_Error( 'invalid_design_template', __( 'Saved Builder template not found.', 'nyxonn-builder' ), [ 'status' => 404 ] ); $params = $request->get_json_params(); $title = sanitize_text_field( $params['title'] ?? '' ); if ( '' === $title ) return new \WP_Error( 'missing_template_title', __( 'A template name is required.', 'nyxonn-builder' ), [ 'status' => 400 ] ); $updated = wp_update_post( [ 'ID' => $template_id, 'post_title' => $title ], true ); if ( is_wp_error( $updated ) ) return $updated; return rest_ensure_response( self::format_design_template_metadata( get_post( $template_id ) ) ); } public static function rest_delete_design_template( $request ) { $template_id = intval( $request['template_id'] ); if ( ! self::design_type_from_record( $template_id ) ) return new \WP_Error( 'invalid_design_template', __( 'Saved Builder template not found.', 'nyxonn-builder' ), [ 'status' => 404 ] ); $deleted = wp_delete_post( $template_id, true ); if ( ! $deleted ) return new \WP_Error( 'template_delete_failed', __( 'The template could not be deleted.', 'nyxonn-builder' ), [ 'status' => 500 ] ); return rest_ensure_response( [ 'success' => true, 'id' => $template_id ] ); } /** * REST: List all templates */ public static function rest_list_templates() { $args = [ 'post_type' => self::TEMPLATES_POST_TYPE, 'posts_per_page' => -1, 'orderby' => 'date', 'order' => 'DESC', 'meta_query' => [ [ 'key' => self::TEMPLATE_TYPE_META, 'value' => [ 'header', 'footer' ], 'compare' => 'IN', ], ], ]; $templates = get_posts( $args ); $result = []; foreach ( $templates as $template ) { $rules = self::get_template_rules( $template->ID ); $result[] = [ 'id' => $template->ID, 'title' => $template->post_title, 'type' => Prefix_Migration::get_post_meta( $template->ID, self::TEMPLATE_TYPE_META, true ), 'status' => $template->post_status, 'rules' => $rules, 'summary' => self::get_rules_summary( $rules ), 'conflicts' => 'publish' === $template->post_status ? self::get_conflicts( $template->ID ) : [], 'created' => $template->post_date, ]; } return rest_ensure_response( $result ); } /** Lightweight, paginated selector data; never returns private content bodies. */ public static function rest_search_theme_content( $request ) { $type = sanitize_key( $request->get_param( 'type' ) ?: 'any' ); $post_types = in_array( $type, [ 'page', 'post' ], true ) ? [ $type ] : [ 'page', 'post' ]; $page = max( 1, absint( $request->get_param( 'page' ) ) ); $query = new \WP_Query( [ 'post_type' => $post_types, 'post_status' => 'publish', 'posts_per_page' => 20, 'paged' => $page, 's' => sanitize_text_field( $request->get_param( 'search' ) ?: '' ), 'orderby' => 'title', 'order' => 'ASC', 'no_found_rows' => false, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ] ); $items = []; foreach ( $query->posts as $post ) { if ( current_user_can( 'read_post', $post->ID ) || current_user_can( 'edit_post', $post->ID ) ) { $items[] = [ 'id' => (int) $post->ID, 'title' => $post->post_title ?: __( '(no title)', 'nyxonn-builder' ), 'type' => $post->post_type ]; } } return rest_ensure_response( [ 'items' => $items, 'page' => $page, 'pages' => (int) $query->max_num_pages ] ); } private static function normalize_section_template_layout( $layout ) { $canonical = Save::canonicalize_layout( $layout ); if ( is_wp_error( $canonical ) ) { return []; } $layout = json_decode( $canonical, true ); $first = $layout[0] ?? null; if ( ! is_array( $first ) || ( $first['type'] ?? '' ) !== 'section' ) { return []; } return [ $first ]; } private static function format_section_template_response( $template ) { $layout = \NyxonnBuilder\Save::get_layout_for_post( $template->ID ); $layout = self::normalize_section_template_layout( $layout ); return [ 'id' => intval( $template->ID ), 'title' => $template->post_title, 'layout' => $layout, 'editUrl' => admin_url( 'admin.php?page=nyxonn-builder-editor&post_id=' . intval( $template->ID ) ), 'modified' => $template->post_modified_gmt, 'postStatus' => $template->post_status, ]; } private static function normalize_page_template_layout( $layout ) { $canonical = Save::canonicalize_layout( $layout ); if ( is_wp_error( $canonical ) ) { return []; } return json_decode( $canonical, true ); } private static function format_page_template_response( $template ) { $layout = \NyxonnBuilder\Save::get_layout_for_post( $template->ID ); $layout = self::normalize_page_template_layout( $layout ); return [ 'id' => intval( $template->ID ), 'title' => $template->post_title, 'layout' => $layout, 'editUrl' => admin_url( 'admin.php?page=nyxonn-builder-editor&post_id=' . intval( $template->ID ) ), 'modified' => $template->post_modified_gmt, 'postStatus' => $template->post_status, 'templateType' => self::PAGE_TEMPLATE_TYPE, ]; } private static function is_saved_page_template( $template_id ) { $template_id = intval( $template_id ); if ( ! $template_id ) { return false; } $type = Prefix_Migration::get_post_meta( $template_id, self::TEMPLATE_TYPE_META, true ); if ( self::PAGE_TEMPLATE_TYPE === $type ) { return true; } $source = Prefix_Migration::get_post_meta( $template_id, self::TEMPLATE_SOURCE_META, true ); if ( self::PAGE_TEMPLATE_SOURCE === $source ) { return true; } // Backward compatibility for the first implementation that stored // saved page layouts using the generic "page" template type. return 'page' === $type; } public static function rest_list_section_templates() { $templates = get_posts( [ 'post_type' => self::TEMPLATES_POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => -1, 'orderby' => 'modified', 'order' => 'DESC', 'meta_key' => self::TEMPLATE_TYPE_META, 'meta_value' => self::SECTION_TEMPLATE_TYPE, ] ); $result = []; foreach ( $templates as $template ) { $result[] = self::format_section_template_response( $template ); } return rest_ensure_response( $result ); } public static function rest_list_page_templates() { $templates = get_posts( [ 'post_type' => self::TEMPLATES_POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => -1, 'orderby' => 'modified', 'order' => 'DESC', ] ); $result = []; foreach ( $templates as $template ) { if ( ! self::is_saved_page_template( $template->ID ) ) { continue; } $result[] = self::format_page_template_response( $template ); } return rest_ensure_response( $result ); } public static function rest_create_section_template( $request ) { $params = $request->get_json_params(); $title = sanitize_text_field( $params['title'] ?? '' ); $layout = self::normalize_section_template_layout( $params['layout'] ?? [] ); if ( '' === $title ) { $title = __( 'Global Section', 'nyxonn-builder' ); } if ( empty( $layout ) ) { return new \WP_Error( 'invalid_layout', __( 'A valid section layout is required.', 'nyxonn-builder' ), [ 'status' => 400 ] ); } $template_id = wp_insert_post( [ 'post_type' => self::TEMPLATES_POST_TYPE, 'post_title' => $title, 'post_status' => 'publish', ] ); if ( is_wp_error( $template_id ) || ! $template_id ) { return new \WP_Error( 'create_failed', __( 'Unable to create section template.', 'nyxonn-builder' ), [ 'status' => 500 ] ); } update_post_meta( $template_id, self::TEMPLATE_TYPE_META, self::SECTION_TEMPLATE_TYPE ); update_post_meta( $template_id, self::TEMPLATE_RULES_META, [] ); $stored = \NyxonnBuilder\Save::store_canonical_layout( $template_id, $layout ); if ( is_wp_error( $stored ) ) return $stored; return rest_ensure_response( self::format_section_template_response( get_post( $template_id ) ) ); } public static function rest_update_section_template( $request ) { $template_id = intval( $request['template_id'] ); $template = get_post( $template_id ); if ( ! $template || self::TEMPLATES_POST_TYPE !== $template->post_type ) { return new \WP_Error( 'invalid_template', __( 'Invalid section template.', 'nyxonn-builder' ), [ 'status' => 404 ] ); } if ( self::SECTION_TEMPLATE_TYPE !== Prefix_Migration::get_post_meta( $template_id, self::TEMPLATE_TYPE_META, true ) ) { return new \WP_Error( 'invalid_type', __( 'This template is not a section template.', 'nyxonn-builder' ), [ 'status' => 400 ] ); } $params = $request->get_json_params(); $title = sanitize_text_field( $params['title'] ?? '' ); $layout = self::normalize_section_template_layout( $params['layout'] ?? [] ); if ( ! empty( $layout ) ) { $stored = \NyxonnBuilder\Save::store_canonical_layout( $template_id, $layout ); if ( is_wp_error( $stored ) ) return $stored; } if ( '' !== $title ) { wp_update_post( [ 'ID' => $template_id, 'post_title' => $title, ] ); } return rest_ensure_response( self::format_section_template_response( get_post( $template_id ) ) ); } public static function rest_create_page_template( $request ) { $params = $request->get_json_params(); $title = sanitize_text_field( $params['title'] ?? '' ); $layout = self::normalize_page_template_layout( $params['layout'] ?? [] ); if ( '' === $title ) { $title = __( 'Saved Page Layout', 'nyxonn-builder' ); } if ( empty( $layout ) ) { return new \WP_Error( 'invalid_layout', __( 'A valid page layout is required.', 'nyxonn-builder' ), [ 'status' => 400 ] ); } $template_id = wp_insert_post( [ 'post_type' => self::TEMPLATES_POST_TYPE, 'post_title' => $title, 'post_status' => 'publish', ] ); if ( is_wp_error( $template_id ) || ! $template_id ) { return new \WP_Error( 'create_failed', __( 'Unable to create page template.', 'nyxonn-builder' ), [ 'status' => 500 ] ); } update_post_meta( $template_id, self::TEMPLATE_TYPE_META, self::PAGE_TEMPLATE_TYPE ); update_post_meta( $template_id, self::TEMPLATE_SOURCE_META, self::PAGE_TEMPLATE_SOURCE ); update_post_meta( $template_id, self::TEMPLATE_RULES_META, [] ); $stored = \NyxonnBuilder\Save::store_canonical_layout( $template_id, $layout ); if ( is_wp_error( $stored ) ) return $stored; return rest_ensure_response( self::format_page_template_response( get_post( $template_id ) ) ); } public static function rest_update_page_template( $request ) { $template_id = intval( $request['template_id'] ); $template = get_post( $template_id ); if ( ! $template || self::TEMPLATES_POST_TYPE !== $template->post_type ) { return new \WP_Error( 'invalid_template', __( 'Invalid page template.', 'nyxonn-builder' ), [ 'status' => 404 ] ); } if ( ! self::is_saved_page_template( $template_id ) ) { return new \WP_Error( 'invalid_type', __( 'This template is not a saved page layout.', 'nyxonn-builder' ), [ 'status' => 400 ] ); } $params = $request->get_json_params(); $title = sanitize_text_field( $params['title'] ?? '' ); $layout = self::normalize_page_template_layout( $params['layout'] ?? [] ); if ( ! empty( $layout ) ) { update_post_meta( $template_id, self::TEMPLATE_TYPE_META, self::PAGE_TEMPLATE_TYPE ); update_post_meta( $template_id, self::TEMPLATE_SOURCE_META, self::PAGE_TEMPLATE_SOURCE ); $stored = \NyxonnBuilder\Save::store_canonical_layout( $template_id, $layout ); if ( is_wp_error( $stored ) ) return $stored; } if ( '' !== $title ) { wp_update_post( [ 'ID' => $template_id, 'post_title' => $title, ] ); } return rest_ensure_response( self::format_page_template_response( get_post( $template_id ) ) ); } public static function get_matching_template( $type, $context = [] ) { if ( ! in_array( $type, [ 'header', 'footer' ], true ) ) { return null; } $cache_key = sanitize_key( $type ) . ':' . md5( wp_json_encode( $context ) ); if ( array_key_exists( $cache_key, self::$request_template_matches ) ) { return self::$request_template_matches[ $cache_key ]; } $args = [ 'post_type' => self::TEMPLATES_POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => -1, 'orderby' => 'ID', 'order' => 'ASC', 'meta_key' => self::TEMPLATE_TYPE_META, 'meta_value' => $type, ]; $templates = get_posts( $args ); if ( empty( $templates ) ) { self::$request_template_matches[ $cache_key ] = null; return null; } $matches = []; foreach ( $templates as $template ) { $rules = self::get_template_rules( $template->ID ); $evaluation = Theme_Conditions::evaluate( $rules, $context ); if ( empty( $evaluation['matched'] ) ) continue; $matches[] = [ 'template' => $template, 'specificity'=> (int) $evaluation['specificity'], 'priority' => (int) ( $rules['priority'] ?? 0 ), ]; } usort( $matches, static function ( $left, $right ) { foreach ( [ 'specificity', 'priority' ] as $key ) { if ( $left[ $key ] !== $right[ $key ] ) return $right[ $key ] <=> $left[ $key ]; } $modified = strcmp( $right['template']->post_modified_gmt, $left['template']->post_modified_gmt ); return 0 !== $modified ? $modified : (int) $right['template']->ID <=> (int) $left['template']->ID; } ); self::$request_template_matches[ $cache_key ] = $matches ? $matches[0]['template'] : null; return self::$request_template_matches[ $cache_key ]; } /** Return and request-cache the matched template and its render layout. */ public static function get_matching_template_layout( $type, $context = [] ) { $cache_key = sanitize_key( $type ) . ':' . md5( wp_json_encode( $context ) ); if ( array_key_exists( $cache_key, self::$request_template_layouts ) ) { return self::$request_template_layouts[ $cache_key ]; } $template = self::get_matching_template( $type, $context ); if ( ! $template ) { self::$request_template_layouts[ $cache_key ] = null; return null; } self::$request_template_layouts[ $cache_key ] = [ 'template' => $template, 'layout' => \NyxonnBuilder\Save::get_layout_for_post( $template->ID ), ]; return self::$request_template_layouts[ $cache_key ]; } /** * Render header in wp_head */ public static function inject_header() { if ( is_admin() || self::$did_render_header ) { return; } self::$did_render_header = true; $match = self::get_matching_template_layout( 'header', self::get_request_context() ); if ( self::has_renderable_template_layout( $match ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Render::render_layout() returns fully escaped markup including required style tags. echo Render::render_layout( $match['layout'], '', 'header' ); } } /** * Render footer in wp_footer */ public static function inject_footer() { if ( is_admin() || self::$did_render_footer ) { return; } self::$did_render_footer = true; $match = self::get_matching_template_layout( 'footer', self::get_request_context() ); if ( self::has_renderable_template_layout( $match ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Render::render_layout() returns fully escaped markup including required style tags. echo Render::render_layout( $match['layout'], '', 'footer' ); } } /** Let block themes keep their native part unless a renderable Nyxonn area applies. */ public static function suppress_matching_theme_part( $pre_render, $parsed_block ) { if ( null !== $pre_render || is_admin() || ! function_exists( 'wp_is_block_theme' ) || ! wp_is_block_theme() || 'core/template-part' !== ( $parsed_block['blockName'] ?? '' ) ) { return $pre_render; } $slug = sanitize_key( $parsed_block['attrs']['slug'] ?? '' ); if ( ! in_array( $slug, [ 'header', 'footer' ], true ) ) { return $pre_render; } $match = self::get_matching_template_layout( $slug, self::get_request_context() ); return self::has_renderable_template_layout( $match ) ? '' : $pre_render; } private static function has_renderable_template_layout( $match ) { if ( ! is_array( $match ) || empty( $match['template'] ) ) { return false; } $layout = $match['layout'] ?? null; if ( is_string( $layout ) ) { $layout = json_decode( $layout, true ); } return is_array( $layout ) && ! empty( $layout ); } public static function add_meta_boxes() { add_meta_box( 'nyxonn_builder_tpl_settings', __( 'Template Settings', 'nyxonn-builder' ), [ __CLASS__, 'render_meta_box' ], self::TEMPLATES_POST_TYPE, 'side', 'high' ); } public static function render_meta_box( $post ) { $type = Prefix_Migration::get_post_meta( $post->ID, self::TEMPLATE_TYPE_META, true ) ?: 'header'; $rules = self::normalize_rules( Prefix_Migration::get_post_meta( $post->ID, self::TEMPLATE_RULES_META, true ) ); wp_nonce_field( 'nyxonn_builder_tpl_meta', 'nyxonn_builder_tpl_meta_nonce' ); self::render_rules_fields( $type, $rules, $post->ID ); } public static function render_rules_fields( $type, $rules, $template_id = 0, $args = [] ) { $args = wp_parse_args( is_array( $args ) ? $args : [], [ 'show_type_selector' => true ] ); $rules = self::normalize_rules( $rules ); if ( apply_filters( 'nyxonn_fx_render_advanced_theme_condition_fields', false, $type, $rules, $template_id, $args ) ) { return; } $is_global = false; foreach ( $rules['conditions'] as $condition ) { if ( 'include' === ( $condition['mode'] ?? '' ) && 'entire_site' === ( $condition['target'] ?? '' ) ) { $is_global = true; break; } } ?>

self::TEMPLATES_POST_TYPE, 'posts_per_page' => -1, 'meta_key' => self::TEMPLATE_TYPE_META, 'meta_value' => $type, ] ); foreach ( $templates as $template ) { if ( $exclude_template_id && intval( $template->ID ) === $exclude_template_id ) { continue; } $template_rules = self::get_template_rules( $template->ID ); if ( ! empty( $template_rules['all'] ) ) { return true; } } return false; } public static function save_meta_boxes( $post_id ) { if ( ! isset( $_POST['nyxonn_builder_tpl_meta_nonce'] ) ) { return; } if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nyxonn_builder_tpl_meta_nonce'] ) ), 'nyxonn_builder_tpl_meta' ) ) { return; } if ( ! current_user_can( 'edit_post', $post_id ) ) { return; } if ( get_post_type( $post_id ) !== self::TEMPLATES_POST_TYPE ) { return; } $type = isset( $_POST['nyxonn_builder_tpl_type'] ) ? sanitize_key( wp_unslash( $_POST['nyxonn_builder_tpl_type'] ) ) : 'header'; $rules = isset( $_POST['nx_conditions'] ) && is_array( $_POST['nx_conditions'] ) ? [ 'version' => 2, 'priority' => intval( wp_unslash( $_POST['nyxonn_builder_tpl_priority'] ?? 0 ) ), 'conditions' => wp_unslash( $_POST['nx_conditions'] ) ] : Prefix_Migration::get_post_meta( $post_id, self::TEMPLATE_RULES_META, true ); self::update_template_rules( $post_id, $type, $rules ); } public static function normalize_rules( $rules ) { return Theme_Conditions::normalize( $rules ); } public static function sanitize_rules( $rules ) { return Theme_Conditions::normalize( $rules ); } public static function get_rules_summary( $rules ) { $rules = self::normalize_rules( $rules ); $registry = Theme_Conditions::registry(); $include = []; $exclude = []; foreach ( $rules['conditions'] as $condition ) { $definition = $registry[ $condition['target'] ] ?? null; $label = $definition ? $definition['label'] : sprintf( /* translators: %s: unavailable condition target. */ __( 'Unavailable: %s', 'nyxonn-builder' ), $condition['target'] ); if ( ! empty( $condition['value'] ) ) { if ( $definition && 'term' === ( $definition['value_type'] ?? '' ) ) { $term = get_term( absint( $condition['value'] ), 'category' ); $label .= ': ' . ( $term && ! is_wp_error( $term ) ? $term->name : '#' . absint( $condition['value'] ) ); } else { $post = get_post( absint( $condition['value'] ) ); $label .= ': ' . ( $post ? $post->post_title : '#' . absint( $condition['value'] ) ); } } if ( 'exclude' === $condition['mode'] ) $exclude[] = $label; else $include[] = $label; } if ( ! $include ) return __( 'Unassigned', 'nyxonn-builder' ); $summary = implode( ', ', $include ); if ( $exclude ) $summary .= ' ' . sprintf( /* translators: %s: excluded template conditions. */ __( 'except %s', 'nyxonn-builder' ), implode( ', ', $exclude ) ); return $summary; } public static function get_request_context() { if ( null !== self::$request_context ) { return self::$request_context; } $object_id = get_queried_object_id(); $queried = get_queried_object(); $post_type = $object_id ? get_post_type( $object_id ) : ''; $term_id = 0; $category_ids = []; if ( is_singular() && $object_id ) { if ( 'post' === $post_type ) { $category_ids = wp_get_post_categories( $object_id, [ 'fields' => 'ids' ] ); } } if ( is_category() && $queried instanceof \WP_Term ) { $term_id = intval( $queried->term_id ); } self::$request_context = [ 'object_id' => intval( $object_id ), 'post_type' => sanitize_key( $post_type ), 'category_id' => intval( $term_id ), 'category_ids' => array_map( 'intval', (array) $category_ids ), 'is_404' => is_404(), 'is_archive' => is_archive(), 'is_category' => is_category(), 'is_singular' => is_singular(), 'is_page' => is_page(), 'is_single' => is_single(), 'is_front_page'=> is_front_page(), 'is_home' => is_home(), 'is_posts_page'=> is_home() && $object_id && absint( get_option( 'page_for_posts' ) ) === absint( $object_id ), ]; return self::$request_context; } private static function context_is_excluded( $rules, $context ) { $object_id = intval( $context['object_id'] ?? 0 ); return $object_id && in_array( $object_id, $rules['exclude'], true ); } private static function context_matches_specific_rule( $rules, $context ) { $object_id = intval( $context['object_id'] ?? 0 ); if ( $object_id && in_array( $object_id, $rules['include'], true ) ) { return true; } $post_type = sanitize_key( $context['post_type'] ?? '' ); if ( $post_type && in_array( $post_type, $rules['post_types'], true ) ) { return true; } $contexts = array_map( 'sanitize_key', (array) $rules['contexts'] ); if ( in_array( 'single', $contexts, true ) && ! empty( $context['is_single'] ) ) { return true; } if ( in_array( 'page', $contexts, true ) && ! empty( $context['is_page'] ) ) { return true; } if ( in_array( 'category', $contexts, true ) && ! empty( $context['is_category'] ) ) { return true; } if ( in_array( 'archive', $contexts, true ) && ! empty( $context['is_archive'] ) ) { return true; } if ( in_array( '404', $contexts, true ) && ! empty( $context['is_404'] ) ) { return true; } $category_id = intval( $context['category_id'] ?? 0 ); if ( $category_id && in_array( $category_id, $rules['categories'], true ) ) { return true; } $post_categories = array_map( 'intval', (array) ( $context['category_ids'] ?? [] ) ); if ( $post_categories && array_intersect( $post_categories, $rules['categories'] ) ) { return true; } return false; } public static function get_context_for_post( $post_id ) { $post_id = intval( $post_id ); $post_type = $post_id ? get_post_type( $post_id ) : ''; $category_ids = []; if ( $post_id && 'post' === $post_type ) { $category_ids = wp_get_post_categories( $post_id, [ 'fields' => 'ids' ] ); } return [ 'object_id' => $post_id, 'post_type' => sanitize_key( $post_type ), 'category_id' => 0, 'category_ids' => array_map( 'intval', (array) $category_ids ), 'is_404' => false, 'is_archive' => false, 'is_category' => false, 'is_singular' => $post_id > 0, 'is_page' => 'page' === $post_type, 'is_single' => 'post' === $post_type, 'is_front_page'=> absint( get_option( 'page_on_front' ) ) === $post_id && 'page' === $post_type, 'is_home' => absint( get_option( 'page_for_posts' ) ) === $post_id && 'page' === $post_type, 'is_posts_page'=> absint( get_option( 'page_for_posts' ) ) === $post_id && 'page' === $post_type, ]; } public static function filter_dynamic_preview_post_id( $context_id, $editing_post_id ) { $editing = get_post( absint( $editing_post_id ) ); if ( ! $editing || self::TEMPLATES_POST_TYPE !== $editing->post_type || ! in_array( self::get_template_type( $editing->ID ), [ 'header', 'footer' ], true ) ) return $context_id; $requested = isset( $_GET['nx_preview_post_id'] ) ? absint( wp_unslash( $_GET['nx_preview_post_id'] ) ) : 0; if ( $requested && ( current_user_can( 'read_post', $requested ) || current_user_can( 'edit_post', $requested ) ) ) { $post = get_post( $requested ); if ( $post && in_array( $post->post_type, [ 'page', 'post' ], true ) ) return $requested; } return $context_id; } public static function get_conflicts( $template_id ) { $template_id = absint( $template_id ); $type = self::get_template_type( $template_id ); if ( ! in_array( $type, [ 'header', 'footer' ], true ) ) return []; if ( ! isset( self::$request_conflicts[ $type ] ) ) { $assignments = []; $map = []; $templates = get_posts( [ 'post_type' => self::TEMPLATES_POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => -1, 'meta_key' => self::TEMPLATE_TYPE_META, 'meta_value' => $type ] ); foreach ( $templates as $template ) { $rules = self::get_template_rules( $template->ID ); foreach ( $rules['conditions'] as $condition ) { if ( 'include' !== $condition['mode'] ) continue; $key = (int) $rules['priority'] . ':' . $condition['target'] . ':' . ( $condition['value'] ?? '' ); $assignments[ $key ][] = (int) $template->ID; } } foreach ( $assignments as $ids ) { if ( count( $ids ) < 2 ) continue; foreach ( $ids as $id ) $map[ $id ] = array_values( array_unique( array_merge( $map[ $id ] ?? [], array_diff( $ids, [ $id ] ) ) ) ); } self::$request_conflicts[ $type ] = $map; } return self::$request_conflicts[ $type ][ $template_id ] ?? []; } }