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 / get-structure-ability.php

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

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