namespace Elementor\Core;
use Elementor\Core\Base\Document;
use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
use Elementor\Core\DocumentTypes\Page;
use Elementor\Core\DocumentTypes\Post;
use Elementor\TemplateLibrary\Source_Local;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
* Elementor documents manager.
* Elementor documents manager handler class is responsible for registering and
* managing Elementor documents.
class Documents_Manager {
* Holds the list of all the registered types.
* Holds the list of all the registered documents.
protected $documents = [];
* Holds the current document.
* Holds the current document when changing to the requested post.
protected $switched_data = [];
* Documents manager constructor.
* Initializing the Elementor documents manager.
public function __construct() {
add_action( 'elementor/documents/register', [ $this, 'register_default_types' ], 0 );
add_action( 'elementor/ajax/register_actions', [ $this, 'register_ajax_actions' ] );
add_filter( 'post_row_actions', [ $this, 'filter_post_row_actions' ], 11, 2 );
add_filter( 'page_row_actions', [ $this, 'filter_post_row_actions' ], 11, 2 );
add_filter( 'user_has_cap', [ $this, 'remove_user_edit_cap' ], 10, 3 );
add_filter( 'elementor/editor/localize_settings', [ $this, 'localize_settings' ] );
add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] );
* Process ajax action handles when saving data and discarding changes.
* Fired by `elementor/ajax/register_actions` action.
* @param Ajax $ajax_manager An instance of the ajax manager.
public function register_ajax_actions( $ajax_manager ) {
$ajax_manager->register_ajax_action( 'save_builder', [ $this, 'ajax_save' ] );
$ajax_manager->register_ajax_action( 'discard_changes', [ $this, 'ajax_discard_changes' ] );
$ajax_manager->register_ajax_action( 'get_document_config', [ $this, 'ajax_get_document_config' ] );
* Register default types.
* Registers the default document types.
public function register_default_types() {
'post' => Post::get_class_full_name(), // BC.
'wp-post' => Post::get_class_full_name(),
'wp-page' => Page::get_class_full_name(),
foreach ( $default_types as $type => $class ) {
$this->register_document_type( $type, $class );
* Register document type.
* Registers a single document.
* @param string $type Document type name.
* @param string $class_name The name of the class that registers the document type.
* Full name with the namespace.
* @return Documents_Manager The updated document manager instance.
public function register_document_type( $type, $class_name ) {
$this->types[ $type ] = $class_name;
$cpt = $class_name::get_property( 'cpt' );
foreach ( $cpt as $post_type ) {
$this->cpt[ $post_type ] = $type;
if ( $class_name::get_property( 'register_type' ) ) {
Source_Local::add_template_type( $type );
* Retrieve the document data based on a post ID.
* @param int $post_id Post ID.
* @param bool $from_cache Optional. Whether to retrieve cached data. Default is true.
* @return false|Document Document data or false if post ID was not entered.
public function get( $post_id, $from_cache = true ) {
$post_id = absint( $post_id );
if ( ! $post_id || ! get_post( $post_id ) ) {
* Retrieve document post ID.
* Filters the document post ID.
* @param int $post_id The post ID of the document.
$post_id = apply_filters( 'elementor/documents/get/post_id', $post_id );
if ( ! $from_cache || ! isset( $this->documents[ $post_id ] ) ) {
$doc_type = $this->get_doc_type_by_id( $post_id );
$doc_type_class = $this->get_document_type( $doc_type );
$this->documents[ $post_id ] = new $doc_type_class( [
return $this->documents[ $post_id ];
* Retrieve a document after checking it exist and allowed to edit.
* @throws \Exception If the document is not found or the current user is not allowed to edit it.
public function get_with_permissions( $id ): Document {
$document = $this->get( $id );
throw new \Exception( 'Not found.' );
if ( ! $document->is_editable_by_current_user() ) {
throw new \Exception( 'Access denied.' );
* A `void` version for `get_with_permissions`.
* @throws \Exception If the document is not found or the current user is not allowed to edit it.
public function check_permissions( $id ) {
$this->get_with_permissions( $id );
* Get document or autosave.
* Retrieve either the document or the autosave.
* @param int $id Optional. Post ID. Default is `0`.
* @param int $user_id Optional. User ID. Default is `0`.
* @return false|Document The document if it exist, False otherwise.
public function get_doc_or_auto_save( $id, $user_id = 0 ) {
$document = $this->get( $id );
if ( $document && $document->get_autosave_id( $user_id ) ) {
$document = $document->get_autosave( $user_id );
* Get document for frontend.
* Retrieve the document for frontend use.
* @param int $post_id Optional. Post ID. Default is `0`.
* @return false|Document The document if it exist, False otherwise.
public function get_doc_for_frontend( $post_id ) {
$preview_id = (int) Utils::get_super_global_value( $_GET, 'preview_id' );
$is_preview = is_preview();
$is_nonce_verify = wp_verify_nonce( Utils::get_super_global_value( $_GET, 'preview_nonce' ), 'post_preview_' . $preview_id );
if ( ( $is_preview && $is_nonce_verify ) || Plugin::$instance->preview->is_preview_mode() ) {
$document = $this->get_doc_or_auto_save( $post_id, get_current_user_id() );
$document = $this->get( $post_id );
* Retrieve the type of any given document.
* @param string $fallback
* @return Document|bool The type of the document.
public function get_document_type( $type, $fallback = 'post' ) {
$types = $this->get_document_types();
if ( isset( $types[ $type ] ) ) {
if ( isset( $types[ $fallback ] ) ) {
return $types[ $fallback ];
* Retrieve the all the registered document types.
* @param array $args Optional. An array of key => value arguments to match against
* the properties. Default is empty array.
* @param string $operator Optional. The logical operation to perform. 'or' means only one
* element from the array needs to match; 'and' means all elements
* must match; 'not' means no elements may match. Default 'and'.
* @return Document[] All the registered document types.
public function get_document_types( $args = [], $operator = 'and' ) {
if ( ! empty( $args ) ) {
$types_properties = $this->get_types_properties();
$filtered = wp_filter_object_list( $types_properties, $args, $operator );
return array_intersect_key( $this->types, $filtered );
* Get document types with their properties.
* @return array A list of properties arrays indexed by the type.
public function get_types_properties() {
foreach ( $this->get_document_types() as $type => $class ) {
$types_properties[ $type ] = $class::get_properties();
return $types_properties;
* Create a new document using any given parameters.
* @param string $type Document type.
* @param array $post_data An array containing the post data.
* @param array $meta_data An array containing the post meta data.
* @return Document The type of the document.
public function create( $type, $post_data = [], $meta_data = [] ) {
$class = $this->get_document_type( $type, false );
return new \WP_Error( 500, sprintf( 'Type %s does not exist.', $type ) );
if ( empty( $post_data['post_title'] ) ) {
$post_data['post_title'] = esc_html__( 'Elementor', 'elementor' );
if ( 'post' !== $type ) {
$post_data['post_title'] = sprintf(
/* translators: %s: Document title. */
__( 'Elementor %s', 'elementor' ),
call_user_func( [ $class, 'get_title' ] )
$meta_data['_elementor_edit_mode'] = 'builder';
// Save the type as-is for plugins that hooked at `wp_insert_post`.
$meta_data[ Document::TYPE_META_KEY ] = $type;
$post_data['meta_input'] = $meta_data;
$post_types = $class::get_property( 'cpt' );
if ( ! empty( $post_types[0] ) && empty( $post_data['post_type'] ) ) {
$post_data['post_type'] = $post_types[0];
$post_id = wp_insert_post( $post_data );
if ( ! empty( $update_title ) ) {
$post_data['ID'] = $post_id;
$post_data['post_title'] .= ' #' . $post_id;
// The meta doesn't need update.
unset( $post_data['meta_input'] );
wp_update_post( $post_data );
/** @var Document $document */
$document = new $class( [
// Let the $document to re-save the template type by his way + version.
* Remove user edit capabilities if document is not editable.
* Filters the user capabilities to disable editing in admin.
* @param array $allcaps An array of all the user's capabilities.
* @param array $caps Actual capabilities for meta capability.
* @param array $args Optional parameters passed to has_cap(), typically object ID.
public function remove_user_edit_cap( $allcaps, $caps, $args ) {
if ( ! in_array( $pagenow, [ 'post.php', 'edit.php' ], true ) ) {
// Don't touch not existing or not allowed caps.
if ( empty( $caps[0] ) || empty( $allcaps[ $caps[0] ] ) ) {
if ( 'edit_post' !== $capability ) {
if ( empty( $args[2] ) ) {
$document = Plugin::$instance->documents->get( $post_id );
$allcaps[ $caps[0] ] = $document::get_property( 'is_editable' );
* Filter Post Row Actions.
* Let the Document to filter the array of row action links on the Posts list table.
public function filter_post_row_actions( $actions, $post ) {
$document = $this->get( $post->ID );
$actions = $document->filter_admin_row_actions( $actions );
* Save document data using ajax.
* Save the document on the builder using ajax, when saving the changes, and refresh the editor.