__( 'Copy section', 'advanced-animation' ), 'copied' => __( 'Component JSON copied to clipboard successfully.', 'advanced-animation' ), 'failed' => __( 'Automatic copying failed. Select the JSON below and copy it manually.', 'advanced-animation' ), ) ); } /** Resolve only existing files contained by the provider's local directory. */ private static function resolve_file( $directory, $relative ) { if ( ! is_string( $directory ) || ! is_string( $relative ) || '' === $relative || preg_match( '#(^[/\\\\]|^[a-z]+:|(^|[/\\\\])\.\.([/\\\\]|$))#i', $relative ) ) { return false; } $root = realpath( $directory ); $file = $root ? realpath( $root . '/' . $relative ) : false; return $file && 0 === strpos( $file, $root . DIRECTORY_SEPARATOR ) && is_file( $file ) && is_readable( $file ) ? $file : false; } private static function read_json( $file ) { if ( ! $file || filesize( $file ) > 2 * MB_IN_BYTES ) { throw new RuntimeException( 'Invalid component file.' ); } $data = json_decode( file_get_contents( $file ), true ); if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $data ) ) { throw new RuntimeException( 'Invalid component JSON.' ); } return $data; } /** * Providers register { directory, url, tier } under a unique library key. * Each directory contains catalog.json; files and previews are relative paths. * Pro records replace locked Pro cards with the same ID and inherit their * preview image URL when the Pro record omits its preview path. */ public static function get_components() { $libraries = apply_filters( 'wpaddons_component_libraries', array( 'free' => array( 'directory' => dirname( __DIR__ ) . '/assets/components', 'url' => plugins_url( 'assets/components/', dirname( __DIR__ ) . '/advanced-animation.php' ), 'tier' => 'free', ), ) ); $components = array(); foreach ( is_array( $libraries ) ? $libraries : array() as $library ) { if ( ! is_array( $library ) || ! isset( $library['directory'], $library['url'], $library['tier'] ) || ! in_array( $library['tier'], array( 'free', 'pro' ), true ) || ! is_string( $library['url'] ) ) { continue; } try { $catalog = self::read_json( self::resolve_file( $library['directory'], 'catalog.json' ) ); } catch ( Throwable $error ) { continue; } if ( array_values( $catalog ) !== $catalog ) { continue; } foreach ( $catalog as $entry ) { if ( ! is_array( $entry ) || ! isset( $entry['id'], $entry['title'], $entry['description'], $entry['editor'], $entry['tier'] ) ) { continue; } foreach ( array( 'id', 'title', 'description', 'editor', 'tier' ) as $key ) { if ( ! is_string( $entry[ $key ] ) ) { continue 2; } } if ( ! preg_match( '/^[a-z0-9-]+$/', $entry['id'] ) || '' === trim( $entry['title'] ) || ! in_array( $entry['editor'], array( 'classic', 'atomic' ), true ) || ! in_array( $entry['tier'], array( 'free', 'pro' ), true ) ) { continue; } $id = $entry['id']; if ( isset( $components[ $id ] ) && ( 'pro' !== $library['tier'] || 'pro' !== $entry['tier'] || 'pro' !== $components[ $id ]['tier'] || $entry['editor'] !== $components[ $id ]['editor'] ) ) { continue; } $card = array_intersect_key( $entry, array_flip( array( 'id', 'title', 'description', 'editor', 'tier' ) ) ); if ( array_key_exists( 'preview', $entry ) ) { if ( ! is_string( $entry['preview'] ) ) { continue; } $preview = self::resolve_file( $library['directory'], $entry['preview'] ); $card['preview'] = $preview ? esc_url_raw( trailingslashit( $library['url'] ) . str_replace( '\\', '/', $entry['preview'] ) ) : ''; } elseif ( isset( $components[ $id ] ) && 'pro' === $library['tier'] && 'pro' === $entry['tier'] ) { // Keep the resolved URL rooted in the original (Free) library. $card['preview'] = $components[ $id ]['preview']; } else { continue; } $preview_url = $components[ $id ]['preview_url'] ?? '#'; $card['preview_url'] = isset( $entry['preview_url'] ) && is_string( $entry['preview_url'] ) ? esc_url_raw( $entry['preview_url'] ) : $preview_url; if ( '' === $card['preview_url'] ) { $card['preview_url'] = $preview_url; } $card['locked'] = 'pro' === $entry['tier'] && 'pro' !== $library['tier']; $card['payload'] = null; $card['error'] = ''; if ( ! $card['locked'] ) { try { if ( $entry['tier'] !== $library['tier'] ) { throw new RuntimeException( 'Invalid component tier.' ); } $payload = self::read_json( self::resolve_file( $library['directory'], $entry['file'] ?? null ) ); if ( 'elementor' !== ( $payload['type'] ?? '' ) || empty( $payload['elements'] ) || ! is_array( $payload['elements'] ) || array_values( $payload['elements'] ) !== $payload['elements'] ) { throw new RuntimeException( 'Invalid Elementor payload.' ); } self::validate_elements( $payload['elements'], $entry['editor'] ); if ( ! class_exists( '\\Elementor\\Plugin' ) ) { $card['error'] = __( 'Activate Elementor to use this section.', 'advanced-animation' ); } else { if ( 'atomic' === $entry['editor'] ) { self::normalize_atomic_elements( $payload['elements'] ); } $payload['siteurl'] = rest_url(); $card['payload'] = wp_json_encode( $payload ); } } catch ( Throwable $error ) { $card['error'] = __( 'This section is unavailable. Check its JSON file and Elementor compatibility.', 'advanced-animation' ); } } $components[ $id ] = $card; } } return array_values( $components ); } private static function validate_elements( $elements, $editor, $depth = 0 ) { if ( $depth > 30 || array_values( $elements ) !== $elements ) { throw new RuntimeException( 'Invalid element tree.' ); } foreach ( $elements as $element ) { if ( ! is_array( $element ) || empty( $element['id'] ) || ! is_string( $element['id'] ) || ! isset( $element['elType'], $element['settings'], $element['elements'] ) || ! is_string( $element['elType'] ) || ! is_array( $element['settings'] ) || ! is_array( $element['elements'] ) ) { throw new RuntimeException( 'Invalid element.' ); } $type = 'widget' === $element['elType'] ? ( $element['widgetType'] ?? '' ) : $element['elType']; if ( ! is_string( $type ) || '' === $type || ( 'classic' === $editor && 0 === strpos( $type, 'e-' ) ) ) { throw new RuntimeException( 'Wrong editor format.' ); } self::validate_elements( $element['elements'], $editor, $depth + 1 ); } } /** Match the static-text choice made by src/elementor-sidebar/atomic-text-props.js. */ private static function text_type( $schema ) { if ( ! is_array( $schema ) ) { throw new RuntimeException( 'Missing text schema.' ); } if ( 'overridable' === ( $schema['key'] ?? '' ) ) { return self::text_type( $schema['settings']['origin_prop_type'] ?? null ); } if ( 'union' === ( $schema['kind'] ?? '' ) ) { $types = array_diff_key( $schema['prop_types'] ?? array(), array_flip( array( 'dynamic', 'overridable' ) ) ); $default = $schema['default']['$$type'] ?? ''; if ( isset( $types[ $default ] ) ) { return self::text_type( $types[ $default ] ); } if ( 1 === count( $types ) ) { return self::text_type( reset( $types ) ); } throw new RuntimeException( 'Unsupported text union.' ); } return $schema['key'] ?? ''; } private static function normalize_atomic_elements( &$elements ) { $plugin = \Elementor\Plugin::instance(); $keys = array( 'e-heading' => 'title', 'e-paragraph' => 'paragraph', 'e-button' => 'text' ); foreach ( $elements as &$element ) { $type = 'widget' === $element['elType'] ? $element['widgetType'] : $element['elType']; // Atomic sections may contain Classic widgets or containers. Preserve their // settings, but still normalize any Atomic descendants. if ( 0 !== strpos( $type, 'e-' ) ) { self::normalize_atomic_elements( $element['elements'] ); continue; } $widget = 'widget' === $element['elType'] ? $plugin->widgets_manager->get_widget_types( $type ) : $plugin->elements_manager->get_element_types( $type ); if ( ! $widget || ! is_callable( array( $widget, 'get_props_schema' ) ) ) { throw new RuntimeException( 'Atomic element is not supported.' ); } if ( isset( $keys[ $type ], $element['settings'][ $keys[ $type ] ] ) ) { $key = $keys[ $type ]; $schema = json_decode( wp_json_encode( $widget::get_props_schema() ), true ); $text_type = self::text_type( $schema[ $key ] ?? null ); $prop = $element['settings'][ $key ]; $value = $prop['value'] ?? null; $source_type = $prop['$$type'] ?? ''; if ( in_array( $source_type, array( 'html-v2', 'html-v3' ), true ) && is_array( $value ) ) { $value = $value['content'] ?? null; $value = is_array( $value ) ? ( $value['value'] ?? null ) : $value; } elseif ( ! in_array( $source_type, array( 'escaped-html', 'html', 'string' ), true ) ) { throw new RuntimeException( 'Unsupported source text.' ); } if ( ! is_string( $value ) || ! in_array( $text_type, array( 'escaped-html', 'html', 'string', 'html-v2', 'html-v3' ), true ) ) { throw new RuntimeException( 'Unsupported text schema.' ); } if ( 'string' === $text_type ) { $value = html_entity_decode( wp_strip_all_tags( $value ), ENT_QUOTES, 'UTF-8' ); } elseif ( in_array( $text_type, array( 'html-v2', 'html-v3' ), true ) ) { $value = array( 'content' => 'html-v3' === $text_type ? array( '$$type' => 'string', 'value' => $value ) : $value, 'children' => $prop['value']['children'] ?? array() ); } $element['settings'][ $key ] = array( '$$type' => $text_type, 'value' => $value ); } self::normalize_atomic_elements( $element['elements'] ); } } public static function render_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $components = self::get_components(); $pro_url = apply_filters( 'wpaddons_pro_url', WPAddons_Admin_Promo::PRO_URL ); ?>