Initial public release of BreznFlow, an n8n workflow renderer for WordPress. Fully PHPCS-compliant (WordPress Coding Standards), security-hardened, and ready for WordPress.org plugin review. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
265 lines
6.8 KiB
PHP
265 lines
6.8 KiB
PHP
<?php
|
|
/**
|
|
* Validates and stores custom theme definitions.
|
|
*
|
|
* @package BreznFlow
|
|
* @since 1.0.0
|
|
*/
|
|
|
|
namespace BreznFlow\Features;
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Handles import, validation and storage of custom theme JSON files.
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
class ThemeImporter {
|
|
|
|
/**
|
|
* All 41 allowed token names. Any key outside this list is rejected during import.
|
|
*
|
|
* @var string[]
|
|
*/
|
|
const ALLOWED_TOKENS = array(
|
|
'canvas_bg',
|
|
'node_bg',
|
|
'node_text',
|
|
'node_sub',
|
|
'node_border',
|
|
'connection',
|
|
'connection_hover',
|
|
'toolbar_bg',
|
|
'toolbar_text',
|
|
'toolbar_border',
|
|
'panel_bg',
|
|
'panel_text',
|
|
'panel_border',
|
|
'btn_bg',
|
|
'btn_text',
|
|
'btn_border',
|
|
'btn_hover_bg',
|
|
'action_bar_bg',
|
|
'action_bar_border',
|
|
'modal_overlay_bg',
|
|
'modal_bg',
|
|
'modal_border',
|
|
'modal_title',
|
|
'modal_text',
|
|
'modal_sub',
|
|
'modal_close',
|
|
'modal_secondary_bg',
|
|
'modal_secondary_border',
|
|
'modal_code_bg',
|
|
'tooltip_bg',
|
|
'tooltip_text',
|
|
'fullscreen_overlay_bg',
|
|
'minimap_bg',
|
|
'minimap_border',
|
|
'color_trigger',
|
|
'color_http',
|
|
'color_code',
|
|
'color_logic',
|
|
'color_database',
|
|
'color_ai',
|
|
'color_fallback',
|
|
);
|
|
|
|
/** IDs reserved for built-in themes — custom themes must not use these. */
|
|
const BUILTIN_IDS = array( 'dark', 'light', 'minimal', 'tech', 'brezn' );
|
|
|
|
/**
|
|
* Validates a decoded theme array.
|
|
*
|
|
* @param array $data Decoded JSON as PHP array.
|
|
* @return true|\WP_Error
|
|
*/
|
|
public static function validate( array $data ) {
|
|
// Required top-level fields.
|
|
foreach ( array( 'name', 'id', 'version', 'tokens' ) as $field ) {
|
|
if ( ! isset( $data[ $field ] ) ) {
|
|
return new \WP_Error(
|
|
'missing_field',
|
|
/* translators: %s: field name */
|
|
sprintf( __( 'Missing required field: %s', 'breznflow' ), $field )
|
|
);
|
|
}
|
|
}
|
|
|
|
// No extra top-level fields allowed.
|
|
$extra = array_diff( array_keys( $data ), array( 'name', 'id', 'version', 'tokens' ) );
|
|
if ( ! empty( $extra ) ) {
|
|
return new \WP_Error(
|
|
'extra_fields',
|
|
/* translators: %s: comma-separated field names */
|
|
sprintf( __( 'Unexpected fields: %s', 'breznflow' ), implode( ', ', $extra ) )
|
|
);
|
|
}
|
|
|
|
// id: must sanitize cleanly and not collide with built-ins.
|
|
$id = sanitize_key( (string) $data['id'] );
|
|
if ( '' === $id || $id !== (string) $data['id'] ) {
|
|
return new \WP_Error(
|
|
'invalid_id',
|
|
__( 'Theme ID must contain only lowercase letters, numbers, and hyphens.', 'breznflow' )
|
|
);
|
|
}
|
|
if ( in_array( $id, self::BUILTIN_IDS, true ) ) {
|
|
return new \WP_Error(
|
|
'reserved_id',
|
|
/* translators: %s: theme ID */
|
|
sprintf( __( 'Theme ID "%s" is reserved for built-in themes.', 'breznflow' ), $id )
|
|
);
|
|
}
|
|
|
|
// name: non-empty string, max 80 chars.
|
|
$name = sanitize_text_field( (string) $data['name'] );
|
|
if ( '' === $name ) {
|
|
return new \WP_Error( 'invalid_name', __( 'Theme name must not be empty.', 'breznflow' ) );
|
|
}
|
|
if ( mb_strlen( $name ) > 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;
|
|
}
|
|
}
|