PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.4
Elementor Website Builder – more than just a page builder v4.3.4
4.3.4 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 All 457 releases
elementor / modules / mcp / abilities / get-structure-ability.php

get-structure-ability.php in Elementor Website Builder – more than just a page builder 4.3.4, at modules/mcp/abilities/get-structure-ability.php

383 lines 13.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;
4
5 use Elementor\Modules\AtomicWidgets\PropsResolver\Render_Props_Resolver;
6 use Elementor\Modules\AtomicWidgets\Styles\Local_Style_Serializer;
7 use Elementor\Modules\AtomicWidgets\Utils\Element_Structure_Title;
8 use Elementor\Modules\DefaultStyles\Default_Styles_Repository;
9 use Elementor\Modules\GlobalClasses\Utils\Atomic_Elements_Utils;
10 use Elementor\Modules\Interactions\Props\Interaction_Item_Prop_Type;
11 use Elementor\Modules\Mcp\Abilities\Appliers\V3_Node_Bridge;
12 use Elementor\Modules\Mcp\Abilities\Appliers\V3\V3_Non_Style_Allowlist;
13 use Elementor\Modules\Mcp\Abilities\Appliers\V3\V3_Style_Serializer;
14 use Elementor\Modules\Mcp\Abilities\Appliers\V3\V3_Widget_Bridge_Registry;
15 use Elementor\Modules\Mcp\Abilities\Utils\Atomic_Container_Presentation;
16 use Elementor\Modules\Mcp\Abilities\Utils\Element_Default_Styles_Builder;
17 use Elementor\Modules\Mcp\Abilities\Utils\Element_Tag_Resolver;
18 use Elementor\Modules\Mcp\Abilities\Utils\Widget_Context_Helper;
19 use Elementor\Plugin;
20 use Elementor\Utils;
21
22 if ( ! defined( 'ABSPATH' ) ) {
23 exit;
24 }
25
26 class Get_Structure_Ability extends Abstract_Ability {
27
28 private ?Default_Styles_Repository $default_styles_repository;
29
30 public function __construct( ?Default_Styles_Repository $default_styles_repository = null ) {
31 $this->default_styles_repository = $default_styles_repository;
32 }
33
34 protected function get_ability_id(): string {
35 return 'elementor/get-page-structure';
36 }
37
38 protected function get_definition(): Ability_Definition {
39 return new Ability_Definition(
40 __( 'Get Elementor Page Structure', 'elementor' ),
41 __( 'Returns a lean Elementor element tree skeleton (id, elType, widgetType, version, title, nested elements) for a single post or page ID. Each node is tagged with version=3 (legacy) or version=4 (atomic). Only version=4 nodes can be modified via elementor/manage-elements or referenced by elementor/build-composition element_config; version=3 nodes are returned for context only and must be edited directly in the Elementor editor. Optionally scope to a subtree via element_id. Set include_content=true (requires element_id) to also return each V4 node\'s settings in the plain shape that manage-elements.update.settings / build-composition.element_config accept, so a value can be sent back as-is, styles (as { __style_id, css } where css is a raw CSS string round-trippable to manage-elements.update.style / build-composition.style in replace mode), interactions, its rendered HTML tag when known, and default_styles (a raw CSS string of the widget base layer followed by the kit\'s site-wide default layer for that tag, in browser cascade order — includes selectors, @media(--breakpoint) blocks, and pseudo-states as the frontend renders them). V3 nodes are returned with empty settings and styles. Only works for posts that were saved with Elementor.', 'elementor' ),
42 'elementor',
43 [
44 'type' => 'object',
45 'properties' => [
46 'elements' => [
47 'type' => 'array',
48 'description' => 'Skeleton of Elementor elements (id, elType, widgetType, version, title, nested elements). When include_content is true, V4 nodes also include settings, styles (as { __style_id, css } — raw CSS string with @media(--breakpoint) + &:hover/&:focus/&:active), interactions, tag (rendered HTML wrapper tag when known), and default_styles (raw CSS string, in cascade order: `.e-con` shell styles of atomic elements, including document-root rules for top-level elements + widget base layer + kit site-wide default for that tag). V3 nodes always have empty settings and styles.',
49 ],
50 ],
51 ],
52 [
53 'annotations' => [
54 'readonly' => true,
55 'idempotent' => true,
56 'destructive' => false,
57 ],
58 ],
59 function () {
60 return current_user_can( 'edit_posts' );
61 },
62 [
63 'type' => 'object',
64 'required' => [ 'post_id' ],
65 'properties' => [
66 'post_id' => [
67 'type' => 'integer',
68 'description' => 'WordPress post ID of the Elementor document.',
69 ],
70 'element_id' => [
71 'type' => 'string',
72 'description' => 'Optional. If provided, returns only the subtree rooted at that element id.',
73 ],
74 'include_content' => [
75 'type' => 'boolean',
76 'default' => false,
77 'description' => 'If true, includes each V4 node\'s settings (plain shape, writable as-is), styles (as { __style_id, css } — raw CSS string), interactions, rendered tag, and default_styles (raw CSS string: base layer + kit default for that tag). The styles.css value is round-trippable to build-composition.style / manage-elements.update.style in replace mode. Requires element_id.',
78 ],
79 ],
80 ]
81 );
82 }
83
84 public function execute( $input = [] ) {
85 $post_id = $this->resolve_post_id( $input );
86 if ( is_wp_error( $post_id ) ) {
87 return $post_id;
88 }
89
90 $document = $this->get_editable_document( $post_id );
91 if ( is_wp_error( $document ) ) {
92 return $document;
93 }
94
95 $elements = $document->get_elements_data();
96 $elements = is_array( $elements ) ? $elements : [];
97
98 $element_id = isset( $input['element_id'] ) ? (string) $input['element_id'] : '';
99 $include_content = ! empty( $input['include_content'] );
100
101 if ( $include_content && '' === $element_id ) {
102 return new \WP_Error(
103 'invalid_input',
104 __( 'include_content requires element_id.', 'elementor' ),
105 [ 'status' => \WP_Http::BAD_REQUEST ]
106 );
107 }
108
109 $document_root_ids = array_column( $elements, 'id' );
110
111 if ( '' !== $element_id ) {
112 $subtree = Utils::find_element_recursive( $elements, $element_id );
113
114 if ( ! $subtree ) {
115 return new \WP_Error(
116 'element_not_found',
117 __( 'Element not found in this document.', 'elementor' ),
118 [ 'status' => \WP_Http::NOT_FOUND ]
119 );
120 }
121
122 $elements = [ $subtree ];
123 }
124
125 return [
126 'elements' => $this->prune_elements( $elements, $include_content, $document_root_ids ),
127 ];
128 }
129
130 private function prune_elements( array $elements, bool $include_content = false, array $document_root_ids = [] ): array {
131 return Plugin::$instance->db->iterate_data( $elements, function ( $node ) use ( $include_content, $document_root_ids ) {
132 $skeleton = [
133 'id' => $node['id'] ?? null,
134 'elType' => $node['elType'] ?? null,
135 ];
136
137 if ( isset( $node['widgetType'] ) ) {
138 $skeleton['widgetType'] = $node['widgetType'];
139 }
140
141 $title = Element_Structure_Title::resolve( $node );
142
143 if ( null !== $title ) {
144 $skeleton['title'] = $title;
145 }
146
147 if ( ! empty( $node['elements'] ) ) {
148 $skeleton['elements'] = $node['elements'];
149 }
150
151 if ( $include_content ) {
152 $this->populate_content( $skeleton, $node, in_array( $node['id'] ?? null, $document_root_ids, true ) );
153 }
154
155 return $skeleton;
156 } );
157 }
158
159 private function populate_content( array &$skeleton, array $node, bool $is_document_root ): void {
160 $skeleton['interactions'] = $this->normalize_interactions( $node['interactions'] ?? null );
161
162 if ( V3_Node_Bridge::is_v3_node( $node ) ) {
163 $this->populate_v3_content( $skeleton, $node );
164 return;
165 }
166
167 $config = $this->resolve_widget_config( $node );
168 $props_schema = $config['atomic_props_schema'] ?? null;
169 $raw_settings = $node['settings'] ?? [];
170
171 if ( ! $props_schema ) {
172 $skeleton['settings'] = (object) [];
173 $skeleton['styles'] = (object) [];
174 return;
175 }
176
177 $skeleton['settings'] = $this->serialize_settings_for_llm( $props_schema, $raw_settings );
178 $skeleton['styles'] = Local_Style_Serializer::serialize( $node['styles'] ?? [] );
179
180 $rendered_settings = $this->resolve_rendered_settings( $props_schema, $raw_settings );
181 $this->populate_default_styles( $skeleton, $node, $config, $rendered_settings, $is_document_root );
182 }
183
184 private function serialize_settings_for_llm( array $props_schema, $raw_settings ) {
185 if ( ! is_array( $raw_settings ) ) {
186 return (object) [];
187 }
188
189 [ $serialized, $static_schema ] = $this->split_dynamic_settings( $props_schema, $raw_settings );
190
191 if ( ! empty( $static_schema ) ) {
192 $serialized += Render_Props_Resolver::for_plain()->resolve( $static_schema, $raw_settings );
193 }
194
195 return ! empty( $serialized ) ? $serialized : (object) [];
196 }
197
198 private function resolve_rendered_settings( array $props_schema, $raw_settings ): array {
199 if ( ! is_array( $raw_settings ) ) {
200 return [];
201 }
202
203 [ , $static_schema ] = $this->split_dynamic_settings( $props_schema, $raw_settings );
204
205 return empty( $static_schema ) ? [] : Render_Props_Resolver::for_settings()->resolve( $static_schema, $raw_settings );
206 }
207
208 private function split_dynamic_settings( array $props_schema, array $raw_settings ): array {
209 $dynamic_settings = [];
210 $static_schema = [];
211
212 foreach ( array_intersect_key( $props_schema, $raw_settings ) as $key => $prop_type ) {
213 $dynamic = Dynamic_Tag_Llm_Resolver::try_serialize( $raw_settings[ $key ] );
214
215 if ( null !== $dynamic ) {
216 $dynamic_settings[ $key ] = $dynamic;
217 continue;
218 }
219
220 $static_schema[ $key ] = $prop_type;
221 }
222
223 return [ $dynamic_settings, $static_schema ];
224 }
225
226 private function populate_default_styles( array &$skeleton, array $node, array $config, array $resolved_settings, bool $is_document_root ): void {
227 $base_styles = is_array( $config['base_styles'] ?? null ) ? $config['base_styles'] : [];
228 $props_schema = is_array( $config['atomic_props_schema'] ?? null ) ? $config['atomic_props_schema'] : [];
229 $tag = $this->resolve_html_tag( $node, $resolved_settings );
230
231 $default_styles_css = Element_Default_Styles_Builder::render(
232 $base_styles,
233 $tag,
234 $this->get_default_styles_repository(),
235 null,
236 Atomic_Container_Presentation::applies_to( $config ) ? Atomic_Container_Presentation::to_css_string( $is_document_root ) : ''
237 );
238
239 if ( null !== $tag ) {
240 $skeleton['tag'] = $tag;
241 }
242
243 if ( '' !== $default_styles_css ) {
244 $skeleton['default_styles'] = $default_styles_css;
245 }
246 }
247
248 private function resolve_html_tag( array $node, array $resolved_settings ): ?string {
249 $type = Atomic_Elements_Utils::get_element_type( $node );
250
251 if ( ! $type ) {
252 return null;
253 }
254
255 return Element_Tag_Resolver::resolve( $resolved_settings, $type );
256 }
257
258 private function get_default_styles_repository(): ?Default_Styles_Repository {
259 if ( null !== $this->default_styles_repository ) {
260 return $this->default_styles_repository;
261 }
262
263 if ( ! class_exists( Default_Styles_Repository::class ) ) {
264 return null;
265 }
266
267 $this->default_styles_repository = Default_Styles_Repository::make();
268
269 return $this->default_styles_repository;
270 }
271
272 private function populate_v3_content( array &$skeleton, array $node ): void {
273 $widget_type = (string) ( $node['widgetType'] ?? '' );
274 $raw_settings = is_array( $node['settings'] ?? null ) ? $node['settings'] : [];
275
276 $allowed = V3_Widget_Bridge_Registry::get_non_style_keys( $widget_type );
277
278 if ( empty( $allowed ) && empty( V3_Widget_Bridge_Registry::get_style_overrides( $widget_type ) ) ) {
279 $skeleton['settings'] = (object) [];
280 $skeleton['style'] = '';
281 return;
282 }
283
284 $filter = V3_Non_Style_Allowlist::filter( $widget_type, $raw_settings );
285 $allowed_settings = $filter['allowed'];
286
287 $widget_config = Widget_Context_Helper::get_widget_config( $widget_type );
288 $style = ( new V3_Style_Serializer() )->serialize( $raw_settings, $widget_type, $widget_config ?? [] );
289
290 $skeleton['settings'] = ! empty( $allowed_settings ) ? $allowed_settings : (object) [];
291 $skeleton['style'] = $style;
292 }
293
294 private function normalize_interactions( $interactions ): array {
295 if ( is_string( $interactions ) ) {
296 $decoded = json_decode( $interactions, true );
297 $interactions = ( JSON_ERROR_NONE === json_last_error() && is_array( $decoded ) ) ? $decoded : [];
298 }
299
300 if ( ! is_array( $interactions ) || empty( $interactions['items'] ) ) {
301 return [];
302 }
303
304 $resolver = Render_Props_Resolver::for_plain();
305 $prop_type = Interaction_Item_Prop_Type::make();
306 $plain = [];
307
308 foreach ( $interactions['items'] as $item ) {
309 $serialized = $resolver->resolve_value( $item, $prop_type );
310
311 if ( is_array( $serialized ) && ! empty( $serialized ) ) {
312 $plain[] = $serialized;
313 }
314 }
315
316 return $plain;
317 }
318
319 private function resolve_widget_config( array $node ): array {
320 $type = Atomic_Elements_Utils::get_element_type( $node );
321
322 if ( ! $type ) {
323 return [];
324 }
325
326 $config = Widget_Context_Helper::get_widget_config( (string) $type );
327
328 return is_array( $config ) ? $config : [];
329 }
330
331 private function resolve_post_id( $input ) {
332 $post_id = isset( $input['post_id'] ) ? absint( $input['post_id'] ) : 0;
333
334 if ( ! $post_id ) {
335 return new \WP_Error(
336 'invalid_post_id',
337 __( 'A valid post_id is required.', 'elementor' ),
338 [ 'status' => \WP_Http::BAD_REQUEST ]
339 );
340 }
341
342 if ( ! current_user_can( 'edit_post', $post_id ) ) {
343 return new \WP_Error(
344 'rest_cannot_view',
345 __( 'Sorry, you are not allowed to access this document.', 'elementor' ),
346 [ 'status' => \WP_Http::FORBIDDEN ]
347 );
348 }
349
350 return $post_id;
351 }
352
353 private function get_editable_document( int $post_id ) {
354 $document = Plugin::$instance->documents->get( $post_id );
355
356 if ( ! $document ) {
357 return new \WP_Error(
358 'document_not_found',
359 __( 'Document not found.', 'elementor' ),
360 [ 'status' => \WP_Http::NOT_FOUND ]
361 );
362 }
363
364 if ( ! $document->is_built_with_elementor() ) {
365 return new \WP_Error(
366 'not_elementor',
367 __( 'This post is not built with Elementor.', 'elementor' ),
368 [ 'status' => \WP_Http::BAD_REQUEST ]
369 );
370 }
371
372 if ( ! $document->is_editable_by_current_user() ) {
373 return new \WP_Error(
374 'rest_cannot_view',
375 __( 'Sorry, you are not allowed to edit this document.', 'elementor' ),
376 [ 'status' => \WP_Http::FORBIDDEN ]
377 );
378 }
379
380 return $document;
381 }
382 }
383