PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.0-beta3
Elementor Website Builder – more than just a page builder v4.3.0-beta3
4.3.0-beta3 4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 4.1.0-beta1 4.1.0-dev1 All 452 releases
elementor / modules / mcp / abilities / build-composition / form-structure-validator.php

form-structure-validator.php in Elementor Website Builder – more than just a page builder 4.3.0-beta3, at modules/mcp/abilities/build-composition/form-structure-validator.php

233 lines 6.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Elementor\Modules\Mcp\Abilities\Build_Composition;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9 class Form_Structure_Validator {
10
11 const FORM_ELEMENT_TYPE = 'e-form';
12
13 const FORM_FIELD_ELEMENT_TYPES = [
14 'e-form-input',
15 'e-form-textarea',
16 'e-form-label',
17 'e-form-checkbox',
18 'e-form-submit-button',
19 'e-form-select',
20 'e-form-radio-button',
21 'e-form-file-upload',
22 'e-form-date-picker',
23 'e-form-time-picker',
24 ];
25
26 private const DEFAULT_PARENT_ID = 'document';
27
28 private Xml_Parser $xml_parser;
29
30 public function __construct( ?Xml_Parser $xml_parser = null ) {
31 $this->xml_parser = $xml_parser ?? new Xml_Parser();
32 }
33
34 /**
35 * @return \WP_Error|null
36 */
37 public function validate( \DOMDocument $dom, array $document_tree, string $parent_id ) {
38 $errors = $this->collect_errors( $dom, $document_tree, $parent_id );
39
40 if ( empty( $errors ) ) {
41 return null;
42 }
43
44 return new \WP_Error(
45 'elementor_invalid_form_structure',
46 implode( ' ', $errors ),
47 [ 'status' => \WP_Http::BAD_REQUEST ]
48 );
49 }
50
51 /**
52 * @return string[]
53 */
54 public function collect_errors( \DOMDocument $dom, array $document_tree, string $parent_id ): array {
55 $parent_within_form = $this->is_parent_within_existing_form( $document_tree, $parent_id );
56
57 return array_merge(
58 $this->collect_nested_form_errors( $dom, $parent_within_form ),
59 $this->collect_form_ancestor_errors( $dom, $parent_within_form ),
60 $this->collect_submit_button_errors( $dom ),
61 $this->collect_empty_message_errors( $dom )
62 );
63 }
64
65 /**
66 * @return string[]
67 */
68 private function collect_nested_form_errors( \DOMDocument $dom, bool $parent_within_form ): array {
69 $errors = [];
70
71 foreach ( $this->xml_parser->iterate_all_descendants( $dom ) as $node ) {
72 $tag = strtolower( $this->xml_parser->get_tag_name( $node ) );
73
74 if ( self::FORM_ELEMENT_TYPE !== $tag ) {
75 continue;
76 }
77
78 if ( ! $parent_within_form && ! $this->has_form_ancestor_in_dom( $node ) ) {
79 continue;
80 }
81
82 $errors[] = '<' . $this->xml_parser->get_tag_name( $node ) . '> cannot be nested inside another <e-form>.';
83 }
84
85 return $errors;
86 }
87
88 /**
89 * @return string[]
90 */
91 private function collect_form_ancestor_errors( \DOMDocument $dom, bool $parent_within_form ): array {
92 if ( $parent_within_form ) {
93 return [];
94 }
95
96 $errors = [];
97
98 foreach ( $this->xml_parser->iterate_all_descendants( $dom ) as $node ) {
99 $tag = strtolower( $this->xml_parser->get_tag_name( $node ) );
100
101 if ( ! in_array( $tag, self::FORM_FIELD_ELEMENT_TYPES, true ) ) {
102 continue;
103 }
104
105 if ( $this->has_form_ancestor_in_dom( $node ) ) {
106 continue;
107 }
108
109 $errors[] = $this->form_field_must_be_in_form_error(
110 $tag,
111 $this->xml_parser->get_configuration_id( $node )
112 );
113 }
114
115 return $errors;
116 }
117
118 /**
119 * @return string[]
120 */
121 private function collect_submit_button_errors( \DOMDocument $dom ): array {
122 $errors = [];
123 $xpath = new \DOMXPath( $dom );
124
125 foreach ( $xpath->query( '//' . self::FORM_ELEMENT_TYPE ) as $form ) {
126 if ( ! $form instanceof \DOMElement ) {
127 continue;
128 }
129
130 $submit_buttons = $xpath->query( './/e-form-submit-button', $form );
131 $submit_button_count = $submit_buttons ? $submit_buttons->length : 0;
132
133 if ( 0 === $submit_button_count ) {
134 $errors[] = '<e-form> has no <e-form-submit-button>.';
135 } elseif ( $submit_button_count > 1 ) {
136 $errors[] = '<e-form> has ' . $submit_button_count . ' submit buttons — only 1 is allowed.';
137 }
138 }
139
140 return $errors;
141 }
142
143 /**
144 * @return string[]
145 */
146 private function collect_empty_message_errors( \DOMDocument $dom ): array {
147 $errors = [];
148 $xpath = new \DOMXPath( $dom );
149
150 foreach ( $xpath->query( '//e-form-success-message|//e-form-error-message' ) as $node ) {
151 if ( ! $node instanceof \DOMElement ) {
152 continue;
153 }
154
155 if ( ! empty( $this->xml_parser->get_child_elements( $node ) ) ) {
156 continue;
157 }
158
159 $errors[] = '<' . $this->xml_parser->get_tag_name( $node ) . '> must have at least one child element (e.g. <e-paragraph>).';
160 }
161
162 return $errors;
163 }
164
165 private function form_field_must_be_in_form_error( string $tag, ?string $configuration_id ): string {
166 $configuration_attribute = $configuration_id ? ' configuration-id="' . $configuration_id . '"' : '';
167
168 return '<' . $tag . $configuration_attribute . '> must be nested inside <e-form> (any ancestor depth is allowed).';
169 }
170
171 private function has_form_ancestor_in_dom( \DOMElement $node ): bool {
172 $parent = $node->parentNode; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- DOMElement API.
173
174 while ( $parent instanceof \DOMElement ) {
175 if ( Xml_Parser::COMPOSITION_ROOT_TAG === strtolower( $parent->tagName ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- DOMElement API.
176 break;
177 }
178
179 if ( self::FORM_ELEMENT_TYPE === strtolower( $parent->tagName ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- DOMElement API.
180 return true;
181 }
182
183 $parent = $parent->parentNode; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- DOMElement API.
184 }
185
186 return false;
187 }
188
189 private function is_parent_within_existing_form( array $document_tree, string $parent_id ): bool {
190 if ( self::DEFAULT_PARENT_ID === $parent_id ) {
191 return false;
192 }
193
194 $context = $this->locate_node_form_context( $document_tree, $parent_id );
195
196 return $context['found'] && $context['within_form'];
197 }
198
199 /**
200 * @return array{found: bool, within_form: bool}
201 */
202 private function locate_node_form_context( array $tree, string $target_id, bool $within_form = false ): array {
203 foreach ( $tree as $node ) {
204 $node_is_form = $within_form || $this->is_form_element( $node );
205 $id = $node['id'] ?? '';
206
207 if ( $id === $target_id ) {
208 return [
209 'found' => true,
210 'within_form' => $node_is_form,
211 ];
212 }
213
214 if ( ! empty( $node['elements'] ) && is_array( $node['elements'] ) ) {
215 $result = $this->locate_node_form_context( $node['elements'], $target_id, $node_is_form );
216
217 if ( $result['found'] ) {
218 return $result;
219 }
220 }
221 }
222
223 return [
224 'found' => false,
225 'within_form' => false,
226 ];
227 }
228
229 private function is_form_element( array $node ): bool {
230 return self::FORM_ELEMENT_TYPE === ( $node['elType'] ?? '' );
231 }
232 }
233