*/ public static function abilities(): array { $allowed = static fn (array $args): bool => current_user_can('edit_theme_options'); return [ new Ability( 'sarcio/theme-settings-list', __('Look at the theme’s options', 'sarcio'), Ability::READ, 'The active theme’s own options from the Customizer (id, label, section, kind of field, choices, current value). Only plain options are listed, never code or CSS. A block theme usually has few or none: use the site styles there.', ['properties' => [], 'type' => 'object'], $allowed, static fn (array $args): array => ['settings' => self::list()], ), new Ability( 'sarcio/theme-setting-set', __('Change a theme option', 'sarcio'), Ability::WRITE, 'Set one of the active theme’s options by its id from theme-settings-list. The theme checks the value as the Customizer would; pick from choices where the option has them.', [ 'properties' => [ 'id' => ['maxLength' => 200, 'minLength' => 1, 'type' => 'string'], 'value' => ['maxLength' => self::MAX_VALUE, 'type' => ['string', 'number', 'boolean']], ], 'required' => ['id', 'value'], 'type' => 'object', ], $allowed, static fn (array $args): array => self::set((string) $args['id'], $args['value']), static fn (array $args): array => self::preview((string) $args['id'], $args['value']), static fn (array $before, array $after): array => self::set((string) ($before['id'] ?? ''), $before['value'] ?? '', false), ), ]; } /** @return array> */ public static function list(): array { $manager = self::manager(); if ($manager === null) { return []; } $out = []; foreach ($manager->settings() as $setting) { $control = self::controlFor($manager, $setting); if ($control === null || !self::offered($setting, $control)) { continue; } $section = $control->section !== '' ? $manager->get_section($control->section) : null; $value = $setting->value(); $row = [ 'id' => $setting->id, 'kind' => (string) $control->type, 'label' => wp_strip_all_tags((string) $control->label), 'section' => is_object($section) ? wp_strip_all_tags((string) $section->title) : '', 'value' => is_scalar($value) || $value === null ? $value : wp_json_encode($value), ]; if (is_array($control->choices) && $control->choices !== []) { $row['choices'] = array_map(static fn ($label): string => wp_strip_all_tags((string) $label), $control->choices); } $out[] = $row; } return $out; } private static function preview(string $id, mixed $value): array { $manager = self::manager(); $setting = $manager?->get_setting($id); $control = $setting !== null ? self::controlFor($manager, $setting) : null; if ($setting === null || $control === null || !self::offered($setting, $control)) { return ['error' => __('The active theme has no option by that id.', 'sarcio')]; } if (!is_scalar($value) || self::refusedValue($value)) { return ['error' => __('That value is not one this option takes.', 'sarcio')]; } $before = $setting->value(); return [ 'after' => is_scalar($value) ? (string) $value : '', 'before' => is_scalar($before) ? (string) $before : '', 'summary' => sprintf( /* translators: %s: the option's label. */ __('Change the theme option “%s”', 'sarcio'), wp_strip_all_tags((string) $control->label) ?: $id, ), ]; } private static function set(string $id, mixed $value, bool $journal = true): array { $manager = self::manager(); $setting = $manager?->get_setting($id); $control = $setting !== null ? self::controlFor($manager, $setting) : null; if ($manager === null || $setting === null || $control === null || !self::offered($setting, $control)) { return ['error' => __('The active theme has no option by that id.', 'sarcio')]; } if (!is_scalar($value) || (is_string($value) && strlen($value) > self::MAX_VALUE) || self::refusedValue($value)) { return ['error' => __('That value is not one this option takes.', 'sarcio')]; } $validity = $setting->validate($value); if (is_wp_error($validity)) { return ['error' => wp_strip_all_tags($validity->get_error_message())]; } $sanitized = $setting->sanitize($value); if ($sanitized === null || is_wp_error($sanitized)) { return ['error' => __('The theme did not accept that value.', 'sarcio')]; } $before = $setting->value(); if ($before === $sanitized) { return ['error' => __('Nothing would change.', 'sarcio')]; } $manager->set_post_value($id, $value); if ($setting->save() === false) { return ['error' => __('WordPress could not save that option.', 'sarcio')]; } $output = ['saved' => true, 'value' => $setting->value()]; if ($journal) { $output['journal'] = [ 'after' => ['id' => $id, 'value' => $sanitized], 'before' => ['id' => $id, 'value' => $before], 'summary' => sprintf( /* translators: %s: the option's label. */ __('Changed the theme option “%s”', 'sarcio'), wp_strip_all_tags((string) $control->label) ?: $id, ), 'target' => 'theme_mod:' . $id, ]; } return $output; } private const CONTROL_TYPES = ['checkbox', 'color', 'cropped_image', 'dropdown-pages', 'email', 'image', 'media', 'number', 'radio', 'range', 'select', 'site_icon', 'text', 'url']; private const REFUSED_NAMES = '~css|script|javascript|\\bjs\\b|_js|js_|code|html|analytics|tracking|pixel|embed~i'; private static function offered(\WP_Customize_Setting $setting, \WP_Customize_Control $control): bool { if ($setting->type !== 'theme_mod' || str_starts_with($setting->id, 'custom_css') || !$setting->check_capabilities()) { return false; } if ($control instanceof \WP_Customize_Code_Editor_Control || !in_array((string) $control->type, self::CONTROL_TYPES, true)) { return false; } if (property_exists($control, 'code_type') && $control->code_type !== '') { return false; } return !preg_match(self::REFUSED_NAMES, $setting->id . ' ' . wp_strip_all_tags((string) $control->label)); } private static function refusedValue(mixed $value): bool { return is_string($value) && (preg_match('~[<>{}]~', $value) === 1 || stripos($value, 'javascript:') !== false); } private static function controlFor(\WP_Customize_Manager $manager, \WP_Customize_Setting $setting): ?\WP_Customize_Control { foreach ($manager->controls() as $control) { foreach ($control->settings as $linked) { if (is_object($linked) && $linked->id === $setting->id) { return $control; } } } return null; } private static function manager(): ?\WP_Customize_Manager { if (!class_exists('WP_Customize_Manager')) { if (!defined('ABSPATH') || !defined('WPINC') || !is_readable(ABSPATH . WPINC . '/class-wp-customize-manager.php')) { return null; } require_once ABSPATH . WPINC . '/class-wp-customize-manager.php'; } $noComponents = static fn (): array => []; add_filter('customize_loaded_components', $noComponents, 1000); $manager = new \WP_Customize_Manager(); remove_filter('customize_loaded_components', $noComponents, 1000); do_action('customize_register', $manager); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress's own Customizer registration, so the theme adds its options. return $manager; } }