PluginProbe
Elementor Website Builder – more than just a page builder / 3.6.0-dev1
Elementor Website Builder – more than just a page builder v3.6.0-dev1
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-dev1, at core/base/document.php

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