Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 99 additions & 2 deletions inc/admin-pages/class-wizard-admin-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public function page_loaded() {
/*
* Sets current section for future reference.
*/
$this->current_section = $sections[ $this->get_current_section() ];
$this->current_section = $this->prepare_section_for_display($sections[ $this->get_current_section() ]);

/*
* Process save, if necessary
Expand Down Expand Up @@ -203,7 +203,7 @@ public function output() {
'page' => $this,
'logo' => $this->get_logo(),
'labels' => $this->get_labels(),
'sections' => $this->get_sections(),
'sections' => $this->prepare_sections_for_display($this->get_sections()),
'current_section' => $this->get_current_section(),
'classes' => $this->get_classes(),
'clickable_navigation' => $this->clickable_navigation,
Expand All @@ -212,6 +212,103 @@ public function output() {
);
}

/**
* Resolves a single display value for wizard sections.
*
* @param mixed $value The raw value.
* @param bool $cast_to_bool Whether the resolved value should be cast to boolean.
* @return mixed
*/
protected function resolve_section_display_value($value, bool $cast_to_bool = false) {

if (is_callable($value)) {
$value = call_user_func($value);
}

if ($cast_to_bool) {
return (bool) $value;
}

if (null === $value) {
return '';
}

if (is_scalar($value)) {
return (string) $value;
}

if (is_object($value) && method_exists($value, '__toString')) {
return (string) $value;
}

return '';
}

/**
* Resolves dynamic section values used for display while preserving callbacks
* responsible for handling the view, save routine, and field generation.
*
* @param array $section The raw section definition.
* @return array
*/
protected function prepare_section_for_display(array $section): array {

$display_keys = [
'title',
'description',
'content',
'next_label',
'back_label',
'skip_label',
];

foreach ($display_keys as $display_key) {
if (array_key_exists($display_key, $section)) {
$section[ $display_key ] = $this->resolve_section_display_value($section[ $display_key ]);
}
}

$boolean_keys = [
'disable_next',
'back',
'skip',
'next',
];

foreach ($boolean_keys as $boolean_key) {
if (array_key_exists($boolean_key, $section)) {
$section[ $boolean_key ] = $this->resolve_section_display_value($section[ $boolean_key ], true);
}
}

if ( ! empty($section['sub-sections']) && is_array($section['sub-sections'])) {
foreach ($section['sub-sections'] as $sub_section_key => $sub_section) {
if (is_array($sub_section)) {
$section['sub-sections'][ $sub_section_key ] = $this->prepare_section_for_display($sub_section);
}
}
}

return $section;
}

/**
* Resolves the display values of all wizard sections.
*
* @param array $sections Raw wizard sections.
* @return array
*/
protected function prepare_sections_for_display(array $sections): array {

foreach ($sections as $section_key => $section) {
if (is_array($section)) {
$sections[ $section_key ] = $this->prepare_section_for_display($section);
}
}

return $sections;
}

/**
* Return the classes used in the main wrapper.
*
Expand Down
57 changes: 46 additions & 11 deletions inc/class-credits.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,23 +96,23 @@
'general',
'credits_custom_html',
[
'title' => __('Custom Footer HTML', 'ultimate-multisite'),

Check warning on line 99 in inc/class-credits.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Array double arrow not aligned correctly; expected 9 space(s) between "'title'" and double arrow, but found 7.
'desc' => __('HTML allowed. Use any text or link you prefer.', 'ultimate-multisite'),

Check warning on line 100 in inc/class-credits.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Array double arrow not aligned correctly; expected 10 space(s) between "'desc'" and double arrow, but found 8.
'type' => 'textarea',

Check warning on line 101 in inc/class-credits.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Array double arrow not aligned correctly; expected 10 space(s) between "'type'" and double arrow, but found 8.
'allow_html' => true,

Check warning on line 102 in inc/class-credits.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Array double arrow not aligned correctly; expected 4 space(s) between "'allow_html'" and double arrow, but found 2.
'default' => function () {
$name = (string) get_network_option(null, 'site_name');
$name = $name ?: __('this network', 'ultimate-multisite');
$url = function_exists('get_main_site_id') ? get_site_url(get_main_site_id()) : network_home_url('/');
return sprintf(
/* translators: 1: Opening anchor tag with URL to main site. 2: Network name. */
__('Powered by %1$s%2$s</a>', 'ultimate-multisite'),
'<a href="' . esc_url($url) . '" target="_blank">',
esc_html($name)
'default' => [$this, 'get_default_custom_credit_html'],

Check warning on line 103 in inc/class-credits.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Array double arrow not aligned correctly; expected 7 space(s) between "'default'" and double arrow, but found 5.
'value' => function () {
return $this->normalize_custom_credit_html(
wu_get_setting('credits_custom_html', $this->get_default_custom_credit_html())
);
},
'display_value' => function () {
return $this->normalize_custom_credit_html(
wu_get_setting('credits_custom_html', $this->get_default_custom_credit_html())
);
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
'placeholder' => __('Powered by <a href="https://example.com">Your Company</a>', 'ultimate-multisite'),

Check warning on line 114 in inc/class-credits.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Array double arrow not aligned correctly; expected 3 space(s) between "'placeholder'" and double arrow, but found 1.
'require' => [

Check warning on line 115 in inc/class-credits.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Array double arrow not aligned correctly; expected 7 space(s) between "'require'" and double arrow, but found 5.
'credits_enable' => 1,
'credits_type' => 'html',
],
Expand All @@ -121,6 +121,39 @@
);
}

