PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.3
Elementor Website Builder – more than just a page builder v4.3.3
4.3.3 4.3.2 4.3.1 4.3.0 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 All 456 releases
elementor / modules / mcp / abilities / build-composition / xml-parser.php

xml-parser.php in Elementor Website Builder – more than just a page builder 4.3.3, at modules/mcp/abilities/build-composition/xml-parser.php

159 lines 4.4 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 Xml_Parser {
10
11 const CONFIGURATION_ID_ATTRIBUTE = 'configuration-id';
12 const COMPOSITION_ROOT_TAG = 'composition-root';
13
14 public function parse( string $xml_structure ) {
15 $wrapped = '<' . self::COMPOSITION_ROOT_TAG . '>' . $xml_structure . '</' . self::COMPOSITION_ROOT_TAG . '>';
16
17 $previous = libxml_use_internal_errors( true );
18 $dom = new \DOMDocument();
19 $loaded = $dom->loadXML( $wrapped, LIBXML_NONET | LIBXML_NOENT );
20 $errors = libxml_get_errors();
21 libxml_clear_errors();
22 libxml_use_internal_errors( $previous );
23
24 if ( ! $loaded ) {
25 $message = $errors ? $this->describe_libxml_error( trim( $errors[0]->message ) ) : 'Unknown XML error.';
26 return new \WP_Error(
27 'invalid_xml',
28 /* translators: %s: XML parse error message */
29 sprintf( __( 'Failed to parse xml_structure: %s', 'elementor' ), $message ),
30 [ 'status' => \WP_Http::BAD_REQUEST ]
31 );
32 }
33
34 return $dom;
35 }
36
37 private function describe_libxml_error( string $message ): string {
38 $root = preg_quote( self::COMPOSITION_ROOT_TAG, '/' );
39
40 if ( preg_match( '/tag mismatch: ' . $root . ' line \d+ and (\S+)$/', $message, $matches ) ) {
41 /* translators: %s: XML tag name */
42 return sprintf( __( 'Closing tag </%s> has no matching opening tag.', 'elementor' ), $matches[1] );
43 }
44
45 if ( preg_match( '/tag mismatch: (\S+) line \d+ and ' . $root . '$/', $message, $matches ) ) {
46 /* translators: %s: XML tag name */
47 return sprintf( __( 'Element <%s> is not closed.', 'elementor' ), $matches[1] );
48 }
49
50 return $message;
51 }
52
53 public function get_root( \DOMDocument $dom ): ?\DOMElement {
54 return $dom->documentElement ?? null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- DOMDocument API.
55 }
56
57 /**
58 * @return \DOMElement[]
59 */
60 public function get_child_elements( \DOMElement $node ): array {
61 $children = [];
62 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- DOMElement API.
63 foreach ( $node->childNodes as $child ) {
64 if ( $child instanceof \DOMElement ) {
65 $children[] = $child;
66 }
67 }
68 return $children;
69 }
70
71 public function get_tag_name( \DOMElement $node ): string {
72 return $node->tagName; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- DOMElement API.
73 }
74
75 public function get_configuration_id( \DOMElement $node ): ?string {
76 return $node->hasAttribute( self::CONFIGURATION_ID_ATTRIBUTE )
77 ? $node->getAttribute( self::CONFIGURATION_ID_ATTRIBUTE )
78 : null;
79 }
80
81 public function serialize_children( \DOMDocument $dom ): string {
82 $root = $this->get_root( $dom );
83 if ( ! $root ) {
84 return '';
85 }
86
87 $serialized = '';
88 foreach ( $this->get_child_elements( $root ) as $child ) {
89 $serialized .= $dom->saveXML( $child );
90 }
91 return $serialized;
92 }
93
94 /**
95 * @return \DOMElement[]
96 */
97 public function iterate_all_descendants( \DOMDocument $dom ): array {
98 $root = $this->get_root( $dom );
99 if ( ! $root ) {
100 return [];
101 }
102
103 $descendants = [];
104 $xpath = new \DOMXPath( $dom );
105 foreach ( $xpath->query( './/*', $root ) as $node ) {
106 if ( $node instanceof \DOMElement ) {
107 $descendants[] = $node;
108 }
109 }
110 return $descendants;
111 }
112
113 public function collect_duplicate_configuration_id_errors( \DOMDocument $dom ): array {
114 $counts = [];
115
116 foreach ( $this->iterate_all_descendants( $dom ) as $node ) {
117 $configuration_id = $this->get_configuration_id( $node );
118 if ( null === $configuration_id || '' === $configuration_id ) {
119 continue;
120 }
121
122 if ( ! isset( $counts[ $configuration_id ] ) ) {
123 $counts[ $configuration_id ] = 0;
124 }
125
126 $counts[ $configuration_id ]++;
127 }
128
129 $errors = [];
130 foreach ( $counts as $configuration_id => $count ) {
131 if ( $count <= 1 ) {
132 continue;
133 }
134
135 $errors[] = sprintf(
136 /* translators: %s: duplicate configuration-id value */
137 __( 'Duplicate configuration-id "%s". Each element must have a unique configuration-id.', 'elementor' ),
138 $configuration_id
139 );
140 }
141
142 return $errors;
143 }
144
145 public function validate_unique_configuration_ids( \DOMDocument $dom ): ?\WP_Error {
146 $errors = $this->collect_duplicate_configuration_id_errors( $dom );
147
148 if ( empty( $errors ) ) {
149 return null;
150 }
151
152 return new \WP_Error(
153 'elementor_duplicate_configuration_id',
154 implode( ' ', $errors ),
155 [ 'status' => \WP_Http::BAD_REQUEST ]
156 );
157 }
158 }
159