PluginProbe
Elementor Website Builder – more than just a page builder / 3.6.0-dev10
Elementor Website Builder – more than just a page builder v3.6.0-dev10
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 4.0.7 All 451 releases
elementor / core / base / document.php

document.php in Elementor Website Builder – more than just a page builder 3.6.0-dev10, at core/base/document.php

1,678 lines 40.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Elementor\Core\Base;
3
4 use Elementor\Core\Base\Elements_Iteration_Actions\Assets as Assets_Iteration_Action;
5 use Elementor\Core\Base\Elements_Iteration_Actions\Base as Elements_Iteration_Action;
6 use Elementor\Core\Files\CSS\Post as Post_CSS;
7 use Elementor\Core\Settings\Page\Model as Page_Model;
8 use Elementor\Core\Utils\Exceptions;
9 use Elementor\Includes\Elements\Container;
10 use Elementor\Plugin;
11 use Elementor\Controls_Manager;
12 use Elementor\Controls_Stack;
13 use Elementor\TemplateLibrary\Source_Local;
14 use Elementor\User;
15 use Elementor\Core\Settings\Manager as SettingsManager;
16 use Elementor\Utils;
17 use Elementor\Widget_Base;
18 use Elementor\Core\Settings\Page\Manager as PageManager;
19
20 if ( ! defined( 'ABSPATH' ) ) {
21 exit; // Exit if accessed directly
22 }
23
24 /**
25 * Elementor document.
26 *
27 * An abstract class that provides the needed properties and methods to
28 * manage and handle documents in inheriting classes.
29 *
30 * @since 2.0.0
31 * @abstract
32 */
33 abstract class Document extends Controls_Stack {
34
35 /**
36 * Document type meta key.
37 */
38 const TYPE_META_KEY = '_elementor_template_type';
39 const PAGE_META_KEY = '_elementor_page_settings';
40
41 const BUILT_WITH_ELEMENTOR_META_KEY = '_elementor_edit_mode';
42
43 /**
44 * Document publish status.
45 */
46 const STATUS_PUBLISH = 'publish';
47
48 /**
49 * Document draft status.
50 */
51 const STATUS_DRAFT = 'draft';
52
53 /**
54 * Document private status.
55 */
56 const STATUS_PRIVATE = 'private';
57
58 /**
59 * Document autosave status.
60 */
61 const STATUS_AUTOSAVE = 'autosave';
62
63 /**
64 * Document pending status.
65 */
66 const STATUS_PENDING = 'pending';
67
68
69 private $main_id;
70
71 /**
72 * @var bool
73 */
74 private $is_saving = false;
75
76 private static $properties = [];
77
78 /**
79 * @var Elements_Iteration_Action[]
80 */
81 private $elements_iteration_actions = [];
82
83 /**
84 * Document post data.
85 *
86 * Holds the document post data.
87 *
88 * @since 2.0.0
89 * @access protected
90 *
91 * @var \WP_Post WordPress post data.
92 */
93 protected $post;
94
95 /**
96 * @since 2.1.0
97 * @access protected
98 * @static
99 */
100 protected static function get_editor_panel_categories() {
101 return Plugin::$instance->elements_manager->get_categories();
102 }
103
104 /**
105 * Get properties.
106 *
107 * Retrieve the document properties.
108 *
109 * @since 2.0.0
110 * @access public
111 * @static
112 *
113 * @return array Document properties.
114 */
115 public static function get_properties() {
116 return [
117 'has_elements' => true,
118 'is_editable' => true,
119 'edit_capability' => '',
120 'show_in_finder' => true,
121 'show_on_admin_bar' => true,
122 'support_kit' => false,
123 ];
124 }
125
126 /**
127 * @since 2.1.0
128 * @access public
129 * @static
130 */
131 public static function get_editor_panel_config() {
132 $default_route = 'panel/elements/categories';
133
134 if ( ! Plugin::instance()->role_manager->user_can( 'design' ) ) {
135 $default_route = 'panel/page-settings/settings';
136 }
137
138 return [
139 'title' => static::get_title(), // JS Container title.
140 'widgets_settings' => [],
141 'elements_categories' => static::get_editor_panel_categories(),
142 'default_route' => $default_route,
143 'has_elements' => static::get_property( 'has_elements' ),
144 'support_kit' => static::get_property( 'support_kit' ),
145 'messages' => [
146 /* translators: %s: Document title. */
147 'publish_notification' => sprintf( esc_html__( 'Hurray! Your %s is live.', 'elementor' ), static::get_title() ),
148 ],
149 ];
150 }
151
152 /**
153 * Get element title.
154 *
155 * Retrieve the element title.
156 *
157 * @since 2.0.0
158 * @access public
159 * @static
160 *
161 * @return string Element title.
162 */
163 public static function get_title() {
164 return esc_html__( 'Document', 'elementor' );
165 }
166
167 public static function get_plural_title() {
168 return static::get_title();
169 }
170
171 public static function get_add_new_title() {
172 return sprintf( esc_html__( 'Add New %s', 'elementor' ), static::get_title() );
173 }
174
175 /**
176 * Get property.
177 *
178 * Retrieve the document property.
179 *
180 * @since 2.0.0
181 * @access public
182 * @static
183 *
184 * @param string $key The property key.
185 *
186 * @return mixed The property value.
187 */
188 public static function get_property( $key ) {
189 $id = static::get_class_full_name();
190
191 if ( ! isset( self::$properties[ $id ] ) ) {
192 self::$properties[ $id ] = static::get_properties();
193 }
194
195 return self::get_items( self::$properties[ $id ], $key );
196 }
197
198 /**
199 * @since 2.0.0
200 * @access public
201 * @static
202 */
203 public static function get_class_full_name() {
204 return get_called_class();
205 }
206
207 public static function get_create_url() {
208 $properties = static::get_properties();
209
210 // BC Support - Each document should define it own CPT this code is for BC support.
211 $cpt = Source_Local::CPT;
212
213 if ( isset( $properties['cpt'][0] ) ) {
214 $cpt = $properties['cpt'][0];
215 }
216
217 return Plugin::$instance->documents->get_create_new_post_url( $cpt, static::get_type() );
218 }
219
220 public function get_name() {
221 return static::get_type();
222 }
223
224 /**
225 * @since 2.0.0
226 * @access public
227 */
228 public function get_unique_name() {
229 return static::get_type() . '-' . $this->post->ID;
230 }
231
232 /**
233 * @since 2.3.0
234 * @access public
235 */
236 public function get_post_type_title() {
237 $post_type_object = get_post_type_object( $this->post->post_type );
238
239 return $post_type_object->labels->singular_name;
240 }
241
242 /**
243 * @since 2.0.0
244 * @access public
245 */
246 public function get_main_id() {
247 if ( ! $this->main_id ) {
248 $post_id = $this->post->ID;
249
250 $parent_post_id = wp_is_post_revision( $post_id );
251
252 if ( $parent_post_id ) {
253 $post_id = $parent_post_id;
254 }
255
256 $this->main_id = $post_id;
257 }
258
259 return $this->main_id;
260 }
261
262 /**
263 * @since 2.0.0
264 * @access public
265 *
266 * @param $data
267 *
268 * @throws \Exception If the widget was not found.
269 *
270 * @return string
271 */
272 public function render_element( $data ) {
273 // Start buffering
274 ob_start();
275
276 /** @var Widget_Base $widget */
277 $widget = Plugin::$instance->elements_manager->create_element_instance( $data );
278
279 if ( ! $widget ) {
280 throw new \Exception( 'Widget not found.' );
281 }
282
283 $widget->render_content();
284
285 $render_html = ob_get_clean();
286
287 return $render_html;
288 }
289
290 /**
291 * @since 2.0.0
292 * @access public
293 */
294 public function get_main_post() {
295 return get_post( $this->get_main_id() );
296 }
297
298 public function get_container_attributes() {
299 $id = $this->get_main_id();
300
301 $attributes = [
302 'data-elementor-type' => $this->get_name(),
303 'data-elementor-id' => $id,
304 'class' => 'elementor elementor-' . $id,
305 ];
306
307 $version_meta = $this->get_main_meta( '_elementor_version' );
308
309 if ( version_compare( $version_meta, '2.5.0', '<' ) ) {
310 $attributes['class'] .= ' elementor-bc-flex-widget';
311 }
312
313 if ( Plugin::$instance->preview->is_preview() ) {
314 $attributes['data-elementor-title'] = static::get_title();
315 } else {
316 $attributes['data-elementor-settings'] = wp_json_encode( $this->get_frontend_settings() );
317 }
318
319 return $attributes;
320 }
321
322 /**
323 * @since 2.0.0
324 * @access public
325 */
326 public function get_wp_preview_url() {
327 $main_post_id = $this->get_main_id();
328 $document = $this;
329
330 // Ajax request from editor.
331 // PHPCS - only reading the value from $_POST['initial_document_id'].
332 if ( ! empty( $_POST['initial_document_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
333 // PHPCS - only reading the value from $_POST['initial_document_id'].
334 $document = Plugin::$instance->documents->get( $_POST['initial_document_id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
335 }
336
337 $url = get_preview_post_link(
338 $document->get_main_id(),
339 [
340 'preview_id' => $main_post_id,
341 'preview_nonce' => wp_create_nonce( 'post_preview_' . $main_post_id ),
342 ]
343 );
344
345 /**
346 * Document "WordPress preview" URL.
347 *
348 * Filters the WordPress preview URL.
349 *
350 * @since 2.0.0
351 *
352 * @param string $url WordPress preview URL.
353 * @param Document $this The document instance.
354 */
355 $url = apply_filters( 'elementor/document/urls/wp_preview', $url, $this );
356
357 return $url;
358 }
359
360 /**
361 * @since 2.0.0
362 * @access public
363 */
364 public function get_exit_to_dashboard_url() {
365 $url = get_edit_post_link( $this->get_main_id(), 'raw' );
366
367 /**
368 * Document "exit to dashboard" URL.
369 *
370 * Filters the "Exit To Dashboard" URL.
371 *
372 * @since 2.0.0
373 *
374 * @param string $url The exit URL
375 * @param Document $this The document instance.
376 */
377 $url = apply_filters( 'elementor/document/urls/exit_to_dashboard', $url, $this );
378
379 return $url;
380 }
381
382 /**
383 * Get auto-saved post revision.
384 *
385 * Retrieve the auto-saved post revision that is newer than current post.
386 *
387 * @since 2.0.0
388 * @access public
389 *
390 *
391 * @return bool|Document
392 */
393
394 public function get_newer_autosave() {
395 $autosave = $this->get_autosave();
396
397 // Detect if there exists an autosave newer than the post.
398 if ( $autosave && mysql2date( 'U', $autosave->get_post()->post_modified_gmt, false ) > mysql2date( 'U', $this->post->post_modified_gmt, false ) ) {
399 return $autosave;
400 }
401
402 return false;
403 }
404
405 /**
406 * @since 2.0.0
407 * @access public
408 */
409 public function is_autosave() {
410 return wp_is_post_autosave( $this->post->ID );
411 }
412
413 /**
414 * Check if the current document is a 'revision'
415 *
416 * @return bool
417 */
418 public function is_revision() {
419 return 'revision' === $this->post->post_type;
420 }
421
422 /**
423 * Checks if the current document status is 'trash'.
424 *
425 * @return bool
426 */
427 public function is_trash() {
428 return 'trash' === $this->post->post_status;
429 }
430
431 /**
432 * @since 2.0.0
433 * @access public
434 *
435 * @param int $user_id
436 * @param bool $create
437 *
438 * @return bool|Document
439 */
440 public function get_autosave( $user_id = 0, $create = false ) {
441 if ( ! $user_id ) {
442 $user_id = get_current_user_id();
443 }
444
445 $autosave_id = $this->get_autosave_id( $user_id );
446
447 if ( $autosave_id ) {
448 $document = Plugin::$instance->documents->get( $autosave_id );
449 } elseif ( $create ) {
450 $autosave_id = wp_create_post_autosave( [
451 'post_ID' => $this->post->ID,
452 'post_type' => $this->post->post_type,
453 'post_title' => $this->post->post_title,
454 'post_excerpt' => $this->post->post_excerpt,
455 // Hack to cause $autosave_is_different=true in `wp_create_post_autosave`.
456 'post_content' => '<!-- Created With Elementor -->',
457 'post_modified' => current_time( 'mysql' ),
458 ] );
459
460 Plugin::$instance->db->copy_elementor_meta( $this->post->ID, $autosave_id );
461
462 $document = Plugin::$instance->documents->get( $autosave_id );
463 $document->save_template_type();
464 } else {
465 $document = false;
466 }
467
468 return $document;
469 }
470
471 /**
472 * Add/Remove edit link in dashboard.
473 *
474 * Add or remove an edit link to the post/page action links on the post/pages list table.
475 *
476 * Fired by `post_row_actions` and `page_row_actions` filters.
477 *
478 * @access public
479 *
480 * @param array $actions An array of row action links.
481 *
482 * @return array An updated array of row action links.
483 */
484 public function filter_admin_row_actions( $actions ) {
485 if ( $this->is_built_with_elementor() && $this->is_editable_by_current_user() ) {
486 $actions['edit_with_elementor'] = sprintf(
487 '<a href="%1$s">%2$s</a>',
488 $this->get_edit_url(),
489 __( 'Edit with Elementor', 'elementor' )
490 );
491 }
492
493 return $actions;
494 }
495
496 /**
497 * @since 2.0.0
498 * @access public
499 */
500 public function is_editable_by_current_user() {
501 $edit_capability = static::get_property( 'edit_capability' );
502 if ( $edit_capability && ! current_user_can( $edit_capability ) ) {
503 return false;
504 }
505
506 return self::get_property( 'is_editable' ) && User::is_current_user_can_edit( $this->get_main_id() );
507 }
508
509 /**
510 * @since 2.9.0
511 * @access protected
512 */
513 protected function get_initial_config() {
514 // Get document data *after* the scripts hook - so plugins can run compatibility before get data, but *before* enqueue the editor script - so elements can enqueue their own scripts that depended in editor script.
515
516 $locked_user = Plugin::$instance->editor->get_locked_user( $this->get_main_id() );
517
518 if ( $locked_user ) {
519 $locked_user = $locked_user->display_name;
520 }
521
522 $post_type_object = get_post_type_object( $this->get_main_post()->post_type );
523
524 $settings = SettingsManager::get_settings_managers_config();
525
526 $config = [
527 'id' => $this->get_main_id(),
528 'type' => $this->get_name(),
529 'version' => $this->get_main_meta( '_elementor_version' ),
530 'settings' => $settings['page'],
531 'remoteLibrary' => $this->get_remote_library_config(),
532 'last_edited' => $this->get_last_edited(),
533 'panel' => static::get_editor_panel_config(),
534 'container' => 'body',
535 'post_type_title' => $this->get_post_type_title(),
536 'user' => [
537 'can_publish' => current_user_can( $post_type_object->cap->publish_posts ),
538
539 // Deprecated config since 2.9.0.
540 'locked' => $locked_user,
541 ],
542 'urls' => [
543 'exit_to_dashboard' => $this->get_exit_to_dashboard_url(),
544 'preview' => $this->get_preview_url(),
545 'wp_preview' => $this->get_wp_preview_url(),
546 'permalink' => $this->get_permalink(),
547 'have_a_look' => $this->get_have_a_look_url(),
548 ],
549 ];
550
551 do_action( 'elementor/document/before_get_config', $this );
552
553 if ( static::get_property( 'has_elements' ) ) {
554 $container = [];
555 $experiments_manager = Plugin::$instance->experiments;
556
557 if ( $experiments_manager->is_feature_active( 'container' ) ) {
558 $container = [
559 'container' => ( new Container() )->get_config(),
560 ];
561 }
562
563 $config['elements'] = $this->get_elements_raw_data( null, true );
564 $config['widgets'] = $container + Plugin::$instance->widgets_manager->get_widget_types_config();
565 }
566
567 $additional_config = [];
568
569 /**
570 * Additional document configuration.
571 *
572 * Filters the document configuration by adding additional configuration.
573 * External developers can use this hook to add custom configuration in
574 * addition to Elementor's initial configuration.
575 *
576 * Use the $post_id to add custom configuration for different pages.
577 *
578 * @param array $additional_config The additional document configuration.
579 * @param int $post_id The post ID of the document.
580 */
581 $additional_config = apply_filters( 'elementor/document/config', $additional_config, $this->get_main_id() );
582
583 if ( ! empty( $additional_config ) ) {
584 $config = array_replace_recursive( $config, $additional_config );
585 }
586
587 return $config;
588 }
589
590 /**
591 * @since 3.1.0
592 * @access protected
593 */
594 protected function register_controls() {
595 $this->register_document_controls();
596
597 /**
598 * Register document controls.
599 *
600 * Fires after Elementor registers the document controls.
601 *
602 * External developers can use this hook to add new controls to the document.
603 *
604 * @since 2.0.0
605 *
606 * @param Document $this The document instance.
607 */
608 do_action( 'elementor/documents/register_controls', $this );
609 }
610
611 /**
612 * @since 2.0.0
613 * @access public
614 *
615 * @param $data
616 *
617 * @return bool
618 */
619 public function save( $data ) {
620 /**
621 * Document save data.
622 *
623 * Filter the document data before saving process starts.
624 *
625 * External developers can use this hook to change the data before
626 * saving it to the database.
627 *
628 * @since 3.3.0
629 *
630 * @param array $data The document data.
631 * @param \Elementor\Core\Base\Document $this The document instance.
632 */
633 $data = apply_filters( 'elementor/document/save/data', $data, $this );
634
635 $this->add_handle_revisions_changed_filter();
636
637 if ( ! $this->is_editable_by_current_user() ) {
638 return false;
639 }
640
641 $this->set_is_saving( true );
642
643 /**
644 * Before document save.
645 *
646 * Fires when document save starts on Elementor.
647 *
648 * @since 2.5.12
649 *
650 * @param \Elementor\Core\Base\Document $this The current document.
651 * @param $data.
652 */
653 do_action( 'elementor/document/before_save', $this, $data );
654
655 if ( ! current_user_can( 'unfiltered_html' ) ) {
656 $data = wp_kses_post_deep( $data );
657 }
658
659 if ( ! empty( $data['settings'] ) ) {
660 if ( isset( $data['settings']['post_status'] ) && self::STATUS_AUTOSAVE === $data['settings']['post_status'] ) {
661 if ( ! defined( 'DOING_AUTOSAVE' ) ) {
662 define( 'DOING_AUTOSAVE', true );
663 }
664 }
665
666 $this->save_settings( $data['settings'] );
667
668 $this->refresh_post();
669 }
670
671 // Don't check is_empty, because an empty array should be saved.
672 if ( isset( $data['elements'] ) && is_array( $data['elements'] ) ) {
673 $this->save_elements( $data['elements'] );
674 }
675
676 $this->save_template_type();
677
678 $this->save_version();
679
680 // Remove Post CSS
681 $post_css = Post_CSS::create( $this->post->ID );
682
683 $post_css->delete();
684
685 /**
686 * After document save.
687 *
688 * Fires when document save is complete.
689 *
690 * @since 2.5.12
691 *
692 * @param \Elementor\Core\Base\Document $this The current document.
693 * @param $data.
694 */
695 do_action( 'elementor/document/after_save', $this, $data );
696
697 $this->set_is_saving( false );
698
699 $this->remove_handle_revisions_changed_filter();
700
701 return true;
702 }
703
704 public function refresh_post() {
705 $this->post = get_post( $this->post->ID );
706 }
707
708 /**
709 * @param array $new_settings
710 *
711 * @return static
712 */
713 public function update_settings( array $new_settings ) {
714 $document_settings = $this->get_meta( PageManager::META_KEY );
715
716 if ( ! $document_settings ) {
717 $document_settings = [];
718 }
719
720 $this->save_settings(
721 array_replace_recursive( $document_settings, $new_settings )
722 );
723
724 return $this;
725 }
726
727 /**
728 * Is built with Elementor.
729 *
730 * Check whether the post was built with Elementor.
731 *
732 * @since 2.0.0
733 * @access public
734 *
735 * @return bool Whether the post was built with Elementor.
736 */
737 public function is_built_with_elementor() {
738 return ! ! $this->get_meta( self::BUILT_WITH_ELEMENTOR_META_KEY );
739 }
740
741 /**
742 * Mark the post as "built with elementor" or not.
743 *
744 * @param bool $is_built_with_elementor
745 *
746 * @return $this
747 */
748 public function set_is_built_with_elementor( $is_built_with_elementor ) {
749 if ( $is_built_with_elementor ) {
750 // Use the string `builder` and not a boolean for rollback compatibility
751 $this->update_meta( self::BUILT_WITH_ELEMENTOR_META_KEY, 'builder' );
752 } else {
753 $this->delete_meta( self::BUILT_WITH_ELEMENTOR_META_KEY );
754 }
755
756 return $this;
757 }
758
759 /**
760 * @since 2.0.0
761 * @access public
762 * @static
763 *
764 * @return mixed
765 */
766 public function get_edit_url() {
767 $url = add_query_arg(
768 [
769 'post' => $this->get_main_id(),
770 'action' => 'elementor',
771 ],
772 admin_url( 'post.php' )
773 );
774
775 /**
776 * Document edit url.
777 *
778 * Filters the document edit url.
779 *
780 * @since 2.0.0
781 *
782 * @param string $url The edit url.
783 * @param Document $this The document instance.
784 */
785 $url = apply_filters( 'elementor/document/urls/edit', $url, $this );
786
787 return $url;
788 }
789
790 /**
791 * @since 2.0.0
792 * @access public
793 */
794 public function get_preview_url() {
795 /**
796 * Use a static var - to avoid change the `ver` parameter on every call.
797 */
798 static $url;
799
800 if ( empty( $url ) ) {
801
802 add_filter( 'pre_option_permalink_structure', '__return_empty_string' );
803
804 $url = set_url_scheme( add_query_arg( [
805 'elementor-preview' => $this->get_main_id(),
806 'ver' => time(),
807 ], $this->get_permalink() ) );
808
809 remove_filter( 'pre_option_permalink_structure', '__return_empty_string' );
810
811 /**
812 * Document preview URL.
813 *
814 * Filters the document preview URL.
815 *
816 * @since 2.0.0
817 *
818 * @param string $url The preview URL.
819 * @param Document $this The document instance.
820 */
821 $url = apply_filters( 'elementor/document/urls/preview', $url, $this );
822 }
823
824 return $url;
825 }
826
827 /**
828 * @since 2.0.0
829 * @access public
830 *
831 * @param string $key
832 *
833 * @return array
834 */
835 public function get_json_meta( $key ) {
836 $meta = get_post_meta( $this->post->ID, $key, true );
837
838 if ( is_string( $meta ) && ! empty( $meta ) ) {
839 $meta = json_decode( $meta, true );
840 }
841
842 if ( empty( $meta ) ) {
843 $meta = [];
844 }
845
846 return $meta;
847 }
848
849 /**
850 * @since 2.0.0
851 * @access public
852 *
853 * @param null $data
854 * @param bool $with_html_content
855 *
856 * @return array
857 */
858 public function get_elements_raw_data( $data = null, $with_html_content = false ) {
859 if ( ! static::get_property( 'has_elements' ) ) {
860 return [];
861 }
862
863 if ( is_null( $data ) ) {
864 $data = $this->get_elements_data();
865 }
866
867 // Change the current documents, so widgets can use `documents->get_current` and other post data
868 Plugin::$instance->documents->switch_to_document( $this );
869
870 $editor_data = [];
871
872 foreach ( $data as $element_data ) {
873 if ( ! is_array( $element_data ) ) {
874 throw new \Exception( 'Invalid data: ' . wp_json_encode( [
875 'data' => $data,
876 'element' => $element_data,
877 ] ) );
878 }
879
880 $element = Plugin::$instance->elements_manager->create_element_instance( $element_data );
881
882 if ( ! $element ) {
883 continue;
884 }
885
886 $editor_data[] = $element->get_raw_data( $with_html_content );
887 } // End foreach().
888
889 Plugin::$instance->documents->restore_document();
890
891 return $editor_data;
892 }
893
894 /**
895 * @since 2.0.0
896 * @access public
897 *
898 * @param string $status
899 *
900 * @return array
901 */
902 public function get_elements_data( $status = self::STATUS_PUBLISH ) {
903 $elements = $this->get_json_meta( '_elementor_data' );
904
905 if ( self::STATUS_DRAFT === $status ) {
906 $autosave = $this->get_newer_autosave();
907
908 if ( is_object( $autosave ) ) {
909 $autosave_elements = Plugin::$instance->documents
910 ->get( $autosave->get_post()->ID )
911 ->get_json_meta( '_elementor_data' );
912 }
913 }
914
915 if ( Plugin::$instance->editor->is_edit_mode() ) {
916 if ( empty( $elements ) && empty( $autosave_elements ) ) {
917 // Convert to Elementor.
918 $elements = $this->convert_to_elementor();
919 if ( $this->is_autosave() ) {
920 Plugin::$instance->db->copy_elementor_meta( $this->post->post_parent, $this->post->ID );
921 }
922 }
923 }
924
925 if ( ! empty( $autosave_elements ) ) {
926 $elements = $autosave_elements;
927 }
928
929 return $elements;
930 }
931
932 /**
933 * Get document setting from DB.
934 *
935 * @return array
936 */
937 public function get_db_document_settings() {
938 return $this->get_meta( static::PAGE_META_KEY );
939 }
940
941 /**
942 * @since 2.3.0
943 * @access public
944 */
945 public function convert_to_elementor() {
946 $this->save( [] );
947
948 if ( empty( $this->post->post_content ) ) {
949 return [];
950 }
951
952 // Check if it's only a shortcode.
953 preg_match_all( '/' . get_shortcode_regex() . '/', $this->post->post_content, $matches, PREG_SET_ORDER );
954 if ( ! empty( $matches ) ) {
955 foreach ( $matches as $shortcode ) {
956 if ( trim( $this->post->post_content ) === $shortcode[0] ) {
957 $widget_type = Plugin::$instance->widgets_manager->get_widget_types( 'shortcode' );
958 $settings = [
959 'shortcode' => $this->post->post_content,
960 ];
961 break;
962 }
963 }
964 }
965
966 if ( empty( $widget_type ) ) {
967 $widget_type = Plugin::$instance->widgets_manager->get_widget_types( 'text-editor' );
968 $settings = [
969 'editor' => $this->post->post_content,
970 ];
971 }
972
973 // TODO: Better coding to start template for editor
974 return [
975 [
976 'id' => Utils::generate_random_string(),
977 'elType' => 'section',
978 'elements' => [
979 [
980 'id' => Utils::generate_random_string(),
981 'elType' => 'column',
982 'elements' => [
983 [
984 'id' => Utils::generate_random_string(),
985 'elType' => $widget_type::get_type(),
986 'widgetType' => $widget_type->get_name(),
987 'settings' => $settings,
988 ],
989 ],
990 ],
991 ],
992 ],
993 ];
994 }
995
996 /**
997 * @since 2.1.3
998 * @access public
999 */
1000 public function print_elements_with_wrapper( $elements_data = null ) {
1001 if ( ! $elements_data ) {
1002 $elements_data = $this->get_elements_data();
1003 }
1004
1005 $is_dom_optimization_active = Plugin::$instance->experiments->is_feature_active( 'e_dom_optimization' );
1006 ?>
1007 <div <?php Utils::print_html_attributes( $this->get_container_attributes() ); ?>>
1008 <?php if ( ! $is_dom_optimization_active ) { ?>
1009 <div class="elementor-inner">
1010 <?php } ?>
1011 <div class="elementor-section-wrap">
1012 <?php $this->print_elements( $elements_data ); ?>
1013 </div>
1014 <?php if ( ! $is_dom_optimization_active ) { ?>
1015 </div>
1016 <?php } ?>
1017 </div>
1018 <?php
1019 }
1020
1021 /**
1022 * @since 2.0.0
1023 * @access public
1024 */
1025 public function get_css_wrapper_selector() {
1026 return '';
1027 }
1028
1029 /**
1030 * @since 2.0.0
1031 * @access public
1032 */
1033 public function get_panel_page_settings() {
1034 return [
1035 /* translators: %s: Document title. */
1036 'title' => sprintf( esc_html__( '%s Settings', 'elementor' ), static::get_title() ),
1037 ];
1038 }
1039
1040 /**
1041 * @since 2.0.0
1042 * @access public
1043 */
1044 public function get_post() {
1045 return $this->post;
1046 }
1047
1048 /**
1049 * @since 2.0.0
1050 * @access public
1051 */
1052 public function get_permalink() {
1053 return get_permalink( $this->get_main_id() );
1054 }
1055
1056 /**
1057 * @since 2.0.8
1058 * @access public
1059 */
1060 public function get_content( $with_css = false ) {
1061 return Plugin::$instance->frontend->get_builder_content( $this->post->ID, $with_css );
1062 }
1063
1064 /**
1065 * @since 2.0.0
1066 * @access public
1067 */
1068 public function delete() {
1069 if ( 'revision' === $this->post->post_type ) {
1070 $deleted = wp_delete_post_revision( $this->post );
1071 } else {
1072 $deleted = wp_delete_post( $this->post->ID );
1073 }
1074
1075 return $deleted && ! is_wp_error( $deleted );
1076 }
1077
1078 /**
1079 * Save editor elements.
1080 *
1081 * Save data from the editor to the database.
1082 *
1083 * @since 2.0.0
1084 * @access protected
1085 *
1086 * @param array $elements
1087 */
1088 protected function save_elements( $elements ) {
1089 $editor_data = $this->get_elements_raw_data( $elements );
1090
1091 // We need the `wp_slash` in order to avoid the unslashing during the `update_post_meta`
1092 $json_value = wp_slash( wp_json_encode( $editor_data ) );
1093
1094 // Don't use `update_post_meta` that can't handle `revision` post type
1095 $is_meta_updated = update_metadata( 'post', $this->post->ID, '_elementor_data', $json_value );
1096
1097 /**
1098 * Before saving data.
1099 *
1100 * Fires before Elementor saves data to the database.
1101 *
1102 * @since 1.0.0
1103 *
1104 * @param string $status Post status.
1105 * @param int|bool $is_meta_updated Meta ID if the key didn't exist, true on successful update, false on failure.
1106 */
1107 do_action( 'elementor/db/before_save', $this->post->post_status, $is_meta_updated );
1108
1109 Plugin::$instance->db->save_plain_text( $this->post->ID );
1110
1111 $elements_iteration_actions = $this->get_elements_iteration_actions();
1112
1113 if ( $elements_iteration_actions ) {
1114 $this->iterate_elements( $elements, $elements_iteration_actions, 'save' );
1115 }
1116
1117 /**
1118 * After saving data.
1119 *
1120 * Fires after Elementor saves data to the database.
1121 *
1122 * @since 1.0.0
1123 *
1124 * @param int $post_id The ID of the post.
1125 * @param array $editor_data Sanitize posted data.
1126 */
1127 do_action( 'elementor/editor/after_save', $this->post->ID, $editor_data );
1128 }
1129
1130 /**
1131 * @since 2.0.0
1132 * @access public
1133 *
1134 * @param int $user_id Optional. User ID. Default value is `0`.
1135 *
1136 * @return bool|int
1137 */
1138 public function get_autosave_id( $user_id = 0 ) {
1139 if ( ! $user_id ) {
1140 $user_id = get_current_user_id();
1141 }
1142
1143 $autosave = Utils::get_post_autosave( $this->post->ID, $user_id );
1144 if ( $autosave ) {
1145 return $autosave->ID;
1146 }
1147
1148 return false;
1149 }
1150
1151 public function save_version() {
1152 if ( ! defined( 'IS_ELEMENTOR_UPGRADE' ) ) {
1153 // Save per revision.
1154 $this->update_meta( '_elementor_version', ELEMENTOR_VERSION );
1155
1156 /**
1157 * Document version save.
1158 *
1159 * Fires when document version is saved on Elementor.
1160 * Will not fire during Elementor Upgrade.
1161 *
1162 * @since 2.5.12
1163 *
1164 * @param \Elementor\Core\Base\Document $this The current document.
1165 *
1166 */
1167 do_action( 'elementor/document/save_version', $this );
1168 }
1169 }
1170
1171 /**
1172 * @since 2.3.0
1173 * @access public
1174 */
1175 public function save_template_type() {
1176 return $this->update_main_meta( self::TYPE_META_KEY, $this->get_name() );
1177 }
1178
1179 /**
1180 * @since 2.3.0
1181 * @access public
1182 */
1183 public function get_template_type() {
1184 return $this->get_main_meta( self::TYPE_META_KEY );
1185 }
1186
1187 /**
1188 * @since 2.0.0
1189 * @access public
1190 *
1191 * @param string $key Meta data key.
1192 *
1193 * @return mixed
1194 */
1195 public function get_main_meta( $key ) {
1196 return get_post_meta( $this->get_main_id(), $key, true );
1197 }
1198
1199 /**
1200 * @since 2.0.4
1201 * @access public
1202 *
1203 * @param string $key Meta data key.
1204 * @param string $value Meta data value.
1205 *
1206 * @return bool|int
1207 */
1208 public function update_main_meta( $key, $value ) {
1209 return update_post_meta( $this->get_main_id(), $key, $value );
1210 }
1211
1212 /**
1213 * @since 2.0.4
1214 * @access public
1215 *
1216 * @param string $key Meta data key.
1217 * @param string $value Optional. Meta data value. Default is an empty string.
1218 *
1219 * @return bool
1220 */
1221 public function delete_main_meta( $key, $value = '' ) {
1222 return delete_post_meta( $this->get_main_id(), $key, $value );
1223 }
1224
1225 /**
1226 * @since 2.0.0
1227 * @access public
1228 *
1229 * @param string $key Meta data key.
1230 *
1231 * @return mixed
1232 */
1233 public function get_meta( $key ) {
1234 return get_post_meta( $this->post->ID, $key, true );
1235 }
1236
1237 /**
1238 * @since 2.0.0
1239 * @access public
1240 *
1241 * @param string $key Meta data key.
1242 * @param mixed $value Meta data value.
1243 *
1244 * @return bool|int
1245 */
1246 public function update_meta( $key, $value ) {
1247 // Use `update_metadata` in order to work also with revisions.
1248 return update_metadata( 'post', $this->post->ID, $key, $value );
1249 }
1250
1251 /**
1252 * @since 2.0.3
1253 * @access public
1254 *
1255 * @param string $key Meta data key.
1256 * @param string $value Meta data value.
1257 *
1258 * @return bool
1259 */
1260 public function delete_meta( $key, $value = '' ) {
1261 // Use `delete_metadata` in order to work also with revisions.
1262 return delete_metadata( 'post', $this->post->ID, $key, $value );
1263 }
1264
1265 /**
1266 * @since 2.0.0
1267 * @access public
1268 */
1269 public function get_last_edited() {
1270 $post = $this->post;
1271 $autosave_post = $this->get_autosave();
1272
1273 if ( $autosave_post ) {
1274 $post = $autosave_post->get_post();
1275 }
1276
1277 $date = date_i18n( _x( 'M j, H:i', 'revision date format', 'elementor' ), strtotime( $post->post_modified ) );
1278 $display_name = get_the_author_meta( 'display_name', $post->post_author );
1279
1280 if ( $autosave_post || 'revision' === $post->post_type ) {
1281 /* translators: 1: Saving date, 2: Author display name. */
1282 $last_edited = sprintf( esc_html__( 'Draft saved on %1$s by %2$s', 'elementor' ), '<time>' . $date . '</time>', $display_name );
1283 } else {
1284 /* translators: 1: Editing date, 2: Author display name. */
1285 $last_edited = sprintf( esc_html__( 'Last edited on %1$s by %2$s', 'elementor' ), '<time>' . $date . '</time>', $display_name );
1286 }
1287
1288 return $last_edited;
1289 }
1290
1291
1292 /**
1293 * @return bool
1294 */
1295 public function is_saving() {
1296 return $this->is_saving;
1297 }
1298
1299 /**
1300 * @param $is_saving
1301 *
1302 * @return $this
1303 */
1304 public function set_is_saving( $is_saving ) {
1305 $this->is_saving = $is_saving;
1306
1307 return $this;
1308 }
1309
1310 /**
1311 * @since 2.0.0
1312 * @access public
1313 *
1314 * @param array $data
1315 *
1316 * @throws \Exception If the post does not exist.
1317 */
1318 public function __construct( array $data = [] ) {
1319 if ( $data ) {
1320 if ( empty( $data['post_id'] ) ) {
1321 $this->post = new \WP_Post( (object) [] );
1322 } else {
1323 $this->post = get_post( $data['post_id'] );
1324
1325 if ( ! $this->post ) {
1326 throw new \Exception( sprintf( 'Post ID #%s does not exist.', $data['post_id'] ), Exceptions::NOT_FOUND );
1327 }
1328 }
1329
1330 // Each Control_Stack is based on a unique ID.
1331 $data['id'] = $data['post_id'];
1332
1333 if ( ! isset( $data['settings'] ) ) {
1334 $data['settings'] = [];
1335 }
1336
1337 $saved_settings = get_post_meta( $this->post->ID, '_elementor_page_settings', true );
1338 if ( ! empty( $saved_settings ) && is_array( $saved_settings ) ) {
1339 $data['settings'] += $saved_settings;
1340 }
1341 }
1342
1343 parent::__construct( $data );
1344 }
1345
1346 /*
1347 * Get Export Data
1348 *
1349 * Filters a document's data on export
1350 *
1351 * @since 3.2.0
1352 * @access public
1353 *
1354 * @return array The data to export
1355 */
1356 public function get_export_data() {
1357 $content = Plugin::$instance->db->iterate_data( $this->get_elements_data(), function( $element_data ) {
1358 $element_data['id'] = Utils::generate_random_string();
1359
1360 $element = Plugin::$instance->elements_manager->create_element_instance( $element_data );
1361
1362 // If the widget/element isn't exist, like a plugin that creates a widget but deactivated
1363 if ( ! $element ) {
1364 return null;
1365 }
1366
1367 return $this->process_element_import_export( $element, 'on_export' );
1368 } );
1369
1370 return [
1371 'content' => $content,
1372 'settings' => $this->get_data( 'settings' ),
1373 'metadata' => $this->get_export_metadata(),
1374 ];
1375 }
1376
1377 public function get_export_summary() {
1378 return [
1379 'title' => $this->post->post_title,
1380 'doc_type' => $this->get_name(),
1381 'thumbnail' => get_the_post_thumbnail_url( $this->post ),
1382 ];
1383 }
1384
1385 /*
1386 * Get Import Data
1387 *
1388 * Filters a document's data on import
1389 *
1390 * @since 3.2.0
1391 * @access public
1392 *
1393 * @return array The data to import
1394 */
1395 public function get_import_data( array $data ) {
1396 $data['content'] = Plugin::$instance->db->iterate_data( $data['content'], function( $element_data ) {
1397 $element = Plugin::$instance->elements_manager->create_element_instance( $element_data );
1398
1399 // If the widget/element isn't exist, like a plugin that creates a widget but deactivated
1400 if ( ! $element ) {
1401 return null;
1402 }
1403
1404 return $this->process_element_import_export( $element, 'on_import' );
1405 } );
1406
1407 if ( ! empty( $data['settings'] ) ) {
1408 $template_model = new Page_Model( [
1409 'id' => 0,
1410 'settings' => $data['settings'],
1411 ] );
1412
1413 $page_data = $this->process_element_import_export( $template_model, 'on_import' );
1414
1415 $data['settings'] = $page_data['settings'];
1416 }
1417
1418 return $data;
1419 }
1420
1421 /**
1422 * Import
1423 *
1424 * Allows to import an external data to a document
1425 *
1426 * @since 3.2.0
1427 * @access public
1428 *
1429 * @param array $data
1430 */
1431 public function import( array $data ) {
1432 $data = $this->get_import_data( $data );
1433
1434 $this->save( [
1435 'elements' => $data['content'],
1436 'settings' => $data['settings'],
1437 ] );
1438
1439 if ( $data['import_settings']['thumbnail'] ) {
1440 $attachment = Plugin::$instance->templates_manager->get_import_images_instance()->import( [ 'url' => $data['import_settings']['thumbnail'] ] );
1441
1442 set_post_thumbnail( $this->get_main_post(), $attachment['id'] );
1443 }
1444
1445 if ( ! empty( $data['metadata'] ) ) {
1446 foreach ( $data['metadata'] as $key => $value ) {
1447 $this->update_meta( $key, $value );
1448 }
1449 }
1450 }
1451
1452 private function process_element_import_export( Controls_Stack $element, $method ) {
1453 $element_data = $element->get_data();
1454
1455 if ( method_exists( $element, $method ) ) {
1456 // TODO: Use the internal element data without parameters.
1457 $element_data = $element->{$method}( $element_data );
1458 }
1459
1460 foreach ( $element->get_controls() as $control ) {
1461 $control_class = Plugin::$instance->controls_manager->get_control( $control['type'] );
1462
1463 // If the control isn't exist, like a plugin that creates the control but deactivated.
1464 if ( ! $control_class ) {
1465 return $element_data;
1466 }
1467
1468 if ( method_exists( $control_class, $method ) ) {
1469 $element_data['settings'][ $control['name'] ] = $control_class->{$method}( $element->get_settings( $control['name'] ), $control );
1470 }
1471
1472 // On Export, check if the control has an argument 'export' => false.
1473 if ( 'on_export' === $method && isset( $control['export'] ) && false === $control['export'] ) {
1474 unset( $element_data['settings'][ $control['name'] ] );
1475 }
1476 }
1477
1478 return $element_data;
1479 }
1480
1481 protected function get_export_metadata() {
1482 $metadata = get_post_meta( $this->get_main_id() );
1483
1484 foreach ( $metadata as $meta_key => $meta_value ) {
1485 if ( is_protected_meta( $meta_key, 'post' ) ) {
1486 unset( $metadata[ $meta_key ] );
1487
1488 continue;
1489 }
1490
1491 $metadata[ $meta_key ] = $meta_value[0];
1492 }
1493
1494 return $metadata;
1495 }
1496
1497 protected function get_remote_library_config() {
1498 $config = [
1499 'type' => 'block',
1500 'default_route' => 'templates/blocks',
1501 'category' => $this->get_name(),
1502 'autoImportSettings' => false,
1503 ];
1504
1505 return $config;
1506 }
1507
1508 /**
1509 * @since 2.0.4
1510 * @access protected
1511 *
1512 * @param $settings
1513 */
1514 protected function save_settings( $settings ) {
1515 $page_settings_manager = SettingsManager::get_settings_managers( 'page' );
1516 $page_settings_manager->ajax_before_save_settings( $settings, $this->post->ID );
1517 $page_settings_manager->save_settings( $settings, $this->post->ID );
1518 }
1519
1520 /**
1521 * @since 2.1.3
1522 * @access protected
1523 */
1524 protected function print_elements( $elements_data ) {
1525 // Collect all data updaters that should be updated on runtime.
1526 $runtime_elements_iteration_actions = $this->get_runtime_elements_iteration_actions();
1527
1528 if ( $runtime_elements_iteration_actions ) {
1529 $this->iterate_elements( $elements_data, $runtime_elements_iteration_actions, 'render' );
1530 }
1531
1532 foreach ( $elements_data as $element_data ) {
1533 $element = Plugin::$instance->elements_manager->create_element_instance( $element_data );
1534
1535 if ( ! $element ) {
1536 continue;
1537 }
1538
1539 $element->print_element();
1540 }
1541 }
1542
1543 protected function register_document_controls() {
1544 $this->start_controls_section(
1545 'document_settings',
1546 [
1547 'label' => esc_html__( 'General Settings', 'elementor' ),
1548 'tab' => Controls_Manager::TAB_SETTINGS,
1549 ]
1550 );
1551
1552 $this->add_control(
1553 'post_title',
1554 [
1555 'label' => esc_html__( 'Title', 'elementor' ),
1556 'type' => Controls_Manager::TEXT,
1557 'default' => $this->post->post_title,
1558 'label_block' => true,
1559 'separator' => 'none',
1560 ]
1561 );
1562
1563 $post_type_object = get_post_type_object( $this->post->post_type );
1564
1565 $can_publish = $post_type_object && current_user_can( $post_type_object->cap->publish_posts );
1566 $is_published = self::STATUS_PUBLISH === $this->post->post_status || self::STATUS_PRIVATE === $this->post->post_status;
1567
1568 if ( $is_published || $can_publish || ! Plugin::$instance->editor->is_edit_mode() ) {
1569
1570 $statuses = $this->get_post_statuses();
1571 if ( 'future' === $this->get_main_post()->post_status ) {
1572 $statuses['future'] = esc_html__( 'Future', 'elementor' );
1573 }
1574
1575 $this->add_control(
1576 'post_status',
1577 [
1578 'label' => esc_html__( 'Status', 'elementor' ),
1579 'type' => Controls_Manager::SELECT,
1580 'default' => $this->get_main_post()->post_status,
1581 'options' => $statuses,
1582 ]
1583 );
1584 }
1585
1586 $this->end_controls_section();
1587 }
1588
1589 protected function get_post_statuses() {
1590 return get_post_statuses();
1591 }
1592
1593 protected function get_have_a_look_url() {
1594 return $this->get_permalink();
1595 }
1596
1597 public function handle_revisions_changed( $post_has_changed, $last_revision, $post ) {
1598 // In case default, didn't determine the changes.
1599 if ( ! $post_has_changed ) {
1600 $last_revision_id = $last_revision->ID;
1601 $last_revision_document = Plugin::instance()->documents->get( $last_revision_id );
1602 $post_document = Plugin::instance()->documents->get( $post->ID );
1603
1604 $last_revision_settings = $last_revision_document->get_settings();
1605 $post_settings = $post_document->get_settings();
1606
1607 // TODO: Its better to add crc32 signature for each revision and then only compare one part of the checksum.
1608 $post_has_changed = $last_revision_settings !== $post_settings;
1609 }
1610
1611 return $post_has_changed;
1612 }
1613
1614 private function add_handle_revisions_changed_filter() {
1615 add_filter( 'wp_save_post_revision_post_has_changed', [ $this, 'handle_revisions_changed' ], 10, 3 );
1616 }
1617
1618 private function remove_handle_revisions_changed_filter() {
1619 remove_filter( 'wp_save_post_revision_post_has_changed', [ $this, 'handle_revisions_changed' ] );
1620 }
1621
1622 private function get_runtime_elements_iteration_actions() {
1623 $runtime_elements_iteration_actions = [];
1624
1625 $elements_iteration_actions = $this->get_elements_iteration_actions();
1626
1627 foreach ( $elements_iteration_actions as $elements_iteration_action ) {
1628 if ( $elements_iteration_action->is_action_needed() ) {
1629 $runtime_elements_iteration_actions[] = $elements_iteration_action;
1630 }
1631 }
1632
1633 return $runtime_elements_iteration_actions;
1634 }
1635
1636 private function iterate_elements( $elements, $elements_iteration_actions, $mode ) {
1637 $unique_page_elements = [];
1638
1639 foreach ( $elements_iteration_actions as $elements_iteration_action ) {
1640 $elements_iteration_action->set_mode( $mode );
1641 }
1642
1643 Plugin::$instance->db->iterate_data( $elements, function( array $element_data ) use ( &$unique_page_elements, $elements_iteration_actions ) {
1644 $element_type = 'widget' === $element_data['elType'] ? $element_data['widgetType'] : $element_data['elType'];
1645
1646 $element = Plugin::$instance->elements_manager->create_element_instance( $element_data );
1647
1648 if ( $element ) {
1649 if ( ! in_array( $element_type, $unique_page_elements, true ) ) {
1650 $unique_page_elements[] = $element_type;
1651
1652 foreach ( $elements_iteration_actions as $elements_iteration_action ) {
1653 $elements_iteration_action->unique_element_action( $element );
1654 }
1655 }
1656
1657 foreach ( $elements_iteration_actions as $elements_iteration_action ) {
1658 $elements_iteration_action->element_action( $element );
1659 }
1660 }
1661
1662 return $element_data;
1663 } );
1664
1665 foreach ( $elements_iteration_actions as $elements_iteration_action ) {
1666 $elements_iteration_action->after_elements_iteration();
1667 }
1668 }
1669
1670 private function get_elements_iteration_actions() {
1671 if ( ! $this->elements_iteration_actions ) {
1672 $this->elements_iteration_actions[] = new Assets_Iteration_Action( $this );
1673 }
1674
1675 return $this->elements_iteration_actions;
1676 }
1677 }
1678