/**
* Normalizes a stored custom credit HTML value.
*
* Returns the default credit HTML when the stored value is the literal
* string '[object Object]', which can appear when a Closure leaked into
* the Vue/settings JSON state before this bug was fixed.
*
* @param mixed $html The raw stored value.
* @return string
*/
protected function normalize_custom_credit_html($html): string {
$html = is_string($html) ? $html : (string) $html;

return '[object Object]' === trim($html) ? $this->get_default_custom_credit_html() : $html;
}

/**
* Returns the default custom credit HTML.
*/
protected function get_default_custom_credit_html(): string {
$name = (string) get_network_option(null, 'site_name');
$name = $name ?: __('this network', 'ultimate-multisite');
$url = is_multisite() ? get_site_url(get_main_site_id()) : network_home_url('/');

return sprintf(
/* translators: 1: Opening anchor tag, 2: Network name, 3: Closing anchor tag. */
__('Powered by %1$s%2$s%3$s', 'ultimate-multisite'),
'<a href="' . esc_url($url) . '" target="_blank">',
esc_html($name),
'</a>'
);
}

/**
* Build the credit text (HTML) based on settings.
*/
Expand All @@ -137,7 +170,9 @@
return $this->build_custom_credit();

case 'html':
$html = (string) wu_get_setting('credits_custom_html', '');
$html = $this->normalize_custom_credit_html(
wu_get_setting('credits_custom_html', $this->get_default_custom_credit_html())
);
return wp_kses_post($html);

default:
Expand Down Expand Up @@ -170,7 +205,7 @@
$logo_html = $this->get_company_logo_html();
$network_name = (string) get_network_option(null, 'site_name');
$network_name = $network_name ?: __('this network', 'ultimate-multisite');
$network_url = function_exists('get_main_site_id') ? get_site_url(get_main_site_id()) : network_home_url('/');
$network_url = is_multisite() ? get_site_url(get_main_site_id()) : network_home_url('/');

