first commit

This commit is contained in:
2025-08-04 16:00:02 +02:00
commit 807c9b262f
7069 changed files with 2784666 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Parsers;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Parse_Errors {
/**
* @var array<array{key: string, error: string}>
*/
private array $errors = [];
public static function make() {
return new static();
}
public function add( string $key, string $error ): self {
$this->errors[] = [
'key' => $key,
'error' => $error,
];
return $this;
}
public function is_empty(): bool {
return empty( $this->errors );
}
public function all(): array {
return $this->errors;
}
public function to_string(): string {
$errors = [];
foreach ( $this->errors as $error ) {
$errors[] = $error['key'] . ': ' . $error['error'];
}
return implode( ', ', $errors );
}
public function merge( Parse_Errors $errors, ?string $prefix = null ): self {
foreach ( $errors->all() as $error ) {
$new_key = $prefix ? "{$prefix}.{$error['key']}" : $error['key'];
$this->add( $new_key, $error['error'] );
}
return $this;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Parsers;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Parse_Result {
private Parse_Errors $errors;
private $value;
public static function make() {
return new static();
}
public function __construct() {
$this->errors = Parse_Errors::make();
}
public function wrap( $value ): self {
$this->value = $value;
return $this;
}
public function unwrap() {
return $this->value;
}
public function is_valid(): bool {
return $this->errors->is_empty();
}
public function errors(): Parse_Errors {
return $this->errors;
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Parsers;
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Props_Parser {
private array $schema;
public function __construct( array $schema ) {
$this->schema = $schema;
}
public static function make( array $schema ): self {
return new static( $schema );
}
/**
* @param array $props
* The key of each item represents the prop name (should match the schema),
* and the value is the prop value to validate
*/
public function validate( array $props ): Parse_Result {
$result = Parse_Result::make();
$validated = [];
foreach ( $this->schema as $key => $prop_type ) {
if ( ! ( $prop_type instanceof Prop_Type ) ) {
continue;
}
$value = $props[ $key ] ?? null;
$is_valid = $prop_type->validate( $value ?? $prop_type->get_default() );
if ( ! $is_valid ) {
$result->errors()->add( $key, 'invalid_value' );
continue;
}
if ( ! is_null( $value ) ) {
$validated[ $key ] = $value;
}
}
return $result->wrap( $validated );
}
/**
* @param array $props
* The key of each item represents the prop name (should match the schema),
* and the value is the prop value to sanitize
*/
public function sanitize( array $props ): Parse_Result {
$sanitized = [];
foreach ( $this->schema as $key => $prop_type ) {
if ( ! isset( $props[ $key ] ) ) {
continue;
}
$sanitized[ $key ] = $prop_type->sanitize( $props[ $key ] );
}
return Parse_Result::make()->wrap( $sanitized );
}
/**
* @param array $props
* The key of each item represents the prop name (should match the schema),
* and the value is the prop value to parse
*/
public function parse( array $props ): Parse_Result {
$validate_result = $this->validate( $props );
$sanitize_result = $this->sanitize( $validate_result->unwrap() );
$sanitize_result->errors()->merge( $validate_result->errors() );
return $sanitize_result;
}
}

View File

@@ -0,0 +1,233 @@
<?php
namespace Elementor\Modules\AtomicWidgets\Parsers;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use Elementor\Modules\AtomicWidgets\Module;
use Elementor\Plugin;
class Style_Parser {
const VALID_TYPES = [
'class',
];
const VALID_STATES = [
'hover',
'active',
'focus',
null,
];
private array $schema;
public function __construct( array $schema ) {
$this->schema = $schema;
}
public static function make( array $schema ): self {
return new static( $schema );
}
/**
* @param array $style
* the style object to validate
*/
private function validate( array $style ): Parse_Result {
$validated_style = $style;
$result = Parse_Result::make();
if ( ! isset( $style['id'] ) || ! is_string( $style['id'] ) ) {
$result->errors()->add( 'id', 'missing_or_invalid' );
}
if ( ! isset( $style['type'] ) || ! in_array( $style['type'], self::VALID_TYPES, true ) ) {
$result->errors()->add( 'type', 'missing_or_invalid' );
}
if ( ! isset( $style['label'] ) || ! is_string( $style['label'] ) ) {
$result->errors()->add( 'label', 'missing_or_invalid' );
} elseif ( Plugin::$instance->experiments->is_feature_active( Module::EXPERIMENT_VERSION_3_30 ) ) {
$label_validation = $this->validate_style_label( $style['label'] );
if ( ! $label_validation['is_valid'] ) {
$result->errors()->add( 'label', $label_validation['error_message'] );
}
}
if ( ! isset( $style['variants'] ) || ! is_array( $style['variants'] ) ) {
$result->errors()->add( 'variants', 'missing_or_invalid' );
unset( $validated_style['variants'] );
return $result->wrap( $validated_style );
}
$props_parser = Props_Parser::make( $this->schema );
foreach ( $style['variants'] as $variant_index => $variant ) {
if ( ! isset( $variant['meta'] ) ) {
$result->errors()->add( 'meta', 'missing' );
continue;
}
$meta_result = $this->validate_meta( $variant['meta'] );
$result->errors()->merge( $meta_result->errors(), 'meta' );
if ( $meta_result->is_valid() ) {
$variant_result = $props_parser->validate( $variant['props'] );
$result->errors()->merge( $variant_result->errors(), "variants[$variant_index]" );
$validated_style['variants'][ $variant_index ]['props'] = $variant_result->unwrap();
} else {
unset( $validated_style['variants'][ $variant_index ] );
}
}
return $result->wrap( $validated_style );
}
private function validate_style_label( string $label ): array {
$label = strtolower( $label );
$reserved_class_names = [ 'container' ];
if ( strlen( $label ) > 50 ) {
return [
'is_valid' => false,
'error_message' => 'class_name_too_long',
];
}
if ( strlen( $label ) < 2 ) {
return [
'is_valid' => false,
'error_message' => 'class_name_too_short',
];
}
if ( in_array( $label, $reserved_class_names, true ) ) {
return [
'is_valid' => false,
'error_message' => 'reserved_class_name',
];
}
$regexes = [
[
'pattern' => '/^(|[^0-9].*)$/',
'message' => 'class_name_starts_with_digit',
],
[
'pattern' => '/^\S*$/',
'message' => 'class_name_contains_spaces',
],
[
'pattern' => '/^(|[a-zA-Z0-9_-]+)$/',
'message' => 'class_name_invalid_chars',
],
[
'pattern' => '/^(?!--).*/',
'message' => 'class_name_double_hyphen',
],
[
'pattern' => '/^(?!-[0-9])/',
'message' => 'class_name_starts_with_hyphen_digit',
],
];
foreach ( $regexes as $rule ) {
if ( ! preg_match( $rule['pattern'], $label ) ) {
return [
'is_valid' => false,
'error_message' => $rule['message'],
];
}
}
return [
'is_valid' => true,
'error_message' => null,
];
}
private function validate_meta( $meta ): Parse_Result {
$result = Parse_Result::make();
if ( ! is_array( $meta ) ) {
$result->errors()->add( 'meta', 'invalid_type' );
return $result;
}
if ( ! array_key_exists( 'state', $meta ) || ! in_array( $meta['state'], self::VALID_STATES, true ) ) {
$result->errors()->add( 'state', 'missing_or_invalid_value' );
return $result;
}
// TODO: Validate breakpoint based on the existing breakpoints in the system [EDS-528]
if ( ! isset( $meta['breakpoint'] ) || ! is_string( $meta['breakpoint'] ) ) {
$result->errors()->add( 'breakpoint', 'missing_or_invalid_value' );
return $result;
}
return $result;
}
private function sanitize_meta( $meta ) {
if ( ! is_array( $meta ) ) {
return [];
}
if ( isset( $meta['breakpoint'] ) ) {
$meta['breakpoint'] = sanitize_key( $meta['breakpoint'] );
}
return $meta;
}
/**
* @param array $style
* the style object to sanitize
*/
private function sanitize( array $style ): Parse_Result {
$props_parser = Props_Parser::make( $this->schema );
if ( isset( $style['label'] ) ) {
$style['label'] = sanitize_text_field( $style['label'] );
}
if ( isset( $style['id'] ) ) {
$style['id'] = sanitize_key( $style['id'] );
}
if ( ! empty( $style['variants'] ) ) {
foreach ( $style['variants'] as $variant_index => $variant ) {
$style['variants'][ $variant_index ]['props'] = $props_parser->sanitize( $variant['props'] )->unwrap();
$style['variants'][ $variant_index ]['meta'] = $this->sanitize_meta( $variant['meta'] );
}
}
return Parse_Result::make()->wrap( $style );
}
/**
* @param array $style
* the style object to parse
*/
public function parse( array $style ): Parse_Result {
$validate_result = $this->validate( $style );
$sanitize_result = $this->sanitize( $validate_result->unwrap() );
$sanitize_result->errors()->merge( $validate_result->errors() );
return $sanitize_result;
}
}