first commit
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Components;
|
||||
|
||||
use Elementor\Core\Utils\Api\Parse_Result;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class Components_Parser {
|
||||
const MIN_NAME_LENGTH = 2;
|
||||
const MAX_NAME_LENGTH = 50;
|
||||
|
||||
public static function make() {
|
||||
return new static();
|
||||
}
|
||||
|
||||
public function parse_name( $name, $existing_components_names ): Parse_Result {
|
||||
$result = Parse_Result::make();
|
||||
|
||||
$sanitized = trim( sanitize_text_field( $name ) );
|
||||
|
||||
if ( strlen( $sanitized ) < self::MIN_NAME_LENGTH ) {
|
||||
$result->errors()->add( 'name', 'component_name_too_short_min_' . self::MIN_NAME_LENGTH );
|
||||
}
|
||||
|
||||
if ( strlen( $sanitized ) > self::MAX_NAME_LENGTH ) {
|
||||
$result->errors()->add( 'name', 'component_name_too_long_max_' . self::MAX_NAME_LENGTH );
|
||||
}
|
||||
|
||||
if ( in_array( $sanitized, $existing_components_names, true ) ) {
|
||||
$result->errors()->add( 'name', 'duplicated_component_name' );
|
||||
}
|
||||
|
||||
return $result->wrap( $sanitized );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Components;
|
||||
|
||||
use Elementor\Modules\Components\Documents\Component as Component_Document;
|
||||
use Elementor\Plugin;
|
||||
use Elementor\Modules\Components\Components_REST_API;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class Components_Repository {
|
||||
|
||||
public static function make(): Components_Repository {
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function all() {
|
||||
// Components count is limited to 50, if we increase this number, we need to iterate the posts in batches.
|
||||
$posts = get_posts( [
|
||||
'post_type' => Component_Document::TYPE,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => Components_REST_API::MAX_COMPONENTS,
|
||||
] );
|
||||
|
||||
$components = [];
|
||||
|
||||
foreach ( $posts as $post ) {
|
||||
$doc = Plugin::$instance->documents->get( $post->ID );
|
||||
|
||||
if ( ! $doc ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$components[] = [
|
||||
'id' => $doc->get_main_id(),
|
||||
'name' => $doc->get_post()->post_title,
|
||||
'styles' => $this->extract_styles( $doc->get_elements_data() ),
|
||||
];
|
||||
}
|
||||
|
||||
return Components::make( $components );
|
||||
}
|
||||
|
||||
public function create( string $name, array $content ) {
|
||||
$document = Plugin::$instance->documents->create(
|
||||
Component_Document::get_type(),
|
||||
[
|
||||
'post_title' => $name,
|
||||
'post_status' => 'publish',
|
||||
]
|
||||
);
|
||||
|
||||
$saved = $document->save( [
|
||||
'elements' => $content,
|
||||
] );
|
||||
|
||||
if ( ! $saved ) {
|
||||
throw new \Exception( 'Failed to create component' );
|
||||
}
|
||||
|
||||
return $document->get_main_id();
|
||||
}
|
||||
private function extract_styles( array $elements, array $styles = [] ) {
|
||||
foreach ( $elements as $element ) {
|
||||
if ( isset( $element['styles'] ) ) {
|
||||
$styles = array_merge( $styles, $element['styles'] );
|
||||
}
|
||||
|
||||
if ( isset( $element['elements'] ) ) {
|
||||
$styles = $this->extract_styles( $element['elements'], $styles );
|
||||
}
|
||||
}
|
||||
|
||||
return $styles;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Components;
|
||||
|
||||
use Elementor\Core\Utils\Api\Error_Builder;
|
||||
use Elementor\Core\Utils\Api\Response_Builder;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class Components_REST_API {
|
||||
const API_NAMESPACE = 'elementor/v1';
|
||||
const API_BASE = 'components';
|
||||
const STYLES_ROUTE = 'styles';
|
||||
const MAX_COMPONENTS = 50;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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() => is_user_logged_in(),
|
||||
],
|
||||
] );
|
||||
|
||||
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() => is_user_logged_in(),
|
||||
],
|
||||
] );
|
||||
|
||||
register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE, [
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->create_component( $request ) ),
|
||||
'permission_callback' => fn() => current_user_can( 'manage_options' ),
|
||||
'args' => [
|
||||
'name' => [
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
],
|
||||
'content' => [
|
||||
'type' => 'array',
|
||||
'required' => true,
|
||||
'items' => [
|
||||
'type' => 'object',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
] );
|
||||
}
|
||||
|
||||
private function get_components() {
|
||||
$components = $this->get_repository()->all();
|
||||
|
||||
$components_list = $components->get_components()->map( fn( $component ) => [
|
||||
'id' => $component['id'],
|
||||
'name' => $component['name'],
|
||||
])->all();
|
||||
|
||||
return Response_Builder::make( $components_list )->build();
|
||||
}
|
||||
|
||||
private function get_styles() {
|
||||
$components = $this->get_repository()->all();
|
||||
|
||||
$styles = [];
|
||||
$components->get_components()->each( function( $component ) use ( &$styles ) {
|
||||
$styles[ $component['id'] ] = $component['styles'];
|
||||
} );
|
||||
|
||||
return Response_Builder::make( $styles )->build();
|
||||
}
|
||||
private function create_component( \WP_REST_Request $request ) {
|
||||
$components = $this->get_repository()->all();
|
||||
$components_count = $components->get_components()->count();
|
||||
|
||||
if ( $components_count >= static::MAX_COMPONENTS ) {
|
||||
return Error_Builder::make( 'components_limit_exceeded' )
|
||||
->set_status( 400 )
|
||||
->set_message( sprintf(
|
||||
/* translators: %d: maximum components limit. */
|
||||
__( 'Components limit exceeded. Maximum allowed: %d', 'elementor' ),
|
||||
static::MAX_COMPONENTS
|
||||
) )
|
||||
->build();
|
||||
}
|
||||
|
||||
$parser = Components_Parser::make();
|
||||
|
||||
$name_result = $parser->parse_name( $request->get_param( 'name' ), $components->get_components()->map( fn( $component ) => $component['name'] )->all() );
|
||||
|
||||
if ( ! $name_result->is_valid() ) {
|
||||
return Error_Builder::make( 'invalid_name' )
|
||||
->set_status( 400 )
|
||||
->set_message( 'Invalid component name: ' . $name_result->errors()->to_string() )
|
||||
->build();
|
||||
}
|
||||
|
||||
$name = $name_result->unwrap();
|
||||
// The content is validated & sanitized in the document save process.
|
||||
$content = $request->get_param( 'content' );
|
||||
|
||||
try {
|
||||
$component_id = $this->get_repository()->create( $name, $content );
|
||||
|
||||
return Response_Builder::make( [ 'component_id' => $component_id ] )->set_status( 201 )->build();
|
||||
} catch ( \Exception $e ) {
|
||||
$error_message = $e->getMessage();
|
||||
|
||||
$invalid_elements_structure_error = str_contains( $error_message, 'Invalid data' );
|
||||
$atomic_styles_validation_error = str_contains( $error_message, 'Styles validation failed' );
|
||||
$atomic_settings_validation_error = str_contains( $error_message, 'Settings validation failed' );
|
||||
|
||||
if ( $invalid_elements_structure_error || $atomic_styles_validation_error || $atomic_settings_validation_error ) {
|
||||
return Error_Builder::make( 'content_validation_failed' )
|
||||
->set_status( 400 )
|
||||
->set_message( $error_message )
|
||||
->build();
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Components;
|
||||
|
||||
use Elementor\Core\Utils\Collection;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class Components {
|
||||
private Collection $components;
|
||||
|
||||
public static function make( array $components = [] ) {
|
||||
return new static( $components );
|
||||
}
|
||||
|
||||
private function __construct( array $components = [] ) {
|
||||
$this->components = Collection::make( $components );
|
||||
}
|
||||
|
||||
public function get_components() {
|
||||
return $this->components;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
namespace Elementor\Modules\Components\Documents;
|
||||
|
||||
use Elementor\Core\Base\Document;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
class Component extends Document {
|
||||
const TYPE = 'elementor_component';
|
||||
|
||||
public static function get_properties() {
|
||||
$properties = parent::get_properties();
|
||||
|
||||
$properties['cpt'] = [ self::TYPE ];
|
||||
|
||||
return $properties;
|
||||
}
|
||||
|
||||
|
||||
public static function get_type() {
|
||||
return self::TYPE;
|
||||
}
|
||||
|
||||
public static function get_title() {
|
||||
return esc_html__( 'Component', 'elementor' );
|
||||
}
|
||||
|
||||
public static function get_plural_title() {
|
||||
return esc_html__( 'Components', 'elementor' );
|
||||
}
|
||||
|
||||
public static function get_labels(): array {
|
||||
$plural_label = static::get_plural_title();
|
||||
$singular_label = static::get_title();
|
||||
|
||||
$labels = [
|
||||
'name' => $plural_label,
|
||||
'singular_name' => $singular_label,
|
||||
];
|
||||
|
||||
return $labels;
|
||||
}
|
||||
|
||||
public static function get_supported_features(): array {
|
||||
return [
|
||||
'title',
|
||||
'author',
|
||||
'thumbnail',
|
||||
'custom-fields',
|
||||
'revisions',
|
||||
'elementor',
|
||||
];
|
||||
}
|
||||
}
|
||||
65
wp-content/plugins/elementor/modules/components/module.php
Normal file
65
wp-content/plugins/elementor/modules/components/module.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
namespace Elementor\Modules\Components;
|
||||
|
||||
use Elementor\Core\Base\Module as BaseModule;
|
||||
use Elementor\Core\Experiments\Manager as Experiments_Manager;
|
||||
use Elementor\Modules\Components\Styles\Component_Styles;
|
||||
use Elementor\Modules\Components\Documents\Component as Component_Document;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class Module extends BaseModule {
|
||||
const EXPERIMENT_NAME = 'e_components';
|
||||
const PACKAGES = [ 'editor-components' ];
|
||||
|
||||
public function get_name() {
|
||||
return 'components';
|
||||
}
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
|
||||
add_filter( 'elementor/editor/v2/packages', fn ( $packages ) => $this->add_packages( $packages ) );
|
||||
add_action( 'elementor/documents/register', fn ( $documents_manager ) => $this->register_document_type( $documents_manager ) );
|
||||
|
||||
( new Component_Styles() )->register_hooks();
|
||||
( new Components_REST_API() )->register_hooks();
|
||||
}
|
||||
|
||||
public static function get_experimental_data() {
|
||||
return [
|
||||
'name' => self::EXPERIMENT_NAME,
|
||||
'title' => esc_html__( 'Components', 'elementor' ),
|
||||
'description' => esc_html__( 'Enable components.', 'elementor' ),
|
||||
'hidden' => true,
|
||||
'default' => Experiments_Manager::STATE_INACTIVE,
|
||||
'release_status' => Experiments_Manager::RELEASE_STATUS_DEV,
|
||||
];
|
||||
}
|
||||
|
||||
public function get_widgets() {
|
||||
return [
|
||||
'Component',
|
||||
];
|
||||
}
|
||||
|
||||
private function add_packages( $packages ) {
|
||||
return array_merge( $packages, self::PACKAGES );
|
||||
}
|
||||
|
||||
private function register_document_type( $documents_manager ) {
|
||||
$documents_manager->register_document_type(
|
||||
Component_Document::TYPE,
|
||||
Component_Document::get_class_full_name()
|
||||
);
|
||||
|
||||
register_post_type( Component_Document::TYPE, [
|
||||
'label' => Component_Document::get_title(),
|
||||
'labels' => Component_Document::get_labels(),
|
||||
'public' => false,
|
||||
'supports' => Component_Document::get_supported_features(),
|
||||
] );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Components\Styles;
|
||||
|
||||
use Elementor\Core\Base\Document;
|
||||
use Elementor\Core\Utils\Collection;
|
||||
use Elementor\Modules\AtomicWidgets\Cache_Validity;
|
||||
use Elementor\Modules\AtomicWidgets\Utils;
|
||||
|
||||
/**
|
||||
* Component styles fetching for render
|
||||
*/
|
||||
class Component_Styles {
|
||||
const CACHE_ROOT_KEY = 'component-styles-related-posts';
|
||||
|
||||
public function register_hooks() {
|
||||
add_action( 'elementor/post/render', fn( $post_id ) => $this->render_post( $post_id ) );
|
||||
|
||||
add_action( 'elementor/document/after_save', fn( Document $document ) => $this->invalidate_cache(
|
||||
[ $document->get_main_post()->ID ]
|
||||
), 20, 2 );
|
||||
|
||||
add_action(
|
||||
'elementor/core/files/clear_cache',
|
||||
fn() => $this->invalidate_cache(),
|
||||
);
|
||||
}
|
||||
|
||||
private function render_post( string $post_id ) {
|
||||
$cache_validity = new Cache_Validity();
|
||||
|
||||
if ( $cache_validity->is_valid( [ self::CACHE_ROOT_KEY, $post_id ] ) ) {
|
||||
$component_ids = $cache_validity->get_meta( [ self::CACHE_ROOT_KEY, $post_id ] );
|
||||
|
||||
$this->declare_components_rendered( $component_ids );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$components = $this->get_components_from_post( $post_id );
|
||||
$component_ids = Collection::make( $components )
|
||||
->filter( fn( $component ) => isset( $component['settings']['component_id']['value'] ) )
|
||||
->map( fn( $component ) => $component['settings']['component_id']['value'] )
|
||||
->unique()
|
||||
->all();
|
||||
|
||||
$cache_validity->validate( [ self::CACHE_ROOT_KEY, $post_id ], $component_ids );
|
||||
|
||||
$this->declare_components_rendered( $component_ids );
|
||||
}
|
||||
|
||||
private function declare_components_rendered( array $post_ids ) {
|
||||
foreach ( $post_ids as $post_id ) {
|
||||
do_action( 'elementor/post/render', $post_id );
|
||||
}
|
||||
}
|
||||
|
||||
private function get_components_from_post( string $post_id ): array {
|
||||
$components = [];
|
||||
|
||||
Utils::traverse_post_elements( $post_id, function( $element_data ) use ( &$components ) {
|
||||
if ( isset( $element_data['widgetType'] ) && 'e-component' === $element_data['widgetType'] ) {
|
||||
$components[] = $element_data;
|
||||
}
|
||||
} );
|
||||
|
||||
return $components;
|
||||
}
|
||||
|
||||
private function invalidate_cache( ?array $post_ids = null ) {
|
||||
$cache_validity = new Cache_Validity();
|
||||
|
||||
if ( empty( $post_ids ) ) {
|
||||
$cache_validity->invalidate( [ self::CACHE_ROOT_KEY ] );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $post_ids as $post_id ) {
|
||||
$cache_validity->invalidate( [ self::CACHE_ROOT_KEY, $post_id ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
namespace Elementor\Modules\Components\Widgets;
|
||||
|
||||
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Widget_Base;
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\Number_Prop_Type;
|
||||
use Elementor\Plugin;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class Component extends Atomic_Widget_Base {
|
||||
|
||||
public static function get_element_type(): string {
|
||||
return 'e-component';
|
||||
}
|
||||
|
||||
public function show_in_panel() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function get_title() {
|
||||
$title = esc_html__( 'Component', 'elementor' );
|
||||
|
||||
if ( isset( $this->get_settings ) && null !== $this->get_settings( 'component_id' ) ) {
|
||||
$post_id = $this->get_settings( 'component_id' )['value'];
|
||||
$title = Plugin::$instance->documents->get( $post_id )->get_title();
|
||||
}
|
||||
|
||||
return $title;
|
||||
}
|
||||
|
||||
public function get_keywords() {
|
||||
return [ 'component' ];
|
||||
}
|
||||
|
||||
public function get_icon() {
|
||||
return 'eicon-star';
|
||||
}
|
||||
|
||||
protected static function define_props_schema(): array {
|
||||
return [
|
||||
'component_id' => Number_Prop_Type::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function define_atomic_controls(): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function get_settings_controls(): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function render(): void {
|
||||
if ( null === $this->get_settings( 'component_id' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$post_id = $this->get_settings( 'component_id' )['value'];
|
||||
$content = Plugin::$instance->frontend->get_builder_content( $post_id );
|
||||
$html = sprintf( '<div class="e-component">%s</div>', $content );
|
||||
|
||||
// PHPCS - should not be escaped.
|
||||
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user