*/ public static function abilities(): array { $allowed = static fn (array $args): bool => current_user_can('edit_theme_options'); return [ new Ability( 'sarcio/css-get', __('Look at the extra styles', 'sarcio'), Ability::READ, 'The site’s Additional CSS (the Customizer’s extra stylesheet for the active theme).', ['properties' => [], 'type' => 'object'], $allowed, static fn (array $args): array => ['css' => (string) wp_get_custom_css()], ), new Ability( 'sarcio/styles-get', __('Look at the site styles', 'sarcio'), Ability::READ, 'The site’s Global Styles as the owner set them in the Site Editor: the settings and styles objects (colors, typography, spacing) in theme.json form. Empty when nothing was customised.', ['properties' => [], 'type' => 'object'], $allowed, static fn (array $args): array => self::styles(), ), new Ability( 'sarcio/styles-update', __('Change the site styles', 'sarcio'), Ability::WRITE, 'Merge the given settings and styles objects (theme.json form, e.g. styles.color.background or styles.typography.fontSize) into the site’s Global Styles. Keys not given are kept. WordPress keeps a revision. No css keys: CSS text is refused.', [ 'properties' => [ 'settings' => ['additionalProperties' => true, 'type' => 'object'], 'styles' => ['additionalProperties' => true, 'type' => 'object'], ], 'required' => [], 'type' => 'object', ], $allowed, static fn (array $args): array => self::updateStyles($args), null, static fn (array $before, array $after): array => self::writeStyles($before) + ['restored' => true], false, static fn (array $args, Overlay $overlay): array => self::stageStyles($args, $overlay), ), ]; } /** Whether a styles or settings patch carries custom CSS text anywhere (styles.css, styles.blocks.*.css). */ public static function carriesCss(mixed $value): bool { if (!is_array($value)) { return false; } foreach ($value as $key => $item) { if ($key === 'css' || self::carriesCss($item)) { return true; } } return false; } /** @param array $value */ private static function isList(array $value): bool { return $value === [] || array_keys($value) === range(0, count($value) - 1); } /** * @param array $base * @param array $patch */ public static function merge(array $base, array $patch): array { foreach ($patch as $key => $value) { if (is_array($value) && isset($base[$key]) && is_array($base[$key]) && !self::isList($value)) { $base[$key] = self::merge($base[$key], $value); } else { $base[$key] = $value; } } return $base; } /** @return array{id:int,settings:array,styles:array,raw:array} */ private static function current(?Overlay $overlay = null): array { $id = class_exists('WP_Theme_JSON_Resolver') ? (int) \WP_Theme_JSON_Resolver::get_user_global_styles_post_id() : 0; $post = $id > 0 ? get_post($id) : null; $raw = $post !== null ? json_decode((string) $post->post_content, true) : null; $raw = is_array($raw) ? $raw : []; $staged = $overlay?->globalStyles; return [ 'id' => $id, 'raw' => $raw, 'settings' => $staged !== null ? $staged['settings'] : (is_array($raw['settings'] ?? null) ? $raw['settings'] : []), 'styles' => $staged !== null ? $staged['styles'] : (is_array($raw['styles'] ?? null) ? $raw['styles'] : []), ]; } private static function styles(): array { $current = self::current(Stage::active() ? Stage::overlay() : null); return ['settings' => $current['settings'], 'styles' => $current['styles']]; } /** @param array $args */ private static function stageStyles(array $args, Overlay $overlay): array { if (self::carriesCss($args)) { return ['error' => __('Site styles take colours, fonts and spacing, not CSS text.', 'sarcio')]; } $current = self::current($overlay); if ($current['id'] <= 0) { return ['error' => __('This theme does not support site styles.', 'sarcio')]; } $before = ['settings' => $current['settings'], 'styles' => $current['styles']]; $after = [ 'settings' => self::merge($current['settings'], (array) ($args['settings'] ?? [])), 'styles' => self::merge($current['styles'], (array) ($args['styles'] ?? [])), ]; if ($after === $before) { return ['error' => __('Nothing would change.', 'sarcio')]; } $overlay->globalStyles = $after; $patch = array_filter(['settings' => $args['settings'] ?? [], 'styles' => $args['styles'] ?? []]); return [ 'after' => (string) wp_json_encode($patch, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), 'before' => (string) wp_json_encode(self::pick($before, $patch), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), 'summary' => __('Change the site styles', 'sarcio'), ]; } /** @param array $args */ private static function updateStyles(array $args): array { if (self::carriesCss($args)) { return ['error' => __('Site styles take colours, fonts and spacing, not CSS text.', 'sarcio')]; } $current = self::current(); if ($current['id'] <= 0) { return ['error' => __('This theme does not support site styles.', 'sarcio')]; } $before = ['settings' => $current['settings'], 'styles' => $current['styles']]; $after = [ 'settings' => self::merge($current['settings'], (array) ($args['settings'] ?? [])), 'styles' => self::merge($current['styles'], (array) ($args['styles'] ?? [])), ]; if ($after === $before) { return ['error' => __('Nothing would change.', 'sarcio')]; } $written = self::writeStyles($after); if (isset($written['error'])) { return $written; } return [ 'journal' => [ 'after' => $after, 'before' => $before, 'summary' => __('Changed the site styles', 'sarcio'), 'target' => 'global_styles', ], 'saved' => true, ]; } /** @param array $fields */ private static function writeStyles(array $fields): array { $current = self::current(); if ($current['id'] <= 0) { return ['error' => __('This theme does not support site styles.', 'sarcio')]; } $raw = $current['raw']; $raw['isGlobalStylesUserThemeJSON'] = true; $raw['version'] = $raw['version'] ?? (class_exists('WP_Theme_JSON') ? \WP_Theme_JSON::LATEST_SCHEMA : 3); $raw['settings'] = (array) ($fields['settings'] ?? []); $raw['styles'] = (array) ($fields['styles'] ?? []); $result = wp_update_post([ 'ID' => $current['id'], 'post_content' => (string) wp_json_encode($raw, JSON_UNESCAPED_SLASHES), ], true); if (is_wp_error($result) || (int) $result <= 0) { return ['error' => __('WordPress could not save the site styles.', 'sarcio')]; } return ['id' => $current['id']]; } /** * The current values of exactly the keys a patch touches, so a before block * shows what changes and not the whole stylesheet. * * @param array $current * @param array $patch */ private static function pick(array $current, array $patch): array { $out = []; foreach ($patch as $key => $value) { if (!array_key_exists($key, $current)) { continue; } $out[$key] = is_array($value) && is_array($current[$key]) && !self::isList($value) ? self::pick($current[$key], $value) : $current[$key]; } return $out; } }