File "components-rest-api.php"

Full Path: /home/capoeirajd/www/wp-content/plugins/elementor/modules/components/components-rest-api.php
File size: 19.41 KB
MIME-type: text/x-php
Charset: utf-8

<?php

namespace Elementor\Modules\Components;

use Elementor\Core\Base\Document;
use Elementor\Core\Utils\Api\Error_Builder;
use Elementor\Core\Utils\Api\Response_Builder;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\Components\Documents\Component;
use Elementor\Modules\Components\OverridableProps\Component_Overridable_Props_Parser;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Components_REST_API {
	const API_NAMESPACE = 'elementor/v1';
	const API_BASE = 'components';
	const LOCK_DOCUMENT_TYPE_NAME = 'components';
	const STYLES_ROUTE = 'styles';
	const MAX_COMPONENTS = 100;

	private $repository = null;
	public function register_hooks() {
		add_action( 'rest_api_init', fn() => $this->register_routes() );
	}

	private function get_repository() {
		if ( ! $this->repository ) {
			$this->repository = new Components_Repository();
		}

		return $this->repository;
	}

	/**
	 * @return Component_Lock_Manager instance
	 */
	private function get_component_lock_manager() {
		return Component_Lock_Manager::get_instance();
	}

	private function register_routes() {
		register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE, [
			[
				'methods' => 'GET',
				'callback' => fn() => $this->route_wrapper( fn() => $this->get_components() ),
				'permission_callback' => fn() => current_user_can( 'edit_posts' ),
			],
		] );

		register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/' . self::STYLES_ROUTE, [
			[
				'methods' => 'GET',
				'callback' => fn() => $this->route_wrapper( fn() => $this->get_styles() ),
				'permission_callback' => fn() => current_user_can( 'edit_posts' ),
			],
		] );

		register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE, [
			[
				'methods' => 'POST',
				'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->create_components( $request ) ),
				'permission_callback' => fn() => current_user_can( 'manage_options' ),
				'args' => [
					'status' => [
						'type' => 'string',
						'enum' => [ Document::STATUS_PUBLISH, Document::STATUS_DRAFT, Document::STATUS_AUTOSAVE ],
						'required' => true,
					],
					'items' => [
						'type' => 'array',
						'required' => true,
						'items' => [
							'type' => 'object',
							'properties' => [
								'uid' => [
									'type' => 'string',
									'required' => true,
								],
								'title' => [
									'type' => 'string',
									'required' => true,
									'minLength' => 2,
									'maxLength' => 200,
								],
								'elements' => [
									'type' => 'array',
									'required' => true,
									'items' => [
										'type' => 'object',
									],
								],
								'settings' => [
									'type' => 'object',
									'required' => false,
								],
							],
						],
					],
				],
			],
		] );

		register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/create-validate', [
			[
				'methods' => 'POST',
				'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->create_validate_components( $request ) ),
				'permission_callback' => fn() => current_user_can( 'manage_options' ),
				'args' => [
					'items' => [
						'type' => 'array',
						'required' => true,
						'items' => [
							'type' => 'object',
							'properties' => [
								'uid' => [
									'type' => 'string',
									'required' => true,
								],
								'title' => [
									'type' => 'string',
									'required' => true,
									'minLength' => 2,
									'maxLength' => 200,
								],
								'elements' => [
									'type' => 'array',
									'required' => true,
									'items' => [
										'type' => 'object',
									],
								],
								'settings' => [
									'type' => 'object',
									'required' => false,
								],
							],
						],
					],
				],
			],
		] );

		register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/overridable-props', [
			[
				'methods' => 'GET',
				'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->get_overridable_props( $request ) ),
				'permission_callback' => fn() => current_user_can( 'edit_posts' ),
				'args' => [
					'componentId' => [
						'type' => 'integer',
						'required' => true,
						'description' => 'The component ID to get overridable props for',
					],
				],
			],
		] );

		register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/status', [
			[
				'methods' => 'PUT',
				'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->update_statuses( $request ) ),
				'permission_callback' => fn() => current_user_can( 'manage_options' ),
				'args' => [
					'status' => [
						'type' => 'string',
						'required' => true,
						'enum' => [ Document::STATUS_PUBLISH ],
					],
					'ids' => [
						'type' => 'array',
						'required' => true,
						'items' => [
							'type' => 'number',
							'required' => true,
						],
					],
				],
			],
		] );

		register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/lock', [
			[
				'methods' => 'POST',
				'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->lock_component( $request ) ),
				'permission_callback' => fn() => current_user_can( 'manage_options' ),
				'args' => [
					'componentId' => [
						'type' => 'number',
						'required' => true,
						'description' => 'The component ID to unlock',
					],
				],
			],
		] );

		register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/unlock', [
			[
				'methods' => 'POST',
				'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->unlock_component( $request ) ),
				'permission_callback' => fn() => current_user_can( 'manage_options' ),
				'args' => [
					'componentId' => [
						'type' => 'number',
						'required' => true,
						'description' => 'The component ID to unlock',
					],
				],
			],
		] );

		register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/lock-status', [
			[
				'methods' => 'GET',
				'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->get_lock_status( $request ) ),
				'permission_callback' => fn() => current_user_can( 'manage_options' ),
				'args' => [
					'componentId' => [
						'type' => 'string',
						'required' => true,
						'description' => 'The component ID to check lock status',
					],
				],
			],
		] );

		register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/archive', [
			[
				'methods' => 'POST',
				'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->archive_components( $request ) ),
				'permission_callback' => fn() => current_user_can( 'manage_options' ),
				'args' => [
					'componentIds' => [
						'type' => 'array',
						'items' => [
							'type' => 'number',
							'required' => true,
						],
						'required' => true,
						'description' => 'The component IDs to archive',
					],
					'status' => [
						'type' => 'string',
						'enum' => [ Document::STATUS_PUBLISH, Document::STATUS_DRAFT, Document::STATUS_AUTOSAVE ],
						'required' => true,
					],
				],
			],
		] );

		register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE . '/update-titles', [
			[
				'methods' => 'POST',
				'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->update_components_title( $request ) ),
				'permission_callback' => fn() => current_user_can( 'manage_options' ),
				'args' => [
					'components' => [
						'type' => 'array',
						'required' => true,
						'items' => [
							'type' => 'object',
							'properties' => [
								'componentId' => [
									'type' => 'number',
									'required' => true,
									'description' => 'The component ID to update title',
								],
								'title' => [
									'type' => 'string',
									'required' => true,
									'description' => 'The new title for the component',
								],
							],
						],
					],
					'status' => [
						'type' => 'string',
						'enum' => [ Document::STATUS_PUBLISH, Document::STATUS_DRAFT, Document::STATUS_AUTOSAVE ],
						'required' => true,
					],
				],
			],
		] );
	}

	private function get_components() {
		$components = $this->get_repository()->all();

		$components_list = array_values( $components
			->map( fn( $component ) => [
				'id' => $component['id'],
				'name' => $component['title'],
				'uid' => $component['uid'],
				'isArchived' => $component['is_archived'] ?? false,
			] )
		->all() );

		return Response_Builder::make( $components_list )->build();
	}

	private function get_styles() {
		$components = $this->get_repository()->all();

		$styles = [];
		$components->each( function( $component ) use ( &$styles ) {
			$styles[ $component['id'] ] = $component['styles'];
		} );

		return Response_Builder::make( $styles )->build();
	}

	private function get_overridable_props( \WP_REST_Request $request ) {
		$component_id = (int) $request->get_param( 'componentId' );

		if ( ! $component_id ) {
			return Error_Builder::make( 'invalid_component_id' )
				->set_status( 400 )
				->set_message( __( 'Invalid component ID', 'elementor' ) )
				->build();
		}

		$document = $this->get_repository()->get( $component_id );

		if ( ! $document ) {
			return Error_Builder::make( 'component_not_found' )
				->set_status( 404 )
				->set_message( __( 'Component not found', 'elementor' ) )
				->build();
		}

		$overridable = $document->get_json_meta( Component::OVERRIDABLE_PROPS_META_KEY ) ?? null;

		if ( empty( $overridable ) ) {
			$overridable = null;
		}

		return Response_Builder::make( $overridable )->build();
	}

	private function create_components( \WP_REST_Request $request ) {
		$save_status = $request->get_param( 'status' );

		$items = Collection::make( $request->get_param( 'items' ) );
		$components = $this->get_repository()->all();

		$result = Save_Components_Validator::make( $components )->validate( $items );

		if ( ! $result['success'] ) {
			return Error_Builder::make( 'components_validation_failed' )
				->set_status( 422 )
				->set_message( 'Validation failed: ' . implode( ', ', $result['messages'] ) )
				->build();
		}

		$circular_result = Circular_Dependency_Validator::make()->validate_new_components( $items );

		if ( ! $circular_result['success'] ) {
			return Error_Builder::make( 'circular_dependency_detected' )
				->set_status( 422 )
				->set_message( __( "Can't add this component - components that contain each other can't be nested.", 'elementor' ) )
				->set_meta( [ 'caused_by' => $circular_result['messages'] ] )
				->build();
		}

		$non_atomic_result = Non_Atomic_Widget_Validator::make()->validate_items( $items );

		if ( ! $non_atomic_result['success'] ) {
			return Error_Builder::make( Non_Atomic_Widget_Validator::ERROR_CODE )
				->set_status( 422 )
				->set_message( __( 'Components require atomic elements only. Remove widgets to create this component.', 'elementor' ) )
				->set_meta( [ 'non_atomic_elements' => $non_atomic_result['non_atomic_elements'] ] )
				->build();
		}

		$validation_errors = [];

		$created = $items->map_with_keys( function ( $item ) use ( $save_status, &$validation_errors ) {
			$title = sanitize_text_field( $item['title'] );
			$content = $item['elements'];
			$uid = $item['uid'];

			try {
				$settings = isset( $item['settings'] ) ? $this->parse_settings( $item['settings'] ) : [];

				$status = Document::STATUS_AUTOSAVE === $save_status
					? Document::STATUS_DRAFT
					: $save_status;

				$component_id = $this->get_repository()->create( $title, $content, $status, $uid, $settings );

				return [ $uid => $component_id ];
			} catch ( \Exception $e ) {
				$validation_errors[ $uid ] = $e->getMessage();
				return [ $uid => null ];
			}
		} );

		if ( ! empty( $validation_errors ) ) {
			return Error_Builder::make( 'settings_validation_failed' )
				->set_status( 422 )
				->set_message( 'Settings validation failed: ' . json_encode( $validation_errors ) )
				->build();
		}

		return Response_Builder::make( $created->all() )
			->set_status( 201 )
			->build();
	}

	private function update_statuses( \WP_REST_Request $request ) {
		$result = Collection::make( $request->get_param( 'ids' ) )
			->reduce(
				function ( $result, int $component_id ) {
					$component = $this->get_repository()->get( $component_id );

					if ( ! $component ) {
						$result['failed'][] = $component_id;
						return $result;
					}

					$publish_result = $this->get_repository()->publish_component( $component );

					$result[ $publish_result ? 'success' : 'failed' ][] = $component_id;

					return $result;
				},
				[
					'success' => [],
					'failed' => [],
				]
			);

		return Response_Builder::make( $result )->build();
	}

	private function lock_component( \WP_REST_Request $request ) {
		$component_id = $request->get_param( 'componentId' );
		try {
			$success = $this->get_component_lock_manager()->lock( $component_id );
		} catch ( \Exception $e ) {
			error_log( 'Components REST API lock_component error: ' . $e->getMessage() );
			return Error_Builder::make( 'lock_failed' )
				->set_status( 500 )
				->set_message( __( 'Failed to lock component', 'elementor' ) )
				->build();
		}

		if ( ! $success ) {
			return Error_Builder::make( 'lock_failed' )
				->set_status( 500 )
				->set_message( __( 'Failed to lock component', 'elementor' ) )
				->build();
		}

		return Response_Builder::make( [ 'locked' => $success ] )->build();
	}

	private function unlock_component( \WP_REST_Request $request ) {
		$component_id = $request->get_param( 'componentId' );
		try {
			$success = $this->get_component_lock_manager()->unlock( $component_id );
		} catch ( \Exception $e ) {
			error_log( 'Components REST API unlock_component error: ' . $e->getMessage() );
			return Error_Builder::make( 'unlock_failed' )
				->set_status( 500 )
				->set_message( __( 'Failed to unlock component', 'elementor' ) )
				->build();
		}

		if ( ! $success ) {
			return Error_Builder::make( 'unlock_failed' )
				->set_status( 500 )
				->set_message( __( 'Failed to unlock component', 'elementor' ) )
				->build();
		}
		return Response_Builder::make( [ 'unlocked' => $success ] )->build();
	}

	private function get_lock_status( \WP_REST_Request $request ) {
		$component_id = (int) $request->get_param( 'componentId' );
		try {
			$lock_manager = $this->get_component_lock_manager();
			if ( $lock_manager->is_lock_expired( $component_id ) ) {
				$lock_manager->unlock( $component_id );
			}

			$lock_data = $lock_manager->get_lock_data( $component_id );
			$current_user_id = get_current_user_id();

			// if current  user is the lock user, return true
			if ( $lock_data['locked_by'] && $lock_data['locked_by'] === $current_user_id ) {
				return Response_Builder::make( [
					'is_current_user_allow_to_edit' => true,
					'locked_by' => get_user_by( 'id', $lock_data['locked_by'] )->display_name,
				] )->build();
			}

			// if the user is not the lock user, return false
			if ( $lock_data['locked_by'] && $lock_data['locked_by'] !== $current_user_id ) {
				return Response_Builder::make( [
					'is_current_user_allow_to_edit' => false,
					'locked_by' => get_user_by( 'id', $lock_data['locked_by'] )->display_name,
				] )->build();
			}

			// if the component is not locked, return true
			if ( ! $lock_data['locked_by'] ) {
				return Response_Builder::make( [
					'is_current_user_allow_to_edit' => true,
					'locked_by' => null,
				] )->build();
			}
		} catch ( \Exception $e ) {
			error_log( 'Components REST API get_lock_status error: ' . $e->getMessage() );
			return Error_Builder::make( 'get_lock_status_failed' )
				->set_status( 500 )
				->set_message( __( 'Failed to get lock status', 'elementor' ) )
				->build();
		}
	}

	private function archive_components( \WP_REST_Request $request ) {
		$component_ids = $request->get_param( 'componentIds' );
		$status = $request->get_param( 'status' );

		try {
			$result = $this->get_repository()->archive( $component_ids, $status );
		} catch ( \Exception $e ) {
			error_log( 'Components REST API archive_components error: ' . $e->getMessage() );
			return Error_Builder::make( 'archive_failed' )
				->set_meta( [ 'error' => $e->getMessage() ] )
				->set_status( 500 )
				->set_message( __( 'Failed to archive components', 'elementor' ) )
				->build();
		}
		return Response_Builder::make( $result )->build();
	}

	private function update_components_title( \WP_REST_Request $request ) {
		$failed_ids = [];
		$success_ids = [];
		$components = $request->get_param( 'components' );
		$status = $request->get_param( 'status' );

		foreach ( $components as $component ) {
			$is_success = $this->get_repository()->update_title( $component['componentId'], $component['title'], $status );

			if ( ! $is_success ) {
				$failed_ids[] = $component['componentId'];
				continue;
			}
			$success_ids[] = $component['componentId'];

		}
		return Response_Builder::make( [
			'failedIds' => $failed_ids,
			'successIds' => $success_ids,
		] )->build();
	}

	private function create_validate_components( \WP_REST_Request $request ) {
		$items = Collection::make( $request->get_param( 'items' ) );
		$components = $this->get_repository()->all();

		$result = Save_Components_Validator::make( $components )->validate( $items );

		if ( ! $result['success'] ) {
			return Error_Builder::make( 'components_validation_failed' )
				->set_status( 422 )
				->set_message( 'Validation failed: ' . implode( ', ', $result['messages'] ) )
				->build();
		}

		$circular_result = Circular_Dependency_Validator::make()->validate_new_components( $items );

		if ( ! $circular_result['success'] ) {
			return Error_Builder::make( 'circular_dependency_detected' )
				->set_status( 422 )
				->set_message( __( "Can't add this component - components that contain each other can't be nested.", 'elementor' ) )
				->set_meta( [ 'caused_by' => $circular_result['messages'] ] )
				->build();
		}

		$non_atomic_result = Non_Atomic_Widget_Validator::make()->validate_items( $items );

		if ( ! $non_atomic_result['success'] ) {
			return Error_Builder::make( Non_Atomic_Widget_Validator::ERROR_CODE )
				->set_status( 422 )
				->set_message( __( 'Components require atomic elements only. Remove widgets to create this component.', 'elementor' ) )
				->set_meta( [ 'non_atomic_elements' => $non_atomic_result['non_atomic_elements'] ] )
				->build();
		}

		$validation_errors = $items->map_with_keys( function ( $item ) {
			try {
				if ( isset( $item['settings'] ) ) {
					$this->parse_settings( $item['settings'] );
				}
			} catch ( \Exception $e ) {
				return [ $item['uid'] => $e->getMessage() ];
			}

			return [ $item['uid'] => null ];
		} )
		->filter( fn( $value ) => null !== $value );

		if ( ! $validation_errors->is_empty() ) {
			return Error_Builder::make( 'settings_validation_failed' )
				->set_status( 422 )
				->set_message( 'Settings validation failed: ' . json_encode( $validation_errors->all() ) )
				->build();
		}

		return Response_Builder::make()
			->set_status( 200 )
			->build();
	}

	private function parse_settings( array $settings ): array {
		$result = [];

		if ( empty( $settings ) ) {
			return $result;
		}

		if ( isset( $settings['overridable_props'] ) ) {
			$parser = Component_Overridable_Props_Parser::make();
			$overridable_props_result = $parser->parse( $settings['overridable_props'] );

			if ( ! $overridable_props_result->is_valid() ) {
				throw new \Exception(
					esc_html( 'Validation failed for overridable_props: ' . $overridable_props_result->errors()->to_string() )
				);
			}

			$result['overridable_props'] = $overridable_props_result->unwrap();
		}

		return $result;
	}

	private function route_wrapper( callable $cb ) {
		try {
			$response = $cb();
		} catch ( \Exception $e ) {
			return Error_Builder::make( 'unexpected_error' )
			->set_message( __( 'Something went wrong', 'elementor' ) )
			->build();
		}

		return $response;
	}
}