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