documents-manager.php (17746B)
1 <?php 2 namespace Elementor\Core; 3 4 use Elementor\Core\Base\Document; 5 use Elementor\Core\Common\Modules\Ajax\Module as Ajax; 6 use Elementor\Core\DocumentTypes\Page; 7 use Elementor\Core\DocumentTypes\Post; 8 use Elementor\Plugin; 9 use Elementor\TemplateLibrary\Source_Local; 10 use Elementor\Utils; 11 12 if ( ! defined( 'ABSPATH' ) ) { 13 exit; // Exit if accessed directly 14 } 15 16 /** 17 * Elementor documents manager. 18 * 19 * Elementor documents manager handler class is responsible for registering and 20 * managing Elementor documents. 21 * 22 * @since 2.0.0 23 */ 24 class Documents_Manager { 25 26 /** 27 * Registered types. 28 * 29 * Holds the list of all the registered types. 30 * 31 * @since 2.0.0 32 * @access protected 33 * 34 * @var Document[] 35 */ 36 protected $types = []; 37 38 /** 39 * Registered documents. 40 * 41 * Holds the list of all the registered documents. 42 * 43 * @since 2.0.0 44 * @access protected 45 * 46 * @var Document[] 47 */ 48 protected $documents = []; 49 50 /** 51 * Current document. 52 * 53 * Holds the current document. 54 * 55 * @since 2.0.0 56 * @access protected 57 * 58 * @var Document 59 */ 60 protected $current_doc; 61 62 /** 63 * Switched data. 64 * 65 * Holds the current document when changing to the requested post. 66 * 67 * @since 2.0.0 68 * @access protected 69 * 70 * @var array 71 */ 72 protected $switched_data = []; 73 74 protected $cpt = []; 75 76 /** 77 * Documents manager constructor. 78 * 79 * Initializing the Elementor documents manager. 80 * 81 * @since 2.0.0 82 * @access public 83 */ 84 public function __construct() { 85 add_action( 'elementor/documents/register', [ $this, 'register_default_types' ], 0 ); 86 add_action( 'elementor/ajax/register_actions', [ $this, 'register_ajax_actions' ] ); 87 add_filter( 'post_row_actions', [ $this, 'filter_post_row_actions' ], 11, 2 ); 88 add_filter( 'page_row_actions', [ $this, 'filter_post_row_actions' ], 11, 2 ); 89 add_filter( 'user_has_cap', [ $this, 'remove_user_edit_cap' ], 10, 3 ); 90 add_filter( 'elementor/editor/localize_settings', [ $this, 'localize_settings' ] ); 91 } 92 93 /** 94 * Register ajax actions. 95 * 96 * Process ajax action handles when saving data and discarding changes. 97 * 98 * Fired by `elementor/ajax/register_actions` action. 99 * 100 * @since 2.0.0 101 * @access public 102 * 103 * @param Ajax $ajax_manager An instance of the ajax manager. 104 */ 105 public function register_ajax_actions( $ajax_manager ) { 106 $ajax_manager->register_ajax_action( 'save_builder', [ $this, 'ajax_save' ] ); 107 $ajax_manager->register_ajax_action( 'discard_changes', [ $this, 'ajax_discard_changes' ] ); 108 $ajax_manager->register_ajax_action( 'get_document_config', [ $this, 'ajax_get_document_config' ] ); 109 } 110 111 /** 112 * Register default types. 113 * 114 * Registers the default document types. 115 * 116 * @since 2.0.0 117 * @access public 118 */ 119 public function register_default_types() { 120 $default_types = [ 121 'post' => Post::get_class_full_name(), // BC. 122 'wp-post' => Post::get_class_full_name(), 123 'wp-page' => Page::get_class_full_name(), 124 ]; 125 126 foreach ( $default_types as $type => $class ) { 127 $this->register_document_type( $type, $class ); 128 } 129 } 130 131 /** 132 * Register document type. 133 * 134 * Registers a single document. 135 * 136 * @since 2.0.0 137 * @access public 138 * 139 * @param string $type Document type name. 140 * @param string $class The name of the class that registers the document type. 141 * Full name with the namespace. 142 * 143 * @return Documents_Manager The updated document manager instance. 144 */ 145 public function register_document_type( $type, $class ) { 146 $this->types[ $type ] = $class; 147 148 $cpt = $class::get_property( 'cpt' ); 149 150 if ( $cpt ) { 151 foreach ( $cpt as $post_type ) { 152 $this->cpt[ $post_type ] = $type; 153 } 154 } 155 156 if ( $class::get_property( 'register_type' ) ) { 157 Source_Local::add_template_type( $type ); 158 } 159 160 return $this; 161 } 162 163 /** 164 * Get document. 165 * 166 * Retrieve the document data based on a post ID. 167 * 168 * @since 2.0.0 169 * @access public 170 * 171 * @param int $post_id Post ID. 172 * @param bool $from_cache Optional. Whether to retrieve cached data. Default is true. 173 * 174 * @return false|Document Document data or false if post ID was not entered. 175 */ 176 public function get( $post_id, $from_cache = true ) { 177 $this->register_types(); 178 179 $post_id = absint( $post_id ); 180 181 if ( ! $post_id || ! get_post( $post_id ) ) { 182 return false; 183 } 184 185 /** 186 * Retrieve document post ID. 187 * 188 * Filters the document post ID. 189 * 190 * @since 2.0.7 191 * 192 * @param int $post_id The post ID of the document. 193 */ 194 $post_id = apply_filters( 'elementor/documents/get/post_id', $post_id ); 195 196 if ( ! $from_cache || ! isset( $this->documents[ $post_id ] ) ) { 197 198 if ( wp_is_post_autosave( $post_id ) ) { 199 $post_type = get_post_type( wp_get_post_parent_id( $post_id ) ); 200 } else { 201 $post_type = get_post_type( $post_id ); 202 } 203 204 $doc_type = 'post'; 205 206 if ( isset( $this->cpt[ $post_type ] ) ) { 207 $doc_type = $this->cpt[ $post_type ]; 208 } 209 210 $meta_type = get_post_meta( $post_id, Document::TYPE_META_KEY, true ); 211 212 if ( $meta_type && isset( $this->types[ $meta_type ] ) ) { 213 $doc_type = $meta_type; 214 } 215 216 $doc_type_class = $this->get_document_type( $doc_type ); 217 $this->documents[ $post_id ] = new $doc_type_class( [ 218 'post_id' => $post_id, 219 ] ); 220 } 221 222 return $this->documents[ $post_id ]; 223 } 224 225 /** 226 * Get document or autosave. 227 * 228 * Retrieve either the document or the autosave. 229 * 230 * @since 2.0.0 231 * @access public 232 * 233 * @param int $id Optional. Post ID. Default is `0`. 234 * @param int $user_id Optional. User ID. Default is `0`. 235 * 236 * @return false|Document The document if it exist, False otherwise. 237 */ 238 public function get_doc_or_auto_save( $id, $user_id = 0 ) { 239 $document = $this->get( $id ); 240 if ( $document && $document->get_autosave_id( $user_id ) ) { 241 $document = $document->get_autosave( $user_id ); 242 } 243 244 return $document; 245 } 246 247 /** 248 * Get document for frontend. 249 * 250 * Retrieve the document for frontend use. 251 * 252 * @since 2.0.0 253 * @access public 254 * 255 * @param int $post_id Optional. Post ID. Default is `0`. 256 * 257 * @return false|Document The document if it exist, False otherwise. 258 */ 259 public function get_doc_for_frontend( $post_id ) { 260 if ( is_preview() || Plugin::$instance->preview->is_preview_mode() ) { 261 $document = $this->get_doc_or_auto_save( $post_id, get_current_user_id() ); 262 } else { 263 $document = $this->get( $post_id ); 264 } 265 266 return $document; 267 } 268 269 /** 270 * Get document type. 271 * 272 * Retrieve the type of any given document. 273 * 274 * @since 2.0.0 275 * @access public 276 * 277 * @param string $type 278 * 279 * @param string $fallback 280 * 281 * @return Document|bool The type of the document. 282 */ 283 public function get_document_type( $type, $fallback = 'post' ) { 284 $types = $this->get_document_types(); 285 286 if ( isset( $types[ $type ] ) ) { 287 return $types[ $type ]; 288 } 289 290 if ( isset( $types[ $fallback ] ) ) { 291 return $types[ $fallback ]; 292 } 293 294 return false; 295 } 296 297 /** 298 * Get document types. 299 * 300 * Retrieve the all the registered document types. 301 * 302 * @since 2.0.0 303 * @access public 304 * 305 * @param array $args Optional. An array of key => value arguments to match against 306 * the properties. Default is empty array. 307 * @param string $operator Optional. The logical operation to perform. 'or' means only one 308 * element from the array needs to match; 'and' means all elements 309 * must match; 'not' means no elements may match. Default 'and'. 310 * 311 * @return Document[] All the registered document types. 312 */ 313 public function get_document_types( $args = [], $operator = 'and' ) { 314 $this->register_types(); 315 316 if ( ! empty( $args ) ) { 317 $types_properties = $this->get_types_properties(); 318 319 $filtered = wp_filter_object_list( $types_properties, $args, $operator ); 320 321 return array_intersect_key( $this->types, $filtered ); 322 } 323 324 return $this->types; 325 } 326 327 /** 328 * Get document types with their properties. 329 * 330 * @return array A list of properties arrays indexed by the type. 331 */ 332 public function get_types_properties() { 333 $types_properties = []; 334 335 foreach ( $this->get_document_types() as $type => $class ) { 336 $types_properties[ $type ] = $class::get_properties(); 337 } 338 return $types_properties; 339 } 340 341 /** 342 * Create a document. 343 * 344 * Create a new document using any given parameters. 345 * 346 * @since 2.0.0 347 * @access public 348 * 349 * @param string $type Document type. 350 * @param array $post_data An array containing the post data. 351 * @param array $meta_data An array containing the post meta data. 352 * 353 * @return Document The type of the document. 354 */ 355 public function create( $type, $post_data = [], $meta_data = [] ) { 356 $class = $this->get_document_type( $type, false ); 357 358 if ( ! $class ) { 359 return new \WP_Error( 500, sprintf( 'Type %s does not exist.', $type ) ); 360 } 361 362 if ( empty( $post_data['post_title'] ) ) { 363 $post_data['post_title'] = esc_html__( 'Elementor', 'elementor' ); 364 if ( 'post' !== $type ) { 365 $post_data['post_title'] = sprintf( 366 /* translators: %s: Document title */ 367 __( 'Elementor %s', 'elementor' ), 368 call_user_func( [ $class, 'get_title' ] ) 369 ); 370 } 371 $update_title = true; 372 } 373 374 $meta_data['_elementor_edit_mode'] = 'builder'; 375 376 // Save the type as-is for plugins that hooked at `wp_insert_post`. 377 $meta_data[ Document::TYPE_META_KEY ] = $type; 378 379 $post_data['meta_input'] = $meta_data; 380 381 $post_id = wp_insert_post( $post_data ); 382 383 if ( ! empty( $update_title ) ) { 384 $post_data['ID'] = $post_id; 385 $post_data['post_title'] .= ' #' . $post_id; 386 387 // The meta doesn't need update. 388 unset( $post_data['meta_input'] ); 389 390 wp_update_post( $post_data ); 391 } 392 393 /** @var Document $document */ 394 $document = new $class( [ 395 'post_id' => $post_id, 396 ] ); 397 398 // Let the $document to re-save the template type by his way + version. 399 $document->save( [] ); 400 401 return $document; 402 } 403 404 /** 405 * Remove user edit capabilities if document is not editable. 406 * 407 * Filters the user capabilities to disable editing in admin. 408 * 409 * @param array $allcaps An array of all the user's capabilities. 410 * @param array $caps Actual capabilities for meta capability. 411 * @param array $args Optional parameters passed to has_cap(), typically object ID. 412 * 413 * @return array 414 */ 415 public function remove_user_edit_cap( $allcaps, $caps, $args ) { 416 global $pagenow; 417 418 if ( ! in_array( $pagenow, [ 'post.php', 'edit.php' ], true ) ) { 419 return $allcaps; 420 } 421 422 // Don't touch not existing or not allowed caps. 423 if ( empty( $caps[0] ) || empty( $allcaps[ $caps[0] ] ) ) { 424 return $allcaps; 425 } 426 427 $capability = $args[0]; 428 429 if ( 'edit_post' !== $capability ) { 430 return $allcaps; 431 } 432 433 if ( empty( $args[2] ) ) { 434 return $allcaps; 435 } 436 437 $post_id = $args[2]; 438 439 $document = Plugin::$instance->documents->get( $post_id ); 440 441 if ( ! $document ) { 442 return $allcaps; 443 } 444 445 $allcaps[ $caps[0] ] = $document::get_property( 'is_editable' ); 446 447 return $allcaps; 448 } 449 450 /** 451 * Filter Post Row Actions. 452 * 453 * Let the Document to filter the array of row action links on the Posts list table. 454 * 455 * @param array $actions 456 * @param \WP_Post $post 457 * 458 * @return array 459 */ 460 public function filter_post_row_actions( $actions, $post ) { 461 $document = $this->get( $post->ID ); 462 463 if ( $document ) { 464 $actions = $document->filter_admin_row_actions( $actions ); 465 } 466 467 return $actions; 468 } 469 470 /** 471 * Save document data using ajax. 472 * 473 * Save the document on the builder using ajax, when saving the changes, and refresh the editor. 474 * 475 * @since 2.0.0 476 * @access public 477 * 478 * @param $request Post ID. 479 * 480 * @throws \Exception If current user don't have permissions to edit the post or the post is not using Elementor. 481 * 482 * @return array The document data after saving. 483 */ 484 public function ajax_save( $request ) { 485 $document = $this->get( $request['editor_post_id'] ); 486 487 if ( ! $document->is_built_with_elementor() || ! $document->is_editable_by_current_user() ) { 488 throw new \Exception( 'Access denied.' ); 489 } 490 491 $this->switch_to_document( $document ); 492 493 // Set the post as global post. 494 Plugin::$instance->db->switch_to_post( $document->get_post()->ID ); 495 496 $status = Document::STATUS_DRAFT; 497 498 if ( isset( $request['status'] ) && in_array( $request['status'], [ Document::STATUS_PUBLISH, Document::STATUS_PRIVATE, Document::STATUS_PENDING, Document::STATUS_AUTOSAVE ], true ) ) { 499 $status = $request['status']; 500 } 501 502 if ( Document::STATUS_AUTOSAVE === $status ) { 503 // If the post is a draft - save the `autosave` to the original draft. 504 // Allow a revision only if the original post is already published. 505 if ( in_array( $document->get_post()->post_status, [ Document::STATUS_PUBLISH, Document::STATUS_PRIVATE ], true ) ) { 506 $document = $document->get_autosave( 0, true ); 507 } 508 } 509 510 // Set default page template because the footer-saver doesn't send default values, 511 // But if the template was changed from canvas to default - it needed to save. 512 if ( Utils::is_cpt_custom_templates_supported() && ! isset( $request['settings']['template'] ) ) { 513 $request['settings']['template'] = 'default'; 514 } 515 516 $data = [ 517 'elements' => $request['elements'], 518 'settings' => $request['settings'], 519 ]; 520 521 $document->save( $data ); 522 523 // Refresh after save. 524 $document = $this->get( $document->get_post()->ID, false ); 525 526 $return_data = [ 527 'status' => $document->get_post()->post_status, 528 'config' => [ 529 'document' => [ 530 'last_edited' => $document->get_last_edited(), 531 'urls' => [ 532 'wp_preview' => $document->get_wp_preview_url(), 533 ], 534 ], 535 ], 536 ]; 537 538 /** 539 * Returned documents ajax saved data. 540 * 541 * Filters the ajax data returned when saving the post on the builder. 542 * 543 * @since 2.0.0 544 * 545 * @param array $return_data The returned data. 546 * @param Document $document The document instance. 547 */ 548 $return_data = apply_filters( 'elementor/documents/ajax_save/return_data', $return_data, $document ); 549 550 return $return_data; 551 } 552 553 /** 554 * Ajax discard changes. 555 * 556 * Load the document data from an autosave, deleting unsaved changes. 557 * 558 * @since 2.0.0 559 * @access public 560 * 561 * @param $request 562 * 563 * @return bool True if changes discarded, False otherwise. 564 */ 565 public function ajax_discard_changes( $request ) { 566 $document = $this->get( $request['editor_post_id'] ); 567 568 $autosave = $document->get_autosave(); 569 570 if ( $autosave ) { 571 $success = $autosave->delete(); 572 } else { 573 $success = true; 574 } 575 576 return $success; 577 } 578 579 public function ajax_get_document_config( $request ) { 580 $post_id = absint( $request['id'] ); 581 582 Plugin::$instance->editor->set_post_id( $post_id ); 583 584 $document = $this->get_doc_or_auto_save( $post_id ); 585 586 if ( ! $document ) { 587 throw new \Exception( 'Not Found.' ); 588 } 589 590 if ( ! $document->is_editable_by_current_user() ) { 591 throw new \Exception( 'Access denied.' ); 592 } 593 594 // Set the global data like $post, $authordata and etc 595 Plugin::$instance->db->switch_to_post( $post_id ); 596 597 $this->switch_to_document( $document ); 598 599 // Change mode to Builder 600 $document->set_is_built_with_elementor( true ); 601 602 $doc_config = $document->get_config(); 603 604 return $doc_config; 605 } 606 607 /** 608 * Switch to document. 609 * 610 * Change the document to any new given document type. 611 * 612 * @since 2.0.0 613 * @access public 614 * 615 * @param Document $document The document to switch to. 616 */ 617 public function switch_to_document( $document ) { 618 // If is already switched, or is the same post, return. 619 if ( $this->current_doc === $document ) { 620 $this->switched_data[] = false; 621 return; 622 } 623 624 $this->switched_data[] = [ 625 'switched_doc' => $document, 626 'original_doc' => $this->current_doc, // Note, it can be null if the global isn't set 627 ]; 628 629 $this->current_doc = $document; 630 } 631 632 /** 633 * Restore document. 634 * 635 * Rollback to the original document. 636 * 637 * @since 2.0.0 638 * @access public 639 */ 640 public function restore_document() { 641 $data = array_pop( $this->switched_data ); 642 643 // If not switched, return. 644 if ( ! $data ) { 645 return; 646 } 647 648 $this->current_doc = $data['original_doc']; 649 } 650 651 /** 652 * Get current document. 653 * 654 * Retrieve the current document. 655 * 656 * @since 2.0.0 657 * @access public 658 * 659 * @return Document The current document. 660 */ 661 public function get_current() { 662 return $this->current_doc; 663 } 664 665 public function localize_settings( $settings ) { 666 $translations = []; 667 668 foreach ( $this->get_document_types() as $type => $class ) { 669 $translations[ $type ] = $class::get_title(); 670 } 671 672 return array_replace_recursive( $settings, [ 673 'i18n' => $translations, 674 ] ); 675 } 676 677 private function register_types() { 678 if ( ! did_action( 'elementor/documents/register' ) ) { 679 /** 680 * Register Elementor documents. 681 * 682 * @since 2.0.0 683 * 684 * @param Documents_Manager $this The document manager instance. 685 */ 686 do_action( 'elementor/documents/register', $this ); 687 } 688 } 689 690 /** 691 * Get create new post URL. 692 * 693 * Retrieve a custom URL for creating a new post/page using Elementor. 694 * 695 * @param string $post_type Optional. Post type slug. Default is 'page'. 696 * @param string|null $template_type Optional. Query arg 'template_type'. Default is null. 697 * 698 * @return string A URL for creating new post using Elementor. 699 */ 700 public static function get_create_new_post_url( $post_type = 'page', $template_type = null ) { 701 $query_args = [ 702 'action' => 'elementor_new_post', 703 'post_type' => $post_type, 704 ]; 705 706 if ( $template_type ) { 707 $query_args['template_type'] = $template_type; 708 } 709 710 $new_post_url = add_query_arg( $query_args, admin_url( 'edit.php' ) ); 711 712 $new_post_url = add_query_arg( '_wpnonce', wp_create_nonce( 'elementor_action_new_post' ), $new_post_url ); 713 714 return $new_post_url; 715 } 716 }