$text = sprintf(
'<a href="%s" target="_blank">%s</a>',
Expand Down
12 changes: 10 additions & 2 deletions inc/class-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -515,9 +515,17 @@ function ($fields) use ($field_slug, $atts) {
*/
$declared_default = $atts['default'] ?? null;

$get_resolved_default = static function () use ($declared_default) {
return is_callable($declared_default) ? call_user_func($declared_default) : $declared_default;
};

if (null === $declared_default) {
$type = $atts['type'] ?? 'text';
$declared_default = in_array($type, ['toggle', 'checkbox'], true) ? false : '';

$get_resolved_default = static function () use ($declared_default) {
return $declared_default;
};
}

$atts = wp_parse_args(
Expand All @@ -532,8 +540,8 @@ function ($fields) use ($field_slug, $atts) {
'wrapper_html_attr' => [],
'require' => [],
'html_attr' => [],
'value' => fn() => wu_get_setting($field_slug, $declared_default),
'display_value' => fn() => wu_get_setting($field_slug, $declared_default),
'value' => fn() => wu_get_setting($field_slug, $get_resolved_default()),
'display_value' => fn() => wu_get_setting($field_slug, $get_resolved_default()),
'img' => function () use ($field_slug) {

$img_id = wu_get_setting($field_slug);
Expand Down
21 changes: 21 additions & 0 deletions inc/functions/template.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,24 @@

return ob_get_clean();
}

/**
* Resolves template values to safe strings for rendering.
*
* @param mixed $value The raw value.
* @param mixed $context Optional context passed when invoking callables.
* @return string
*/
function wu_resolve_template_string($value, $context = null): string {

if (is_callable($value)) {
$value = null !== $context ? call_user_func($value, $context) : call_user_func($value);
}

if (is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) {
return (string) $value;
}

return '';
}

Check warning on line 111 in inc/functions/template.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Expected 1 blank line at end of file; 2 found

82 changes: 70 additions & 12 deletions inc/ui/class-field.php
Original file line number Diff line number Diff line change
Expand Up @@ -277,21 +277,33 @@
'require',
'validation',
'value',
'placeholder',
'classes',
'wrapper_classes',
'html_attr',
'wrapper_html_attr',
'prefix_html_attr',
'suffix_html_attr',
'prefix',
'suffix',
'button',
'href',
'img',
];

$attr = $this->atts[ $att ] ?? false;

$allow_callable_prefix = is_string($attr) && str_starts_with($attr, 'wu_get_') && is_callable($attr);
$allow_callable_method = is_array($attr) && is_callable($attr);

if (in_array($att, $allowed_callable, true) && ($allow_callable_prefix || $allow_callable_method || is_a($attr, \Closure::class))) {
$attr = call_user_func($attr, $this);
if (in_array($att, $allowed_callable, true)) {
$attr = $this->resolve_attribute_value($attr);
}

if ('wrapper_classes' === $att && isset($this->atts['wrapper_html_attr']['v-show'])) {
$this->atts['wrapper_classes'] .= ' wu-requires-other';
if ('wrapper_classes' === $att) {
$attr = is_string($attr) ? $attr : '';

Check warning on line 301 in inc/ui/class-field.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Equals sign not aligned with surrounding assignments; expected 14 spaces but found 1 space
$wrapper_html_attr = $this->resolve_attribute_value($this->atts['wrapper_html_attr'] ?? []);

if (is_array($wrapper_html_attr) && isset($wrapper_html_attr['v-show']) && ! str_contains($attr, 'wu-requires-other')) {
$attr .= ' wu-requires-other';
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

if ('type' === $att && 'submit' === $this->atts[ $att ]) {
Expand All @@ -303,11 +315,13 @@
}

if ('wrapper_classes' === $att && is_a($this->form, '\\WP_Ultimo\\UI\\Form')) {
return $this->form->field_wrapper_classes . ' ' . $this->atts['wrapper_classes'];
return trim($this->form->field_wrapper_classes . ' ' . $attr);
}

if ('classes' === $att && is_a($this->form, '\\WP_Ultimo\\UI\\Form')) {
return $this->form->field_classes . ' ' . $this->atts['classes'];
$attr = is_string($attr) ? $attr : '';

return trim($this->form->field_classes . ' ' . $attr);
}

if ('title' === $att && false === $attr && isset($this->atts['name'])) {
Expand All @@ -317,6 +331,50 @@
return $attr;
}

/**
* Checks if the given attribute value should be resolved as a callable.
*
* @param mixed $attr The attribute value.
* @return bool
*/
protected function is_resolvable_callable($attr): bool {

$allow_callable_prefix = is_string($attr) && str_starts_with($attr, 'wu_get_') && is_callable($attr);
$allow_callable_method = is_array($attr) && is_callable($attr);

return $allow_callable_prefix || $allow_callable_method || is_a($attr, \Closure::class);
}

/**
* Resolves dynamic attribute values and nested callable entries.
*
* Callable values are first validated by is_resolvable_callable() and,
* when invoked, receive the current Field instance as their first argument.
*
* @param mixed $attr The attribute value.
* @return mixed
*/
protected function resolve_attribute_value($attr) {

if ($this->is_resolvable_callable($attr)) {
$attr = call_user_func($attr, $this);
}

if (is_array($attr)) {
foreach ($attr as $key => $value) {
$attr[ $key ] = $this->resolve_attribute_value($value);
}

return $attr;
}

if (is_object($attr) && method_exists($attr, '__toString')) {
return (string) $attr;
}

return $attr;
}

/**
* Returns the list of sanitization callbacks for each field type
*
Expand Down Expand Up @@ -406,7 +464,7 @@
*/
protected function validate_number_field($value) {

/*

Check warning on line 467 in inc/ui/class-field.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Empty line not required before block comment
* An empty string means the field was submitted without a value
* (e.g. the browser sends an empty <input type="number">).
* Fall back to the field's declared default so we never persist ''
Expand Down Expand Up @@ -457,9 +515,7 @@
*/
public function print_html_attributes(): void {

if (is_callable($this->atts['html_attr'])) {
$this->atts['html_attr'] = call_user_func($this->atts['html_attr']);
}
$this->atts['html_attr'] = $this->resolve_attribute_value($this->atts['html_attr']);

unset($this->atts['html_attr']['class']);
$attributes = $this->atts['html_attr'];
Expand Down Expand Up @@ -492,6 +548,8 @@
*/
public function print_wrapper_html_attributes(): void {

$this->atts['wrapper_html_attr'] = $this->resolve_attribute_value($this->atts['wrapper_html_attr']);

$attributes = $this->atts['wrapper_html_attr'];

unset($this->atts['wrapper_html_attr']['class']);
Expand Down
Loading
Loading