0 ? get_post( $post_id ) : null; return $post instanceof \WP_Post && class_exists( Role_Manager::class ) && Role_Manager::can_use_builder( $post_id ) && class_exists( Builder_Settings::class ) && Builder_Settings::can_open_builder( $post ); } public static function normalize_legacy_layout_json( $json ) { if ( ! is_string( $json ) || '' === $json ) { return ''; } return str_replace( [ 'f' . 'x-', 'data-' . 'f' . 'x-', 'f' . 'lexora-', 'f' . 'lexero-', 'f' . 'lexora_', 'f' . 'lexero_', 'f' . 'lexora', 'f' . 'lexero', ], [ 'nx-', 'data-nx-', 'nx-', 'nx-', 'nx_', 'nx_', 'nx', 'nx', ], $json ); } /** * AJAX handler (admin-ajax.php) for backwards compatibility. */ public static function ajax_save() { check_ajax_referer( 'nyxonn_builder_save_nonce', 'nonce' ); $post_id = isset( $_POST['post_id'] ) ? intval( wp_unslash( $_POST['post_id'] ) ) : 0; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw layout JSON is intentionally preserved before key-based sanitization. $data = isset( $_POST['data'] ) ? wp_unslash( $_POST['data'] ) : ''; if ( ! self::can_access_layout( $post_id ) ) { wp_send_json_error( 'Permission denied' ); } $stored = self::store_canonical_layout( $post_id, is_string( $data ) ? $data : '' ); if ( is_wp_error( $stored ) ) { wp_send_json_error( $stored->get_error_message(), 400 ); } self::clear_post_content( $post_id ); $request_id = isset( $_POST['request_id'] ) ? sanitize_text_field( wp_unslash( $_POST['request_id'] ) ) : ''; wp_send_json_success( [ 'message' => 'Saved', 'requestId' => $request_id, 'schemaVersion' => Layout_Schema::CURRENT_VERSION, ] ); } /** * REST handler for `/nyxonn-builder/v1/save`. * * @param \WP_REST_Request $request * @return \WP_REST_Response|\WP_Error */ public static function rest_save( \WP_REST_Request $request ) { $post_id = intval( $request->get_param( 'post_id' ) ); // REST requests already provide the raw JSON string. Running wp_unslash() // here corrupts escaped quotes inside rich text HTML such as links. $layout = $request->get_param( 'layout' ); if ( ! self::can_access_layout( $post_id ) ) { return new \WP_Error( 'forbidden', 'You cannot edit this post', [ 'status' => 403 ] ); } $stored = self::store_canonical_layout( $post_id, is_string( $layout ) ? $layout : '' ); if ( is_wp_error( $stored ) ) { $stored->add_data( [ 'status' => 400 ] ); return $stored; } self::clear_post_content( $post_id ); return rest_ensure_response( [ 'success' => true, 'requestId' => sanitize_text_field( (string) $request->get_param( 'request_id' ) ), 'schemaVersion' => Layout_Schema::CURRENT_VERSION, ] ); } /** * REST handler for `/nyxonn-builder/v1/layout`. * * @param \WP_REST_Request $request * @return \WP_REST_Response|\WP_Error */ public static function rest_get_layout( \WP_REST_Request $request ) { $post_id = intval( $request->get_param( 'post_id' ) ); if ( ! self::can_access_layout( $post_id ) ) { return new \WP_Error( 'forbidden', 'You cannot view this layout', [ 'status' => 403 ] ); } $layout = self::get_layout_for_post( $post_id ); return rest_ensure_response( [ 'success' => true, 'layout' => is_string( $layout ) ? $layout : '', 'schemaVersion' => Layout_Schema::CURRENT_VERSION, ] ); } public static function clear_on_save( $post_id, $post ) { if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; } if ( ! current_user_can( 'edit_post', $post_id ) ) { return; } if ( ! self::is_enabled( $post_id ) ) { return; } self::clear_post_content( $post_id ); } private static function clear_post_content( $post_id ) { $post = get_post( $post_id ); if ( ! $post ) { return; } if ( $post->post_content === '' && $post->post_content_filtered === '' ) { return; } remove_action( 'save_post', [ __CLASS__, 'clear_on_save' ], 20 ); wp_update_post( [ 'ID' => $post_id, 'post_content' => '', 'post_content_filtered'=> '', ] ); add_action( 'save_post', [ __CLASS__, 'clear_on_save' ], 20, 2 ); } /** Return validated, recursively sanitized, canonical JSON. */ public static function canonicalize_layout( $layout ) { $json = is_string( $layout ) ? $layout : wp_json_encode( $layout ); if ( ! is_string( $json ) ) { return new \WP_Error( 'invalid_layout_json', __( 'Layout JSON could not be encoded.', 'nyxonn-builder' ) ); } $decoded = Layout_Schema::decode( self::normalize_legacy_layout_json( $json ) ); if ( is_wp_error( $decoded ) ) { return $decoded; } $sanitized = self::deep_sanitize( $decoded ); $canonical = wp_json_encode( $sanitized ); if ( ! is_string( $canonical ) ) { return new \WP_Error( 'invalid_layout_json', __( 'Layout JSON could not be encoded.', 'nyxonn-builder' ) ); } $validated = Layout_Schema::decode( $canonical ); return is_wp_error( $validated ) ? $validated : $canonical; } /** Store one canonical value in both active and compatibility meta. */ public static function store_canonical_layout( $post_id, $layout ) { $canonical = self::canonicalize_layout( $layout ); if ( is_wp_error( $canonical ) ) { return $canonical; } update_post_meta( $post_id, self::LAYOUT_META, wp_slash( $canonical ) ); update_post_meta( $post_id, self::RAW_LAYOUT_META, wp_slash( $canonical ) ); update_post_meta( $post_id, self::ENABLED_META, '1' ); update_post_meta( $post_id, Layout_Schema::VERSION_META, Layout_Schema::CURRENT_VERSION ); self::clear_layout_cache( $post_id ); return $canonical; } /** Backward-compatible wrapper for callers that expect an empty string on failure. */ public static function sanitize_layout( $json ) { $canonical = self::canonicalize_layout( $json ); return is_wp_error( $canonical ) ? '' : $canonical; } public static function validate_layout_json( $json ) { return Layout_Schema::decode( self::normalize_legacy_layout_json( $json ) ); } public static function is_valid_layout_json( $json ) { return ! is_wp_error( self::validate_layout_json( $json ) ); } private static function deep_sanitize( $data, $key = '' ) { if ( '_dynamic' === $key ) { return class_exists( '\NyxonnBuilder\Dynamic_Content' ) ? Dynamic_Content::sanitize_map( $data ) : []; } if ( is_array( $data ) ) { $sanitized = []; foreach ( $data as $child_key => $child_value ) { $sanitized[ $child_key ] = self::deep_sanitize( $child_value, is_string( $child_key ) ? $child_key : '' ); } return $sanitized; } if ( is_string( $data ) ) { $html_keys = [ 'content' ]; // Pricing features are stored as one feature per line and split by the renderer. $textarea_keys = [ 'customCss', 'customCssSelector', 'pricingFeatures' ]; $url_keys = [ 'url', 'headingUrl', 'imageLink', 'iconBoxLink', 'bgImage', 'overlayImage', 'iconBoxImage', 'testimonialImage', 'videoUrl', 'videoFile', 'videoPoster', 'bgVideoUrl', 'bgVideoFile' ]; if ( in_array( $key, $html_keys, true ) ) { return wp_kses_post( $data ); } if ( in_array( $key, $textarea_keys, true ) ) { return sanitize_textarea_field( $data ); } if ( in_array( $key, $url_keys, true ) ) { return esc_url_raw( $data ); } return sanitize_text_field( $data ); } return $data; } }