JFIF $ d d 7 * Z qn )ZP 0 !1 A "#a$2Qr D8 a Ri [f \m I ykI w0 c u F c Rı?lO7 к _f˓ [C $殷WF< _W ԣ s Kc ëI zyQy /_LK ℂ ;C",p F A : /] =H ~ , l s / 9 ć: [=/#f ; ) x { ٛE Q )~ = 𘙲r*2~ a _V = ' ku m F D } KYYC )( { * g& f `툪 r y ` = ^ c J. I ]( * `wq 1dđ #̩ ͑ 0 ; H ] u 搂 @:~ ו K L N s h } O I R* 8 :2 ! lD JV o ( 3 = M ( z Ȱ+i *N A r6K n S l ) ! J J Ӂ* % ݉?|D } d5: eP0 R; {$X' x F @ .ÊB {,WJ uQ ɲRI ;9 Q E琯 6 2fT .DU J ;*c P A\IL Nj! J ۱+ O\ ͔ ] ޒ S J ȧ c%A N ol ՎprU L Zԛe r E2= X D Xg VQeӓ k y P7 U* o m Q I s ,K `) 6\G3t?pgjr m ۛجw luGt f h 9uyP 0D;Uڽ"OXl i f $ ) &|ML 0 Z rm1 [ H XP l P R0 'G=i 2N +0 e 2 ]] 9 VT P O 7 h(F * 癈 ' = Q VZD F,d ߬ ~ T X G[ `le 69C R(!S2! P < 0 x < !1AQ "Raq 02Br #S CTb ? Ζ "]mH5WR 7 k.ۛ ! } Q~ + yԏ z |@ T20 S~ K e k *zF f^2X *( @ 8 r ? C I uI|֓>^Ex LgNU Y+{.R Ѫ τV YT D I 6 2 '8Y2 7'\T P.6 d &˦ @V q i |8 -O Ε ] ʔ U= T L 8= ;6c | !qfF3 a ů& ~$ l} 'NW U s$U k^S V : U#6w + +s &r+n ڐ { @ 29gL u " T Ù M=6 (^ "7 r}=6Yݾ lC u hquymp Ǧ G jhs ǜNl ɻ} o 7 #S6 a w 4 ! OS r D 5 7 % | ?x> L | / nD 6?/8 w # [ ) L 7 +6〼T A T g ! %5 Mm Z /c -{1_Je "| ^$ 'O & ޱմ < H vNqϡQ$ < 6 &Nݛ ֨tǻN (9 n ru~'όN sMG& ,uH Kw *Ī 5@ u6 BԒ i YN * f 0 _ GhKW u lT <nj- 0{ M L~ | /a v\ E = ?1 \ɟuq Ђ Y O a K ޡP q $ H8o Q˼EV ~ k 0 h' L Pp M -M" m^ !6l nմ 쮝C U T . ! :vMǨb h ډ Vc r @~p # !1QAa q ? AL0L x LU 4 d ; x " 16ad0 7.\ߋ 0 = P l v/ " c A Ay A|2pO8"d MV #1] = \ " ˡ ] k v I9 d ۓ & q ) ǐq E ꞅ v "P )q X &Q З non]я u ג./ ; gZ$ M QР 2M ]h ni 0 P $ێ 3CɏGjώy : m c # S_ l@ '_ \m|C 2`K _ 1 cIvGc $? PCQ S+D ? K aײ k Fr b 8 ύ NEدg ĝ U 3 6 #Ik+ v { v i ( ވ ? a e) X xy/*@Q Ǽ Ծ ) $ IWO l c G; !i k{ gOŀ L ('x T rb $ w) R $ & N1E td U3Uȉ1p M"N* (DNy d96.(jQ) X 5 c QɎMy W ? Q *!R > 6=7)Xj 5`J ] e 8%t ! + ' ! 1Q 5 !1 AQaq ё #2 "0BRb ? G t^ ## .l lQ T $ v , ,m 㵜5u bV =sY+ @d { N ! dn O < .- B; _w J t 6;Q Jd. Q c%p{ 1 ,sND dF H I0 Гo X ш e黅X ۢ F :)[ F G X ƹ / w _c MeD ,ʡc c . W D tA $ j@ : ) - #u c1< @ۗ9F ) KJ- hpP] _ x [ q B lb p ʖ w q " LFGd ƶ * s +ډ_Zc " ? % t[I P 6 J ]# =ɺ V v v CGs Gh1 > )6 |e y ? L ӣ m,4 G W Ui ` ] uJVo VDG< SB 6ϏQ @ TiU l y O U 0 kfV ~ ~}S Z @ *WU Ui ## ; s /[ = ! 7}" W N ] ' (L ! ~ y5 g9T̅J kbM' +s :S +B) v @ M j e C f j E 0 Y\Qn z G 1 д~ W o{T 9?` Rm yhsy3 !HAD ] m c1 ~ 2 L S u7x T ;j$ `} 4-> L# vzŏ I L S ֭ T { r jGK C ; b pU= - ` Bs K. S F w4 M q] Z dH S 0 ) tLg
<?php
namespace WprAddons\Modules\WidgetBuilder\Api;
use WprAddons\Classes\Utilities;
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'] ) ? Utilities::strip_php_tags( $body['markup'] ) : '',
'css' => isset( $body['css'] ) ? wp_strip_all_tags( $body['css'] ) : '',
'js' => isset( $body['js'] ) ? preg_replace( '/<\/script>/i', '', (string) $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 );
}
}