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

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