<?php namespace WprAddons\Modules\WidgetBuilder\Api; if ( ! defined( 'ABSPATH' ) ) { exit; } class Endpoints { private $namespace = 'wpr-addons/v1'; private $base = 'widget-builder'; public function __construct() { add_action( 'rest_api_init', [ $this, 'register_routes' ] ); } public function register_routes() { register_rest_route( $this->namespace, '/' . $this->base . '/save/(?P<id>[\d]+)', [ 'methods' => 'POST', 'callback' => [ $this, 'save_widget' ], 'permission_callback' => [ $this, 'check_permissions' ], 'args' => [ 'id' => [ 'validate_callback' => function( $param ) { return is_numeric( $param ); }, ], ], ]); register_rest_route( $this->namespace, '/' . $this->base . '/save', [ 'methods' => 'POST', 'callback' => [ $this, 'save_widget' ], 'permission_callback' => [ $this, 'check_permissions' ], ]); register_rest_route( $this->namespace, '/' . $this->base . '/load/(?P<id>[\d]+)', [ 'methods' => 'GET', 'callback' => [ $this, 'load_widget' ], 'permission_callback' => [ $this, 'check_permissions' ], 'args' => [ 'id' => [ 'validate_callback' => function( $param ) { return is_numeric( $param ); }, ], ], ]); register_rest_route( $this->namespace, '/' . $this->base . '/delete/(?P<id>[\d]+)', [ 'methods' => 'DELETE', 'callback' => [ $this, 'delete_widget' ], 'permission_callback' => [ $this, 'check_permissions' ], 'args' => [ 'id' => [ 'validate_callback' => function( $param ) { return is_numeric( $param ); }, ], ], ]); } public function check_permissions() { return current_user_can( 'manage_options' ); } public function save_widget( $request ) { $id = $request->get_param( 'id' ); $body = $request->get_json_params(); if ( empty( $body ) ) { return new \WP_REST_Response([ 'success' => false, 'message' => esc_html__( 'Invalid data.', 'wpr-addons' ), ], 400 ); } $title = ! empty( $body['title'] ) ? sanitize_text_field( $body['title'] ) : 'Custom Widget #' . time(); $post_data = [ 'post_title' => $title, 'post_status' => 'publish', 'post_type' => 'wpr_custom_widget', ]; // Update existing or create new $existing = $id ? get_post( $id ) : null; if ( $existing && $existing->post_type === 'wpr_custom_widget' ) { $post_data['ID'] = $id; wp_update_post( $post_data ); } else { $id = wp_insert_post( $post_data ); if ( is_wp_error( $id ) ) { return new \WP_REST_Response([ 'success' => false, 'message' => $id->get_error_message(), ], 500 ); } } // Build the widget data to store $category = ! empty( $body['category'] ) ? sanitize_text_field( $body['category'] ) : 'wpr-widgets'; $widget_data = [ 'title' => $title, 'icon' => ! empty( $body['icon'] ) ? sanitize_text_field( $body['icon'] ) : 'eicon-cog', 'categories' => [ $category ], 'push_id' => $id, 'markup' => isset( $body['markup'] ) ? str_replace( [ '<?php', '<?=', '<?', '?>' ], '', $body['markup'] ) : '', 'css' => isset( $body['css'] ) ? wp_strip_all_tags( $body['css'] ) : '', 'js' => isset( $body['js'] ) ? str_replace( '</script>', '', $body['js'] ) : '', 'css_includes' => ! empty( $body['css_includes'] ) ? array_map( 'esc_url_raw', (array) $body['css_includes'] ) : [], 'js_includes' => ! empty( $body['js_includes'] ) ? array_map( 'esc_url_raw', (array) $body['js_includes'] ) : [], 'tabs' => isset( $body['tabs'] ) ? $body['tabs'] : [ 'content' => [], 'style' => [], 'advanced' => [], ], ]; // Sanitize control tabs data $widget_data['tabs'] = $this->sanitize_tabs( $widget_data['tabs'] ); update_post_meta( $id, 'wpr_custom_widget_data', $widget_data ); // Generate widget files (widget.php, style.css, script.js) $writer = new \WprAddons\Modules\WidgetBuilder\Controls\WidgetWriter( $widget_data, $id ); $writer->generate(); return new \WP_REST_Response([ 'success' => true, 'message' => esc_html__( 'Widget saved successfully!', 'wpr-addons' ), 'post_id' => $id, ], 200 ); } public function load_widget( $request ) { $id = intval( $request->get_param( 'id' ) ); $post = get_post( $id ); if ( ! $post || $post->post_type !== 'wpr_custom_widget' ) { return new \WP_REST_Response([ 'success' => false, 'message' => esc_html__( 'Widget not found.', 'wpr-addons' ), ], 404 ); } $widget_data = get_post_meta( $id, 'wpr_custom_widget_data', true ); if ( empty( $widget_data ) ) { $widget_data = [ 'title' => $post->post_title ?: 'New Widget', 'icon' => 'eicon-cog', 'categories' => [ 'wpr-widgets' ], 'push_id' => $id, 'markup' => '', 'css' => '', 'js' => '', 'css_includes' => [], 'js_includes' => [], 'tabs' => [ 'content' => [], 'style' => [], 'advanced' => [], ], ]; } return new \WP_REST_Response([ 'success' => true, 'data' => $widget_data, ], 200 ); } /** * Sanitize the controls tabs data to prevent code injection in generated PHP. * * New format: tabs.content = [ { key, label, controls: [ {type, key, label, ...} ] } ] */ private function sanitize_tabs( $tabs ) { if ( ! is_array( $tabs ) ) { return [ 'content' => [], 'style' => [], 'advanced' => [] ]; } $clean_tabs = []; foreach ( [ 'content', 'style', 'advanced' ] as $tab_key ) { $sections = isset( $tabs[ $tab_key ] ) ? (array) $tabs[ $tab_key ] : []; $clean_sections = []; foreach ( $sections as $section ) { $section = (array) $section; $clean_section = [ 'key' => isset( $section['key'] ) ? sanitize_key( $section['key'] ) : '', 'label' => isset( $section['label'] ) ? sanitize_text_field( $section['label'] ) : 'Section', 'description' => isset( $section['description'] ) ? sanitize_text_field( $section['description'] ) : '', 'controls' => [], ]; if ( empty( $clean_section['key'] ) ) { continue; } $controls = isset( $section['controls'] ) ? (array) $section['controls'] : []; foreach ( $controls as $control ) { $clean_ctrl = $this->sanitize_control( $control ); if ( ! empty( $clean_ctrl['key'] ) ) { $clean_section['controls'][] = $clean_ctrl; } } $clean_sections[] = $clean_section; } $clean_tabs[ $tab_key ] = $clean_sections; } return $clean_tabs; } /** * Sanitize a single control's data. */ private function sanitize_control( $control ) { $control = (array) $control; $clean = [ 'key' => isset( $control['key'] ) ? sanitize_key( $control['key'] ) : '', 'label' => isset( $control['label'] ) ? sanitize_text_field( $control['label'] ) : '', 'type' => isset( $control['type'] ) ? sanitize_text_field( $control['type'] ) : 'text', 'default' => isset( $control['default'] ) ? sanitize_text_field( $control['default'] ) : '', ]; // Selector (group controls) if ( ! empty( $control['selector'] ) ) { $clean['selector'] = sanitize_text_field( $control['selector'] ); } // Separator if ( ! empty( $control['separator'] ) && in_array( $control['separator'], [ 'before', 'after' ], true ) ) { $clean['separator'] = $control['separator']; } // Options (select, select2, choose) if ( ! empty( $control['options'] ) && is_array( $control['options'] ) ) { $clean_options = []; foreach ( $control['options'] as $opt_key => $opt_value ) { $safe_key = sanitize_key( $opt_key ); if ( is_array( $opt_value ) || is_object( $opt_value ) ) { $opt_value = (array) $opt_value; $clean_options[ $safe_key ] = [ 'title' => isset( $opt_value['title'] ) ? sanitize_text_field( $opt_value['title'] ) : $safe_key, 'icon' => isset( $opt_value['icon'] ) ? sanitize_text_field( $opt_value['icon'] ) : '', ]; } else { $clean_options[ $safe_key ] = sanitize_text_field( (string) $opt_value ); } } $clean['options'] = $clean_options; } // Condition if ( ! empty( $control['condition'] ) && is_array( $control['condition'] ) ) { $cond = $control['condition']; if ( ! empty( $cond['key'] ) ) { $clean['condition'] = [ 'key' => sanitize_key( $cond['key'] ), 'value' => isset( $cond['value'] ) ? sanitize_text_field( $cond['value'] ) : '', ]; } } // Slider min/max if ( isset( $control['slider_min'] ) && $control['slider_min'] !== '' ) { $clean['slider_min'] = floatval( $control['slider_min'] ); } if ( isset( $control['slider_max'] ) && $control['slider_max'] !== '' ) { $clean['slider_max'] = floatval( $control['slider_max'] ); } if ( isset( $control['slider_step'] ) && $control['slider_step'] !== '' ) { $clean['slider_step'] = floatval( $control['slider_step'] ); } // No units (slider) if ( ! empty( $control['no_units'] ) ) { $clean['no_units'] = true; } // Allowed dimensions if ( ! empty( $control['allowed_dimensions'] ) && in_array( $control['allowed_dimensions'], [ 'vertical', 'horizontal' ], true ) ) { $clean['allowed_dimensions'] = $control['allowed_dimensions']; } // Code language if ( ! empty( $control['language'] ) ) { $allowed_langs = [ 'html', 'css', 'sass', 'scss', 'javascript', 'json', 'less', 'markdown', 'php', 'python', 'mysql', 'sql', 'svg', 'text', 'twig', 'typescript' ]; $lang = sanitize_text_field( $control['language'] ); if ( in_array( $lang, $allowed_langs, true ) ) { $clean['language'] = $lang; } } // Selectors (CSS mapping) if ( ! empty( $control['selectors'] ) && is_array( $control['selectors'] ) ) { $clean_selectors = []; foreach ( $control['selectors'] as $sel_key => $sel_value ) { $clean_selectors[ sanitize_text_field( $sel_key ) ] = sanitize_text_field( (string) $sel_value ); } $clean['selectors'] = $clean_selectors; } return $clean; } public function delete_widget( $request ) { $id = intval( $request->get_param( 'id' ) ); $post = get_post( $id ); if ( ! $post || $post->post_type !== 'wpr_custom_widget' ) { return new \WP_REST_Response([ 'success' => false, 'message' => esc_html__( 'Widget not found.', 'wpr-addons' ), ], 404 ); } wp_delete_post( $id, true ); return new \WP_REST_Response([ 'success' => true, 'message' => esc_html__( 'Widget deleted successfully!', 'wpr-addons' ), ], 200 ); } }