80 ) { return new \WP_Error( 'invalid_name', __( 'Theme name must be 80 characters or fewer.', 'breznflow' ) ); } // version: must be an integer. if ( ! is_int( $data['version'] ) && ! ctype_digit( (string) $data['version'] ) ) { return new \WP_Error( 'invalid_version', __( 'Theme version must be an integer.', 'breznflow' ) ); } // tokens: must be an array. if ( ! is_array( $data['tokens'] ) ) { return new \WP_Error( 'invalid_tokens', __( 'Tokens must be an object.', 'breznflow' ) ); } // Tokens: no extra keys. $token_keys = array_keys( $data['tokens'] ); $extra_tokens = array_diff( $token_keys, self::ALLOWED_TOKENS ); if ( ! empty( $extra_tokens ) ) { return new \WP_Error( 'unknown_tokens', /* translators: %s: comma-separated token names */ sprintf( __( 'Unknown tokens: %s', 'breznflow' ), implode( ', ', $extra_tokens ) ) ); } // Tokens: all 41 must be present. $missing = array_diff( self::ALLOWED_TOKENS, $token_keys ); if ( ! empty( $missing ) ) { return new \WP_Error( 'missing_tokens', /* translators: %s: comma-separated token names */ sprintf( __( 'Missing tokens: %s', 'breznflow' ), implode( ', ', $missing ) ) ); } // Each token value must be a safe color expression. foreach ( $data['tokens'] as $token => $value ) { if ( ! self::validate_color_value( (string) $value ) ) { return new \WP_Error( 'invalid_color', /* translators: 1: token name, 2: value */ sprintf( __( 'Invalid color value for "%1$s": %2$s', 'breznflow' ), $token, $value ) ); } } return true; } /** * Validates and stores a theme in wp_options. * If a theme with the same ID already exists it is replaced. * * @param array $data Decoded JSON as PHP array. * @return true|\WP_Error */ public static function import( array $data ) { $result = self::validate( $data ); if ( is_wp_error( $result ) ) { return $result; } $themes = get_option( 'breznflow_custom_themes', array() ); // Remove existing entry with the same ID. foreach ( $themes as $i => $t ) { if ( sanitize_key( $data['id'] ) === $t['id'] ) { array_splice( $themes, $i, 1 ); break; } } $themes[] = array( 'id' => sanitize_key( $data['id'] ), 'name' => sanitize_text_field( $data['name'] ), 'version' => (int) $data['version'], 'tokens' => $data['tokens'], ); update_option( 'breznflow_custom_themes', $themes ); return true; } /** * Deletes a custom theme by ID. * * @param string $id Theme ID. * @return bool True if deleted, false if not found. */ public static function delete( string $id ): bool { $id = sanitize_key( $id ); $themes = get_option( 'breznflow_custom_themes', array() ); $new = array_values( array_filter( $themes, fn( $t ) => $t['id'] !== $id ) ); if ( count( $new ) === count( $themes ) ) { return false; } update_option( 'breznflow_custom_themes', $new ); return true; } /** * Returns all stored custom themes. * * @return array[] */ public static function get_all(): array { return get_option( 'breznflow_custom_themes', array() ); } /** * Validates a single CSS color value. * Accepts: #rgb, #rrggbb, #rrggbbaa, rgb(...), rgba(...) * Rejects: url(...), @import, semicolons, curly braces, etc. * * @param string $value Raw token value. * @return bool */ private static function validate_color_value( string $value ): bool { $value = trim( $value ); // Hex: #rgb, #rrggbb, #rrggbbaa. if ( preg_match( '/^#[0-9a-fA-F]{3}([0-9a-fA-F]{3}([0-9a-fA-F]{2})?)?$/', $value ) ) { return true; } // rgb(r, g, b). if ( preg_match( '/^rgb\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\)$/', $value ) ) { return true; } // rgba(r, g, b, a) — alpha must be 0..1 decimal. if ( preg_match( '/^rgba\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*(0(\.\d+)?|1(\.0+)?)\s*\)$/', $value ) ) { return true; } return false; } }