PluginProbe
Ultimate Post Kit – Elementor Post Grid, Post Carousel, Post Slider & Blog Layout Widgets / trunk
Ultimate Post Kit – Elementor Post Grid, Post Carousel, Post Slider & Blog Layout Widgets vtrunk
4.5.4 4.2.1 4.2.2 4.2.3 4.5.0 4.5.2 4.5.3 4.2.0 4.1.18 4.1.17 4.1.16 4.1.15 4.1.14 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.0.9 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 All 146 releases
← All changes | admin/class-settings-api.php +166 -30 4.1.10trunk View file →
@@ -1,6 +1,10 @@
1 1 <?php
2 2
3 +if (!defined('ABSPATH')) {
4 + exit; // Exit if accessed directly.
5 +}
6 +
3 7 if (!class_exists('UltimatePostKit_Settings_API')) :
4 8
5 9 class UltimatePostKit_Settings_API {
6 10
@@ -166,9 +170,9 @@
166 170 if (!empty($field['args']['widget_type']) && 'pro' == $field['args']['widget_type'] && true !== _is_upk_pro_activated()) {
167 171 $data_type .= ' bdt-tooltip="'.esc_html__('Pro widget only works with Pro version.', 'ultimate-post-kit').'"';
168 172 }
169 173
170 - echo "<div class='upk-option-item {$class} {$widget_used_status}' {$data_type}>";
174 + echo wp_kses("<div class='upk-option-item {$class} {$widget_used_status}' {$data_type}>", $this->get_allowed_field_html());
171 175
172 176 call_user_func($field['callback'], $field['args']);
173 177
174 178 echo '</div>';
@@ -192,9 +196,9 @@
192 196
193 197 if (isset($section['desc']) && !empty($section['desc'])) {
194 198 $section['desc'] = '<div class="inside">' . $section['desc'] . '</div>';
195 199 $callback = function () use ($section) {
196 - echo str_replace('"', '\"', $section['desc']);
200 + echo wp_kses_post($section['desc']);
197 201 };
198 202 } else if (isset($section['callback'])) {
199 203 $callback = $section['callback'];
200 204 } else {
@@ -243,9 +247,16 @@
243 247 }
244 248
245 249 // creates our settings in the options table
246 250 foreach ($this->settings_sections as $section) {
247 - register_setting($section['id'], $section['id'], array($this, 'sanitize_options'));
251 + register_setting(
252 + $section['id'],
253 + $section['id'],
254 + array(
255 + 'type' => 'array',
256 + 'sanitize_callback' => array($this, 'sanitize_options'),
257 + )
258 + );
248 259 }
249 260 }
250 261
251 262 /**
@@ -263,8 +274,70 @@
263 274 return $desc;
264 275 }
265 276
266 277 /**
278 + * Allowed HTML for settings-field output. Covers every form control this
279 + * class renders so field markup survives wp_kses() intact.
280 + *
281 + * @return array
282 + */
283 + public function get_allowed_field_html() {
284 + $attr = array(
285 + 'class' => array(),
286 + 'id' => array(),
287 + 'name' => array(),
288 + 'value' => array(),
289 + 'type' => array(),
290 + 'checked' => array(),
291 + 'selected' => array(),
292 + 'multiple' => array(),
293 + 'disabled' => array(),
294 + 'readonly' => array(),
295 + 'placeholder' => array(),
296 + 'min' => array(),
297 + 'max' => array(),
298 + 'step' => array(),
299 + 'rows' => array(),
300 + 'cols' => array(),
301 + 'for' => array(),
302 + 'scope' => array(),
303 + 'style' => array(),
304 + 'title' => array(),
305 + 'target' => array(),
306 + 'href' => array(),
307 + 'src' => array(),
308 + 'rel' => array(),
309 + 'aria-hidden' => array(),
310 + 'bdt-tooltip' => array(),
311 + 'bdt-grid' => array(),
312 + 'data-default-color' => array(),
313 + 'data-type' => array(),
314 + 'data-widget-type' => array(),
315 + 'data-content-type' => array(),
316 + 'data-widget-name' => array(),
317 + );
318 +
319 + return array(
320 + 'div' => $attr,
321 + 'span' => $attr,
322 + 'label' => $attr,
323 + 'input' => $attr,
324 + 'select' => $attr,
325 + 'option' => $attr,
326 + 'textarea' => $attr,
327 + 'fieldset' => $attr,
328 + 'a' => $attr,
329 + 'i' => $attr,
330 + 'p' => $attr,
331 + 'h3' => $attr,
332 + 'hr' => $attr,
333 + 'br' => array(),
334 + 'strong' => array(),
335 + 'em' => array(),
336 + );
337 + }
338 +
339 + /**
267 340 * Displays a text field for a settings field
268 341 *
269 342 * @param array $args settings field args
270 343 */
@@ -291,9 +364,9 @@
291 364 $html .= $this->get_field_description($args);
292 365
293 366 $html .= '</div>';
294 367
295 - echo $html;
368 + echo wp_kses($html, $this->get_allowed_field_html());
296 369 }
297 370
298 371 /**
299 372 * Displays a url field for a settings field
@@ -320,9 +393,9 @@
320 393
321 394 $html = sprintf('<input type="%1$s" class="%2$s-number" id="%3$s[%4$s]" name="%3$s[%4$s]" value="%5$s"%6$s%7$s%8$s%9$s/>', $type, $size, $args['section'], $args['id'], $value, $placeholder, $min, $max, $step);
322 395 $html .= $this->get_field_description($args);
323 396
324 - echo $html;
397 + echo wp_kses($html, $this->get_allowed_field_html());
325 398 }
326 399
327 400 /**
328 401 * Get used widgets.
@@ -392,9 +465,11 @@
392 465 $html .= '</label>';
393 466
394 467 $html .= '<div class="upk-option-links">';
395 468 if ($args['demo_url']) {
396 - $html .= '<a href=' . $args['demo_url'] . ' target="_blank" class="upk-option-demo" title="' . esc_html__('View ' . $args['name'] . ' Widget Demo', 'ultimate-post-kit') . '">' . esc_html__('Demo', 'ultimate-post-kit') . '<i class="upk-icon-preview" aria-hidden="true"></i></a>';
469 + /* translators: %s: widget name */
470 + $demo_title = sprintf(esc_html__('View %s Widget Demo', 'ultimate-post-kit'), $args['name']);
471 + $html .= '<a href=' . $args['demo_url'] . ' target="_blank" class="upk-option-demo" title="' . $demo_title . '">' . esc_html__('Demo', 'ultimate-post-kit') . '<i class="upk-icon-preview" aria-hidden="true"></i></a>';
397 472 }
398 473 if ($args['video_url']) {
399 474 $html .= '<a href=' . $args['video_url'] . ' target="_blank" class="upk-option-video" title="View ' . $args['name'] . ' Video Tutorial">Video<i class="upk-icon-tutorial" aria-hidden="true"></i></a>';
400 475 }
@@ -508,9 +583,9 @@
508 583
509 584 $html .= $this->get_field_description($args);
510 585 $html .= '</fieldset>';
511 586
512 - echo $html;
587 + echo wp_kses($html, $this->get_allowed_field_html());
513 588 }
514 589
515 590 /**
516 591 * Displays a radio button for a settings field
@@ -530,9 +605,9 @@
530 605
531 606 $html .= $this->get_field_description($args);
532 607 $html .= '</fieldset>';
533 608
534 - echo $html;
609 + echo wp_kses($html, $this->get_allowed_field_html());
535 610 }
536 611
537 612 /**
538 613 * Displays a selectbox for a settings field
@@ -551,9 +626,9 @@
551 626
552 627 $html .= sprintf('</select>');
553 628 $html .= $this->get_field_description($args);
554 629
555 - echo $html;
630 + echo wp_kses($html, $this->get_allowed_field_html());
556 631 }
557 632
558 633 /**
559 634 * Displays a textarea for a settings field
@@ -573,9 +648,9 @@
573 648
574 649 $html .= sprintf('<textarea rows="5" cols="55" class="%1$s-text" id="%2$s[%3$s]" name="%2$s[%3$s]" %4$s >%5$s</textarea>', $size, $args['section'], $args['id'], $placeholder, $value);
575 650 $html .= $this->get_field_description($args);
576 651
577 - echo $html;
652 + echo wp_kses($html, $this->get_allowed_field_html());
578 653 }
579 654
580 655 /**
581 656 * Displays the html for a settings field
@@ -583,9 +658,9 @@
583 658 * @param array $args settings field args
584 659 * @return string
585 660 */
586 661 function callback_html($args) {
587 - echo $args['desc'];
662 + echo wp_kses($args['desc'], $this->get_allowed_field_html());
588 663 }
589 664
590 665 /**
591 666 * Displays a file upload field for a settings field
@@ -602,9 +677,9 @@
602 677 $html = sprintf('<input type="text" class="%1$s-text wpsa-url" id="%2$s[%3$s]" name="%2$s[%3$s]" value="%4$s"/>', $size, $args['section'], $args['id'], $value);
603 678 $html .= '<input type="button" class="button wpsa-browse" value="' . $label . '" />';
604 679 $html .= $this->get_field_description($args);
605 680
606 - echo $html;
681 + echo wp_kses($html, $this->get_allowed_field_html());
607 682 }
608 683
609 684 /**
610 685 * Displays a password field for a settings field
@@ -618,9 +693,9 @@
618 693
619 694 $html = sprintf('<input type="password" class="%1$s-text" id="%2$s[%3$s]" name="%2$s[%3$s]" value="%4$s"/>', $size, $args['section'], $args['id'], $value);
620 695 $html .= $this->get_field_description($args);
621 696
622 - echo $html;
697 + echo wp_kses($html, $this->get_allowed_field_html());
623 698 }
624 699
625 700 /**
626 701 * Displays a color picker field for a settings field
@@ -634,9 +709,9 @@
634 709
635 710 $html = sprintf('<input type="text" class="%1$s-text wp-color-picker-field" id="%2$s[%3$s]" name="%2$s[%3$s]" value="%4$s" data-default-color="%5$s" />', $size, $args['section'], $args['id'], $value, $args['std']);
636 711 $html .= $this->get_field_description($args);
637 712
638 - echo $html;
713 + echo wp_kses($html, $this->get_allowed_field_html());
639 714 }
640 715
641 716 /**
642 717 * Displays a 2 colspan subheading field for a settings field
@@ -648,9 +723,9 @@
648 723 $html = '<h3 class="setting_subheading column-merge">' . $args['name'] . '</h3>';
649 724 $html .= $this->get_field_description($args);
650 725 $html .= '<hr class="setting_separator">';
651 726
652 - echo $html;
727 + echo wp_kses($html, $this->get_allowed_field_html());
653 728 }
654 729
655 730 function callback_start_group($args) {
656 731
@@ -667,9 +742,9 @@
667 742 $html .= $this->get_field_description($args);
668 743
669 744 $html .= '<div class="bdt-grid" bdt-grid>';
670 745
671 - echo $html;
746 + echo wp_kses($html, $this->get_allowed_field_html());
672 747 }
673 748
674 749 function callback_end_group($args) {
675 750
@@ -675,9 +750,9 @@
675 750
676 751 $html = '</div>';
677 752 $html .= '</div>';
678 753
679 - echo $html;
754 + echo wp_kses($html, $this->get_allowed_field_html());
680 755 }
681 756
682 757 /**
683 758 * Displays a 2 colspan separator field for a settings field
@@ -689,9 +764,9 @@
689 764 $html = '<hr class="setting_separator column-merge">';
690 765 $html .= $this->get_field_description($args);
691 766
692 767
693 - echo $html;
768 + echo wp_kses($html, $this->get_allowed_field_html());
694 769 }
695 770
696 771
697 772 /**
@@ -706,10 +781,11 @@
706 781 'name' => $args['section'] . '[' . $args['id'] . ']',
707 782 'id' => $args['section'] . '[' . $args['id'] . ']',
708 783 'echo' => 0
709 784 );
785 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- 'echo' => 0 makes wp_dropdown_pages() return (not print) the markup, which is escaped via wp_kses() below.
710 786 $html = wp_dropdown_pages($dropdown_args);
711 - echo $html;
787 + echo wp_kses($html, $this->get_allowed_field_html());
712 788 }
713 789
714 790 /**
715 791 * Sanitize callback for Settings API
@@ -724,13 +800,17 @@
724 800
725 801 foreach ($options as $option_slug => $option_value) {
726 802 $sanitize_callback = $this->get_sanitize_callback($option_slug);
727 803
728 - // If callback is set, call it
804 + // If a field-specific callback is set, use it.
729 805 if ($sanitize_callback) {
730 806 $options[$option_slug] = call_user_func($sanitize_callback, $option_value);
731 807 continue;
732 808 }
809 +
810 + // Otherwise never store the value raw — apply a safe default so no
811 + // submitted field escapes sanitization (wp.org register_setting rule).
812 + $options[$option_slug] = $this->sanitize_default($option_value);
733 813 }
734 814
735 815 return $options;
736 816 }
@@ -735,8 +815,22 @@
735 815 return $options;
736 816 }
737 817
738 818 /**
819 + * Default sanitizer for settings values without a field-specific callback.
820 + *
821 + * @param mixed $value Raw value.
822 + * @return mixed
823 + */
824 + private function sanitize_default($value) {
825 + if (is_array($value)) {
826 + return array_map(array($this, 'sanitize_default'), $value);
827 + }
828 +
829 + return is_scalar($value) ? sanitize_text_field((string) $value) : '';
830 + }
831 +
832 + /**
739 833 * Get sanitization callback for given option slug
740 834 *
741 835 * @param string $slug option slug
742 836 *
@@ -802,13 +896,30 @@
802 896 $icon = isset($tab['icon']) ? $tab['icon'] : 'dashicons dashicons-screenoptions';
803 897 $html .= sprintf('<li><a href="#%1$s" class="bdt-tab-item" id="bdt-%1$s" data-tab-index="%2$s"><i class="%4$s"></i>%3$s</a></li>', $tab['id'], $count++, $tab['title'], $icon);
804 898 }
805 899
900 + // Extension tabs registered by add-ons (e.g. Ultimate Post Kit Pro). The
901 + // core plugin only provides the extension point; it ships no tabs of its own.
902 + foreach ($this->get_extra_dashboard_tabs() as $tab) {
903 + if (empty($tab['id']) || empty($tab['title'])) {
904 + continue;
905 + }
906 + $icon = isset($tab['icon']) ? $tab['icon'] : 'dashicons dashicons-screenoptions';
907 + $html .= sprintf('<li><a href="#%1$s" class="bdt-tab-item" id="bdt-%1$s" data-tab-index="%2$s"><i class="%4$s"></i>%3$s</a></li>', $tab['id'], $count++, esc_html($tab['title']), esc_attr($icon));
908 + }
909 +
806 910 // License section
807 911 $license_wl_status = UltimatePostKit_Admin_Settings::license_wl_status();
808 912
809 913 if (!defined('BDTUPK_LO') || false == $license_wl_status) {
810 - $html .= sprintf('<li><a href="#%1$s" class="bdt-tab-item" id="bdt-%1$s" data-tab-index="%2$s"><i class="dashicons dashicons-admin-network"></i>%3$s</a></li>', 'ultimate_post_kit_license_settings', $count, esc_html__('License', 'ultimate-post-kit'));
914 + // On the free version this tab shows the "Get Pro" page, not a license form,
915 + // so label it accordingly.
916 + $is_pro_activated = function_exists('_is_upk_pro_activated') ? _is_upk_pro_activated() : false;
917 + $license_tab_title = (true === $is_pro_activated)
918 + ? esc_html__('License', 'ultimate-post-kit')
919 + : esc_html__('Get Pro', 'ultimate-post-kit');
920 +
921 + $html .= sprintf('<li><a href="#%1$s" class="bdt-tab-item" id="bdt-%1$s" data-tab-index="%2$s"><i class="dashicons dashicons-admin-network"></i>%3$s</a></li>', 'ultimate_post_kit_license_settings', $count, $license_tab_title);
811 922 }
812 923
813 924 $html .= '</ul>';
814 925 $html .= '</div>';
@@ -835,8 +946,22 @@
835 946 )
836 947 ));
837 948 }
838 949
950 + /**
951 + * Extra dashboard tabs contributed by add-on plugins.
952 + *
953 + * Neutral extension point: the core plugin renders whatever tabs an add-on
954 + * registers here and ships none of its own. Each item is an array
955 + * [ 'id' => string, 'title' => string, 'icon' => string, 'callback' =>
956 + * callable ] where the callback echoes the tab body.
957 + *
958 + * @return array
959 + */
960 + public function get_extra_dashboard_tabs() {
961 + return (array) apply_filters( 'ultimate_post_kit_dashboard_extra_tabs', array() );
962 + }
963 +
839 964 function ultimate_post_kit_settings_save() {
840 965
841 966 if (!check_ajax_referer('ultimate-post-kit-settings-save-nonce')) {
842 967 wp_send_json_error();
@@ -845,14 +970,30 @@
845 970 if (!current_user_can('manage_options')) {
846 971 return;
847 972 }
848 973
849 - $moudle_id = sanitize_text_field($_POST['id']);
974 + $moudle_id = isset($_POST['id']) ? sanitize_text_field(wp_unslash($_POST['id'])) : '';
850 975
851 976 unset($_POST['id']);
852 977
978 + // Only ever write options inside this plugin's own namespace. Without
979 + // this the option name was fully attacker-chosen, letting a request
980 + // overwrite arbitrary core options (default_role, siteurl, ...).
981 + if ('' === $moudle_id || 0 !== strpos($moudle_id, 'ultimate_post_kit')) {
982 + wp_send_json_error();
983 + }
984 +
853 985 if (isset($_POST[$moudle_id])) {
854 - update_option($moudle_id, $_POST[$moudle_id]);
986 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized below via sanitize_options()/sanitize_default().
987 + $raw_value = wp_unslash($_POST[$moudle_id]);
988 +
989 + // Route the value through the registered per-field sanitizers
990 + // instead of storing raw request data.
991 + $value = is_array($raw_value)
992 + ? $this->sanitize_options($raw_value)
993 + : sanitize_text_field($raw_value);
994 +
995 + update_option($moudle_id, $value);
855 996 }
856 997
857 998 wp_send_json_success();
858 999 }
@@ -866,13 +1007,8 @@
866 1007
867 1008 // Add manually created content sections that don't have settings forms
868 1009 $content_only_sections = [
869 1010 [
870 - 'id' => 'ultimate_post_kit_extra_options',
871 - 'title' => esc_html__('Extra Options', 'ultimate-post-kit'),
872 - 'icon' => 'dashicons dashicons-smiley',
873 - ],
874 - [
875 1011 'id' => 'ultimate_post_kit_analytics_system_req',
876 1012 'title' => esc_html__('System Status', 'ultimate-post-kit'),
877 1013 'icon' => 'dashicons dashicons-chart-bar',
878 1014 ],
@@ -1081,13 +1217,13 @@
1081 1217 }
1082 1218
1083 1219 wp_nonce_field('ultimate-post-kit-settings-save-nonce');
1084 1220
1085 - do_action('wsa_form_top_' . $form['id'], $form);
1221 + do_action('ultimate_post_kit_form_top_' . $form['id'], $form);
1086 1222
1087 1223 $this->do_settings_sections($form['id']);
1088 1224
1089 - do_action('wsa_form_bottom_' . $form['id'], $form);
1225 + do_action('ultimate_post_kit_form_bottom_' . $form['id'], $form);
1090 1226
1091 1227 ?>
1092 1228
1093 1229 </form>