Skip to content

Commit c70ea6b

Browse files
committed
Update Settings API from freemkit with bug fixes
- Copy latest Settings API from freemkit - Update namespace to WebberZone\Knowledge_Base\Admin\Settings - Include all bug fixes: * Repeater row_id fix for unique item identification * Hidden input name fix for proper form submission * Regex fix for precise reindexing with lookahead * Selector fix from input to :input for all form elements * JavaScript placement fix moved out of render_repeater_item * Sanitization fix with row_id lookup for sensitive fields - Update package declarations and use statements
1 parent 2f4fdfc commit c70ea6b

5 files changed

Lines changed: 197 additions & 86 deletions

File tree

includes/admin/settings/class-settings-api.php

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
/**
1919
* Settings API wrapper class
2020
*
21-
* @version 2.8.1
21+
* @version 2.8.2
2222
*/
2323
class Settings_API {
2424

@@ -27,7 +27,7 @@ class Settings_API {
2727
*
2828
* @var string
2929
*/
30-
public const VERSION = '2.8.1';
30+
public const VERSION = '2.8.2';
3131

3232
/**
3333
* Settings Key.
@@ -866,6 +866,7 @@ public function settings_sanitize( $input ) {
866866

867867
// Get the various settings we've registered.
868868
$settings = get_option( $this->settings_key );
869+
$settings = is_array( $settings ) ? $settings : array();
869870
$settings_types = $this->get_registered_settings_types();
870871

871872
// Get the tab. This is also our settings' section.
@@ -896,18 +897,15 @@ public function settings_sanitize( $input ) {
896897
continue;
897898
}
898899

899-
if ( array_key_exists( $key, $output ) ) {
900+
if ( array_key_exists( $key, $input ) ) {
900901
$sanitize_callback = $this->get_sanitize_callback( $key );
901902

902903
// If callback is set, call it.
903904
if ( $sanitize_callback ) {
904-
// Pass the field configuration for repeater fields.
905-
if ( 'repeater' === $type && isset( $this->registered_settings[ $key ] ) ) {
906-
$output[ $key ] = call_user_func( $sanitize_callback, $output[ $key ], $this->registered_settings[ $key ] );
907-
} elseif ( 'sensitive' === $type ) {
908-
$output[ $key ] = call_user_func( $sanitize_callback, $output[ $key ], $key );
905+
if ( 'sensitive' === $type ) {
906+
$output[ $key ] = call_user_func( $sanitize_callback, $input[ $key ], $key );
909907
} else {
910-
$output[ $key ] = call_user_func( $sanitize_callback, $output[ $key ] );
908+
$output[ $key ] = call_user_func( $sanitize_callback, $input[ $key ] );
911909
}
912910
continue;
913911
}
@@ -1170,7 +1168,7 @@ public static function parse_field_args( $field, $section = '' ) {
11701168
* @param string $prefix Optional prefix for fallback key.
11711169
* @return string The encryption key.
11721170
*/
1173-
private static function get_encryption_key( $prefix = '' ) {
1171+
public static function get_encryption_key( $prefix = '' ) {
11741172
$fallback = $prefix ? str_replace( '-', '_', $prefix ) . '_encryption_fallback' : 'settings_api_encryption_fallback';
11751173
return defined( 'AUTH_SALT' ) ? AUTH_SALT : ( defined( 'SECURE_AUTH_SALT' ) ? SECURE_AUTH_SALT : hash( 'sha256', __NAMESPACE__ . $fallback ) );
11761174
}

includes/admin/settings/class-settings-form.php

Lines changed: 64 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -862,24 +862,34 @@ public function callback_repeater( $args ) {
862862
</div>
863863

864864
<script>
865-
jQuery(document).ready(function($) {
866-
var wrapper = $('#<?php echo esc_js( $args['id'] ); ?>-wrapper');
867-
var itemsContainer = wrapper.find('.<?php echo esc_js( $args['id'] ); ?>-items');
868-
var index = <?php echo esc_js( (string) count( $value ) ); ?>;
865+
jQuery(document).ready(function($) {
866+
var wrapper = $('#<?php echo esc_js( $args['id'] ); ?>-wrapper');
867+
var itemsContainer = wrapper.find('.<?php echo esc_js( $args['id'] ); ?>-items');
868+
var index = <?php echo esc_js( (string) count( $value ) ); ?>;
869+
var liveUpdateField = '<?php echo esc_js( ! empty( $args['live_update_field'] ) ? $args['live_update_field'] : 'name' ); ?>';
870+
var fallbackTitle = '<?php echo esc_js( ! empty( $args['new_item_text'] ) ? $args['new_item_text'] : $this->translation_strings['repeater_new_item'] ); ?>';
869871

870872
// Add Item
871-
wrapper.on('click', '.add-item', function() {
872-
var template = wrapper.find('.repeater-template').html();
873-
template = template.replace(/{{INDEX}}/g, index);
874-
itemsContainer.append(template);
875-
index++;
876-
877-
// Ensure the toggle icon for the new item is set to the collapsed state (▲)
878-
itemsContainer.find('.repeater-item-header:last .toggle-icon').text('▲');
879-
880-
// Ensure that .repeater-item-content is set to display:block
881-
itemsContainer.find('.repeater-item-content:last').css('display', 'block');
882-
});
873+
wrapper.on('click', '.add-item', function() {
874+
var template = wrapper.find('.repeater-template').html();
875+
var uniqueId = 'row_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
876+
template = template.replace(/{{INDEX}}/g, index);
877+
template = template.replace(/{{ROW_ID}}/g, uniqueId);
878+
itemsContainer.append(template);
879+
index++;
880+
var newItem = itemsContainer.find('.wz-repeater-item:last');
881+
882+
// Ensure the toggle icon for the new item is set to the collapsed state (▲)
883+
itemsContainer.find('.repeater-item-header:last .toggle-icon').text('▲');
884+
885+
// Ensure that .repeater-item-content is set to display:block
886+
itemsContainer.find('.repeater-item-content:last').css('display', 'block');
887+
888+
if (window.WZInitTomSelect) {
889+
window.WZInitTomSelect(newItem.get(0));
890+
}
891+
document.dispatchEvent(new CustomEvent('wz:repeater-item-added', { detail: { container: newItem.get(0) } }));
892+
});
883893

884894
// Remove Item
885895
wrapper.on('click', '.remove-item', function() {
@@ -923,20 +933,28 @@ public function callback_repeater( $args ) {
923933
}
924934
});
925935

926-
// Reindex Items After Adding, Removing, or Moving
927-
function reindexItems() {
928-
itemsContainer.find('.wz-repeater-item').each(function(idx) {
936+
// Reindex Items After Adding, Removing, or Moving
937+
function reindexItems() {
938+
itemsContainer.find('.wz-repeater-item').each(function(idx) {
929939
$(this).find(':input').each(function() {
930940
var name = $(this).attr('name');
931-
if (name) {
932-
name = name.replace(/\[\d+\]/, '[' + idx + ']');
933-
$(this).attr('name', name);
934-
}
941+
if (name) {
942+
name = name.replace(/\[\d+\](?=\[(?:fields|row_id)\])/, '[' + idx + ']');
943+
$(this).attr('name', name);
944+
}
945+
});
935946
});
947+
}
948+
949+
// Live update repeater title when the specified field changes.
950+
wrapper.on('input', '.wz-repeater-item :input[name$="[fields][' + liveUpdateField + ']"]', function() {
951+
var $this = $(this);
952+
var newName = $this.val();
953+
var $repeaterTitle = $this.closest('.wz-repeater-item').find('.repeater-title');
954+
$repeaterTitle.text(newName || fallbackTitle);
936955
});
937-
}
938-
});
939-
</script>
956+
});
957+
</script>
940958
<?php
941959
$html = ob_get_clean();
942960
$html .= $this->get_field_description( $args );
@@ -953,18 +971,33 @@ function reindexItems() {
953971
* @param array|null $item Item data if exists.
954972
* @return void
955973
*/
956-
private function render_repeater_item( $args, $index, $item = null ) {
974+
public function render_repeater_item( $args, $index, $item = null ) {
957975
if ( empty( $args['fields'] ) || ! is_array( $args['fields'] ) ) {
958976
return;
959977
}
960978

979+
$fallback_title = ! empty( $args['new_item_text'] ) ? $args['new_item_text'] : $this->translation_strings['repeater_new_item'];
980+
981+
// Generate or retrieve unique row ID.
982+
$item_id = '';
983+
if ( is_array( $item ) && isset( $item['row_id'] ) ) {
984+
$item_id = $item['row_id'];
985+
} elseif ( '{{INDEX}}' !== $index ) {
986+
// For existing items without row_id, generate a persistent one.
987+
$item_id = 'row_' . md5( $args['id'] . '_' . $index );
988+
} else {
989+
// For new items, use a placeholder that will be replaced.
990+
$item_id = '{{ROW_ID}}';
991+
}
992+
961993
?>
962-
<div class="wz-repeater-item">
963-
<div class="repeater-item-header">
994+
<div class="wz-repeater-item" data-row-id="<?php echo esc_attr( $item_id ); ?>">
995+
<input type="hidden" name="<?php echo esc_attr( $this->settings_key ); ?>[<?php echo esc_attr( $args['id'] ); ?>][<?php echo esc_attr( $index ); ?>][row_id]" value="<?php echo esc_attr( $item_id ); ?>" />
996+
<div class="repeater-item-header">
964997
<?php
965998
$display_field = ! empty( $args['live_update_field'] ) ? $args['live_update_field'] : 'name';
966999
?>
967-
<span class="repeater-title"><?php echo esc_html( ! empty( $item['fields'][ $display_field ] ) ? $item['fields'][ $display_field ] : $this->translation_strings['repeater_new_item'] ); ?></span>
1000+
<span class="repeater-title"><?php echo esc_html( ! empty( $item['fields'][ $display_field ] ) ? $item['fields'][ $display_field ] : $fallback_title ); ?></span>
9681001
<span class="toggle-icon">▼</span>
9691002
</div>
9701003
<div class="repeater-item-content" style="display: none;">
@@ -1027,22 +1060,7 @@ private function render_repeater_item( $args, $index, $item = null ) {
10271060
</div>
10281061
</div>
10291062

1030-
<script>
1031-
jQuery(document).ready(function($) {
1032-
var wrapper = $('#<?php echo esc_js( $args['id'] ); ?>-wrapper');
1033-
var itemsContainer = wrapper.find('.<?php echo esc_js( $args['id'] ); ?>-items');
1034-
1035-
// Live update repeater title when the specified field changes
1036-
var liveUpdateField = '<?php echo esc_js( ! empty( $args['live_update_field'] ) ? $args['live_update_field'] : 'name' ); ?>';
1037-
wrapper.on('input', '.wz-repeater-item input[name$="[fields][' + liveUpdateField + ']"]', function() {
1038-
var $this = $(this);
1039-
var newName = $this.val();
1040-
var $repeaterTitle = $this.closest('.wz-repeater-item').find('.repeater-title');
1041-
$repeaterTitle.text(newName || '<?php echo esc_js( $this->translation_strings['repeater_new_item'] ); ?>'); // Update title or set default if empty
1042-
});
1043-
});
1044-
</script>
1045-
<?php
1063+
<?php
10461064
}
10471065

10481066

includes/admin/settings/class-settings-sanitize.php

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,9 +298,22 @@ public function sanitize_repeater_field( $value, $field = array() ) {
298298
}
299299

300300
$sanitized_value = array();
301+
$existing_rows = array();
301302

302303
// Get the subfields configuration.
303304
$subfields = ! empty( $field['fields'] ) ? $field['fields'] : array();
305+
if ( ! empty( $field['id'] ) ) {
306+
$stored_value = $this->get_option( $field['id'], array() );
307+
$existing_rows = is_array( $stored_value ) ? $stored_value : array();
308+
}
309+
310+
// Create a lookup table for existing rows by row_id.
311+
$existing_by_id = array();
312+
foreach ( $existing_rows as $existing_row ) {
313+
if ( isset( $existing_row['row_id'] ) ) {
314+
$existing_by_id[ $existing_row['row_id'] ] = $existing_row;
315+
}
316+
}
304317

305318
foreach ( $value as $index => $row ) {
306319
// Ensure we have a valid row structure.
@@ -312,6 +325,17 @@ public function sanitize_repeater_field( $value, $field = array() ) {
312325
'fields' => array(),
313326
);
314327

328+
// Preserve row_id if it exists.
329+
if ( isset( $row['row_id'] ) ) {
330+
$sanitized_row['row_id'] = sanitize_text_field( $row['row_id'] );
331+
}
332+
333+
// Get the corresponding existing row for sensitive field preservation.
334+
$existing_row = null;
335+
if ( isset( $row['row_id'] ) && isset( $existing_by_id[ $row['row_id'] ] ) ) {
336+
$existing_row = $existing_by_id[ $row['row_id'] ];
337+
}
338+
315339
foreach ( $row['fields'] as $field_key => $field_value ) {
316340
$field_key = sanitize_key( $field_key );
317341

@@ -331,10 +355,22 @@ public function sanitize_repeater_field( $value, $field = array() ) {
331355
// Get the field type from the subfield configuration.
332356
$field_type = isset( $field_config['type'] ) ? $field_config['type'] : 'text';
333357

358+
// Preserve existing encrypted sensitive values when form submits masked/empty value.
359+
if ( 'sensitive' === $field_type && ( empty( $field_value ) || ( is_string( $field_value ) && false !== strpos( $field_value, '**' ) ) ) ) {
360+
if ( $existing_row && isset( $existing_row['fields'][ $field_key ] ) ) {
361+
$sanitized_row['fields'][ $field_key ] = $existing_row['fields'][ $field_key ];
362+
}
363+
continue;
364+
}
365+
334366
// Call the appropriate sanitization method.
335367
$sanitize_method = 'sanitize_' . $field_type . '_field';
336368
if ( method_exists( $this, $sanitize_method ) ) {
337-
$sanitized_row['fields'][ $field_key ] = $this->$sanitize_method( $field_value, $field_config );
369+
if ( 'sensitive' === $field_type ) {
370+
$sanitized_row['fields'][ $field_key ] = $this->$sanitize_method( $field_value, $field_key );
371+
} else {
372+
$sanitized_row['fields'][ $field_key ] = $this->$sanitize_method( $field_value, $field_config );
373+
}
338374
} else {
339375
$sanitized_row['fields'][ $field_key ] = $this->sanitize_text_field( $field_value );
340376
}

0 commit comments

Comments
 (0)