*/ private const POWER_ID_FIELDS = array( 'triggerPowerId', 'triggerTargetPowerId', 'contentToggleTargetA', 'contentToggleTargetB', 'contentGroupTargetPowerId', 'scrollNavTargetPowerId', 'autoTriggerWatchPowerId', ); /** * Plugin instance. * * @var Divi_Powers_Plugin */ private $plugin; /** * Constructor. * * @param Divi_Powers_Plugin $plugin Plugin instance. */ public function __construct( Divi_Powers_Plugin $plugin ) { $this->plugin = $plugin; } /** * Register hooks. * * @return void */ public function register() { add_action( 'wp_ajax_divi_powers_power_map_scan', array( $this, 'ajax_scan' ) ); add_action( 'wp_ajax_divi_powers_power_map_rename', array( $this, 'ajax_rename' ) ); add_action( 'wp_enqueue_scripts', array( $this, 'print_builder_config' ), 5 ); add_action( 'admin_enqueue_scripts', array( $this, 'print_builder_config' ), 5 ); } /** * Print the small configuration object used by the Visual Builder panel. * * The Divi application can run in a same-origin iframe. The JavaScript checks * its current window, parent, and top window for this object. * * @return void */ public function print_builder_config() { if ( ! $this->plugin->is_power_enabled( 'builder_bridge' ) || ! current_user_can( 'edit_posts' ) ) { return; } $post_id = get_queried_object_id(); if ( ! $post_id && isset( $_GET['post'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $post_id = absint( $_GET['post'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } if ( ! $post_id && isset( $_GET['post_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $post_id = absint( $_GET['post_id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } if ( $post_id && ! current_user_can( 'edit_post', $post_id ) ) { return; } $ai_status = class_exists( 'Divi_Powers_OpenAI' ) ? Divi_Powers_OpenAI::connection_status() : array(); $config = array( 'enabled' => true, 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( self::NONCE_ACTION ), 'postId' => $post_id, 'pageTitle' => $post_id ? get_the_title( $post_id ) : '', 'pageUrl' => $post_id ? get_permalink( $post_id ) : '', 'siteUrl' => home_url( '/' ), 'version' => DIVI_POWERS_VERSION, 'aiConfigured' => ! empty( $ai_status['configured'] ), 'aiStatus' => isset( $ai_status['status'] ) ? sanitize_key( $ai_status['status'] ) : 'not_configured', 'aiModel' => isset( $ai_status['model'] ) ? sanitize_text_field( $ai_status['model'] ) : 'gpt-5.6-sol', 'aiModelLabel' => class_exists( 'Divi_Powers_OpenAI' ) ? Divi_Powers_OpenAI::get_builder_model_label() : 'GPT-5.6 Sol', 'aiSettingsUrl' => admin_url( 'admin.php?page=divi-powers-connection' ), 'aiConnectionUrl' => admin_url( 'admin.php?page=divi-powers-ai-connector' ), 'aiEnabled' => $this->plugin->is_power_enabled( 'ai_connection' ), 'chatgptUrl' => (string) apply_filters( 'divi_powers_chatgpt_plugin_url', 'https://chatgpt.com/' ), 'drawerFallbackEnabled' => $this->plugin->is_power_enabled( 'builder_drawer_fallback' ), 'previewCaptureScriptUrl' => DIVI_POWERS_URL . 'assets/vendor/html2canvas/html2canvas.min.js', ); wp_register_script( 'fivedp-map-config', false, array(), DIVI_POWERS_VERSION, false ); wp_enqueue_script( 'fivedp-map-config' ); wp_add_inline_script( 'fivedp-map-config', 'window.DiviPowersPowerMapConfig=' . wp_json_encode( $config, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ) . ';' ); } /** * Return a relationship scan for the requested post. * * @return void */ public function ajax_scan() { $this->verify_ajax_request(); $post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verify_ajax_request() validates capability and nonce immediately above. $post = $this->get_editable_post( $post_id ); wp_send_json_success( $this->scan_post( $post ) ); } /** * Safely rename one Power ID and every known reference in the saved page. * * @return void */ public function ajax_rename() { $this->verify_ajax_request(); $post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verify_ajax_request() validates capability and nonce immediately above. $old_id = isset( $_POST['old_id'] ) ? sanitize_text_field( wp_unslash( $_POST['old_id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verify_ajax_request() validates capability and nonce immediately above. $new_id = isset( $_POST['new_id'] ) ? sanitize_text_field( wp_unslash( $_POST['new_id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verify_ajax_request() validates capability and nonce immediately above. $post = $this->get_editable_post( $post_id ); if ( '' === $old_id || '' === $new_id ) { wp_send_json_error( array( 'message' => __( 'Enter both the current and new Power ID.', '5d-powers-for-divi' ) ), 400 ); } if ( $old_id === $new_id ) { wp_send_json_error( array( 'message' => __( 'The new Power ID is unchanged.', '5d-powers-for-divi' ) ), 400 ); } if ( ! preg_match( '/^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/', $new_id ) ) { wp_send_json_error( array( 'message' => __( 'Power IDs may contain letters, numbers, hyphens, and underscores, and must begin with a letter or number.', '5d-powers-for-divi' ), ), 400 ); } $before = $this->scan_post( $post ); $ids = isset( $before['power_ids'] ) && is_array( $before['power_ids'] ) ? $before['power_ids'] : array(); if ( ! isset( $ids[ $old_id ] ) ) { wp_send_json_error( array( 'message' => __( 'The current Power ID was not found in the saved page.', '5d-powers-for-divi' ) ), 404 ); } if ( isset( $ids[ $new_id ] ) ) { wp_send_json_error( array( 'message' => __( 'The new Power ID is already used on this page.', '5d-powers-for-divi' ) ), 409 ); } $blocks = parse_blocks( (string) $post->post_content ); $changed = 0; $this->rename_in_blocks( $blocks, $old_id, $new_id, $changed ); if ( 0 === $changed ) { wp_send_json_error( array( 'message' => __( 'No saved Power ID values were changed.', '5d-powers-for-divi' ) ), 400 ); } $result = wp_update_post( array( 'ID' => $post_id, 'post_content' => serialize_blocks( $blocks ), ), true ); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ), 500 ); } $updated_post = get_post( $post_id ); wp_send_json_success( array( 'message' => sprintf( /* translators: 1: old Power ID, 2: new Power ID, 3: number of updated values. */ _n( 'Renamed %1$s to %2$s and updated %3$d saved value. Reload the Visual Builder before making more changes.', 'Renamed %1$s to %2$s and updated %3$d saved values. Reload the Visual Builder before making more changes.', $changed, '5d-powers-for-divi' ), $old_id, $new_id, $changed ), 'changed' => $changed, 'scan' => $this->scan_post( $updated_post ), ) ); } /** * Verify nonce and general request permission. * * @return void */ private function verify_ajax_request() { check_ajax_referer( self::NONCE_ACTION, 'nonce' ); if ( ! $this->plugin->is_power_enabled( 'builder_bridge' ) || ! current_user_can( 'edit_posts' ) ) { wp_send_json_error( array( 'message' => __( 'You are not allowed to use Power Map.', '5d-powers-for-divi' ) ), 403 ); } } /** * Return an editable post or terminate the request. * * @param int $post_id Post ID. * @return WP_Post */ private function get_editable_post( $post_id ) { $post = $post_id ? get_post( $post_id ) : null; if ( ! $post ) { wp_send_json_error( array( 'message' => __( 'Power Map could not determine the current page.', '5d-powers-for-divi' ) ), 404 ); } if ( ! current_user_can( 'edit_post', $post_id ) ) { wp_send_json_error( array( 'message' => __( 'You are not allowed to inspect this page.', '5d-powers-for-divi' ) ), 403 ); } return $post; } /** * Scan one saved Divi page. * * @param WP_Post $post Post object. * @return array */ public function scan_post( $post ) { $state = array( 'nodes' => array(), 'definitions' => array(), 'references' => array(), 'entry_points' => array(), 'groups' => array(), 'issues' => array(), 'divi_blocks' => 0, ); $blocks = parse_blocks( (string) $post->post_content ); $this->scan_blocks( $blocks, array(), $state ); $this->finalize_scan( $state ); $power_ids = array(); foreach ( $state['definitions'] as $power_id => $definitions ) { $inbound = array_values( array_filter( $state['references'], static function ( $reference ) use ( $power_id ) { return isset( $reference['target_id'] ) && $power_id === $reference['target_id']; } ) ); $entry_points = array_values( array_filter( $state['entry_points'], static function ( $entry_point ) use ( $power_id ) { return isset( $entry_point['target_id'] ) && $power_id === $entry_point['target_id']; } ) ); $power_ids[ $power_id ] = array( 'id' => $power_id, 'definitions' => array_values( $definitions ), 'references' => $inbound, 'entry_points' => $entry_points, 'duplicate' => count( $definitions ) > 1, 'unused' => empty( $inbound ) && empty( $entry_points ), ); } $trigger_nodes = array_filter( $state['nodes'], static function ( $node ) { $roles = isset( $node['roles'] ) && is_array( $node['roles'] ) ? $node['roles'] : array(); return (bool) array_intersect( array( 'Click Trigger', 'Automatic Trigger', 'A/B Toggle', 'Content Group Control', 'Scroll Navigation Control' ), $roles ); } ); return array( 'post' => array( 'id' => (int) $post->ID, 'title' => get_the_title( $post ), 'modified' => get_post_modified_time( 'c', true, $post ), ), 'summary' => array( 'power_ids' => count( $power_ids ), 'targets' => array_sum( array_map( 'count', $state['definitions'] ) ), 'triggers' => count( $trigger_nodes ), 'groups' => count( $state['groups'] ), 'issues' => count( $state['issues'] ), 'divi_modules' => $state['divi_blocks'], ), 'power_ids' => $power_ids, 'groups' => array_values( $state['groups'] ), 'issues' => array_values( $state['issues'] ), 'nodes' => array_values( $state['nodes'] ), 'notice' => 0 === $state['divi_blocks'] ? __( 'No saved Divi 5 blocks were found. The page may use legacy modules, a Theme Builder template, or unsaved Visual Builder changes.', '5d-powers-for-divi' ) : __( 'Power Map reads the saved page snapshot. Save the Visual Builder, then refresh the map to include recent changes.', '5d-powers-for-divi' ), ); } /** * Recursively scan blocks. * * @param array> $blocks Blocks. * @param array $parent_path Parent path. * @param array $state Scan state. * @return void */ private function scan_blocks( $blocks, $parent_path, &$state ) { foreach ( $blocks as $index => $block ) { if ( ! is_array( $block ) ) { continue; } $path = array_merge( $parent_path, array( $index ) ); $block_name = isset( $block['blockName'] ) ? (string) $block['blockName'] : ''; if ( 0 === strpos( $block_name, 'divi/' ) ) { $state['divi_blocks']++; $this->scan_divi_block( $block, $path, $state ); } if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) { $this->scan_blocks( $block['innerBlocks'], $path, $state ); } } } /** * Scan one Divi block. * * @param array $block Block. * @param array $path Block path. * @param array $state Scan state. * @return void */ private function scan_divi_block( $block, $path, &$state ) { $attrs = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : array(); $config = $this->get_power_config( $attrs ); if ( empty( $config ) ) { return; } $block_name = isset( $block['blockName'] ) ? (string) $block['blockName'] : ''; $node_id = 'node-' . implode( '-', $path ); $label = $this->get_admin_label( $attrs, $block_name ); $power_id = $this->string_value( $config, 'triggerPowerId' ); $roles = array(); $role_labels = array(); $references = array(); $entry_points = array(); $group_ids = array(); if ( '' !== $power_id ) { $roles[] = 'Target'; $role_labels[] = __( 'Target', '5d-powers-for-divi' ); } if ( 'on' === $this->string_value( $config, 'enabled', 'off' ) ) { $roles[] = 'Dismissible'; $role_labels[] = __( 'Dismissible', '5d-powers-for-divi' ); } if ( 'on' === $this->string_value( $config, 'dismissResetEnabled', 'off' ) ) { $roles[] = 'Dismissal Reset'; $role_labels[] = __( 'Dismissal Reset', '5d-powers-for-divi' ); } if ( 'on' === $this->string_value( $config, 'mediaScrollerEnabled', 'off' ) ) { $roles[] = 'Logo & Image Scroller'; $role_labels[] = __( 'Logo & Image Scroller', '5d-powers-for-divi' ); } $presentation = $this->string_value( $config, 'modalPresentation', 'none' ); if ( 'none' !== $presentation ) { $roles[] = 'Presentation: ' . $this->humanize_slug( $presentation ); $role_labels[] = sprintf( /* translators: %s: presentation mode, such as Modal or Left Drawer. */ __( 'Presentation: %s', '5d-powers-for-divi' ), $this->presentation_label( $presentation ) ); if ( '' === $power_id ) { $this->add_issue( $state, 'error', __( 'Presentation target has no Power ID', '5d-powers-for-divi' ), $node_id, $label, '' ); } } $click_action = $this->string_value( $config, 'triggerClickAction', 'none' ); if ( in_array( $click_action, array( 'toggle', 'show', 'hide' ), true ) ) { $roles[] = 'Click Trigger'; $role_labels[] = __( 'Click Trigger', '5d-powers-for-divi' ); $this->add_reference( $references, $state, $node_id, $label, 'Trigger Target', 'triggerTargetPowerId', $this->string_value( $config, 'triggerTargetPowerId' ) ); } $auto_enabled = 'on' === $this->string_value( $config, 'autoTriggerEnabled', 'off' ); if ( $auto_enabled ) { $roles[] = 'Automatic Trigger'; $role_labels[] = __( 'Automatic Trigger', '5d-powers-for-divi' ); if ( '' === $power_id ) { $this->add_issue( $state, 'error', __( 'Automatic Trigger target has no Power ID', '5d-powers-for-divi' ), $node_id, $label, '' ); } $auto_event = $this->string_value( $config, 'autoTriggerEvent', 'load' ); if ( '' !== $power_id ) { $this->add_entry_point( $entry_points, $state, $node_id, $label, 'Automatic Trigger: ' . $this->humanize_slug( $auto_event ), 'autoTriggerEvent', $power_id, sprintf( /* translators: %s: automatic trigger event, such as On Page Load. */ __( 'Automatic Trigger: %s', '5d-powers-for-divi' ), $this->automatic_event_label( $auto_event ) ) ); } if ( in_array( $auto_event, array( 'viewport', 'pass' ), true ) ) { $this->add_reference( $references, $state, $node_id, $label, 'Watched Power ID', 'autoTriggerWatchPowerId', $this->string_value( $config, 'autoTriggerWatchPowerId' ) ); } } $switcher_enabled = 'on' === $this->string_value( $config, 'contentToggleEnabled', 'off' ); $switcher_mode = $this->string_value( $config, 'contentSwitcherMode', 'toggle' ); if ( $switcher_enabled && in_array( $click_action, array( 'toggle', 'show', 'hide' ), true ) ) { $this->add_issue( $state, 'warning', __( 'Content Switcher and normal click action are both configured', '5d-powers-for-divi' ), $node_id, $label, '' ); } if ( $switcher_enabled && 'toggle' === $switcher_mode ) { $roles[] = 'A/B Toggle'; $role_labels[] = __( 'A/B Toggle', '5d-powers-for-divi' ); $target_a = $this->string_value( $config, 'contentToggleTargetA' ); $target_b = $this->string_value( $config, 'contentToggleTargetB' ); $this->add_reference( $references, $state, $node_id, $label, 'A/B Content A', 'contentToggleTargetA', $target_a ); $this->add_reference( $references, $state, $node_id, $label, 'A/B Content B', 'contentToggleTargetB', $target_b ); $pair_id = trim( $target_a . ' ⇄ ' . $target_b ); if ( '' !== $target_a || '' !== $target_b ) { $this->add_group( $state, 'A/B Toggle', $pair_id, $node_id, $label, array_filter( array( $target_a, $target_b ) ) ); } } if ( $switcher_enabled && 'group' === $switcher_mode ) { $roles[] = 'Content Group Control'; $role_labels[] = __( 'Content Group Control', '5d-powers-for-divi' ); $group_id = $this->string_value( $config, 'contentGroupId' ); $action = $this->string_value( $config, 'contentGroupAction', 'activate' ); $group_ids[] = $group_id; if ( '' === $group_id ) { $this->add_issue( $state, 'error', __( 'Content Group control has no Group ID', '5d-powers-for-divi' ), $node_id, $label, '' ); } else { $target = 'activate' === $action ? $this->string_value( $config, 'contentGroupTargetPowerId' ) : ''; if ( 'activate' === $action ) { $this->add_reference( $references, $state, $node_id, $label, 'Content Group Target', 'contentGroupTargetPowerId', $target ); } $this->add_group( $state, 'Content Group', $group_id, $node_id, $label, array_filter( array( $target ) ) ); } } if ( $switcher_enabled && 'scroll' === $switcher_mode ) { $roles[] = 'Scroll Navigation Control'; $role_labels[] = __( 'Scroll Navigation Control', '5d-powers-for-divi' ); $group_id = $this->string_value( $config, 'scrollNavGroupId' ); $action = $this->string_value( $config, 'scrollNavAction', 'activate' ); $group_ids[] = $group_id; if ( '' === $group_id ) { $this->add_issue( $state, 'error', __( 'Scroll Navigation control has no Group ID', '5d-powers-for-divi' ), $node_id, $label, '' ); } else { $target = 'activate' === $action ? $this->string_value( $config, 'scrollNavTargetPowerId' ) : ''; if ( 'activate' === $action ) { $this->add_reference( $references, $state, $node_id, $label, 'Scroll Navigation Target', 'scrollNavTargetPowerId', $target ); } $this->add_group( $state, 'Scroll Navigation', $group_id, $node_id, $label, array_filter( array( $target ) ) ); } } $node = array( 'id' => $node_id, 'path' => implode( '.', $path ), 'label' => $label, 'module' => $this->humanize_module( $block_name ), 'block_name' => $block_name, 'power_id' => $power_id, 'roles' => array_values( array_unique( $roles ) ), 'role_labels' => array_values( array_unique( $role_labels ) ), 'presentation' => 'none' === $presentation ? '' : $presentation, 'group_ids' => array_values( array_filter( array_unique( $group_ids ) ) ), 'references' => array_values( $references ), 'entry_points' => array_values( $entry_points ), ); $state['nodes'][ $node_id ] = $node; if ( '' !== $power_id ) { if ( ! isset( $state['definitions'][ $power_id ] ) ) { $state['definitions'][ $power_id ] = array(); } $state['definitions'][ $power_id ][] = array( 'node_id' => $node_id, 'label' => $label, 'module' => $this->humanize_module( $block_name ), 'path' => implode( '.', $path ), ); } } /** * Add a target reference and a missing-value issue when needed. * * @param array> $node_references Node references. * @param array $state Scan state. * @param string $node_id Node ID. * @param string $label Admin label. * @param string $kind Reference kind. * @param string $field Settings field. * @param string $target_id Target Power ID. * @return void */ private function add_reference( &$node_references, &$state, $node_id, $label, $kind, $field, $target_id ) { $kind_label = $this->reference_kind_label( $kind ); if ( '' === $target_id ) { $this->add_issue( $state, 'error', sprintf( /* translators: %s: relationship type, such as Trigger Target. */ __( '%s is empty', '5d-powers-for-divi' ), $kind_label ), $node_id, $label, '' ); return; } $reference = array( 'node_id' => $node_id, 'label' => $label, 'kind' => $kind, 'kind_label' => $kind_label, 'field' => $field, 'target_id' => $target_id, ); $node_references[] = $reference; $state['references'][] = $reference; } /** * Register a non-click entry point that can activate a Power ID directly. * * Automatic triggers are valid activation sources even though no separate * Divi element references their target Power ID. Keeping these separate from * mapped references lets Power Map describe the journey accurately without * reporting intentional automatic targets as unused. * * @param array> $node_entry_points Node entry points. * @param array $state Scan state. * @param string $node_id Node ID. * @param string $label Admin label. * @param string $kind Entry-point kind. * @param string $field Settings field. * @param string $target_id Target Power ID. * @param string $kind_label Localized entry-point label. * @return void */ private function add_entry_point( &$node_entry_points, &$state, $node_id, $label, $kind, $field, $target_id, $kind_label = '' ) { if ( '' === $target_id ) { return; } $entry_point = array( 'node_id' => $node_id, 'label' => $label, 'kind' => $kind, 'kind_label' => '' !== $kind_label ? $kind_label : $kind, 'field' => $field, 'target_id' => $target_id, ); $node_entry_points[] = $entry_point; $state['entry_points'][] = $entry_point; } /** * Add or update a group record. * * @param array $state Scan state. * @param string $type Group type. * @param string $group_id Group ID. * @param string $node_id Node ID. * @param string $label Admin label. * @param array $targets Target IDs. * @return void */ private function add_group( &$state, $type, $group_id, $node_id, $label, $targets ) { if ( '' === $group_id ) { return; } $key = strtolower( str_replace( ' ', '-', $type ) ) . ':' . $group_id; if ( ! isset( $state['groups'][ $key ] ) ) { $state['groups'][ $key ] = array( 'key' => $key, 'type' => $type, 'type_label' => $this->group_type_label( $type ), 'id' => $group_id, 'controls' => array(), 'targets' => array(), ); } $state['groups'][ $key ]['controls'][] = array( 'node_id' => $node_id, 'label' => $label, ); $state['groups'][ $key ]['targets'] = array_values( array_unique( array_merge( $state['groups'][ $key ]['targets'], array_values( $targets ) ) ) ); } /** * Finalize diagnostics after every definition and reference is known. * * @param array $state Scan state. * @return void */ private function finalize_scan( &$state ) { foreach ( $state['definitions'] as $power_id => $definitions ) { if ( count( $definitions ) > 1 ) { $labels = wp_list_pluck( $definitions, 'label' ); $this->add_issue( $state, 'error', sprintf( /* translators: %s: duplicated Power ID. */ __( 'Duplicate Power ID: %s', '5d-powers-for-divi' ), $power_id ), '', implode( ', ', $labels ), $power_id ); } } foreach ( $state['references'] as $reference ) { $target_id = $reference['target_id']; if ( ! isset( $state['definitions'][ $target_id ] ) ) { $this->add_issue( $state, 'error', sprintf( /* translators: %s: missing target Power ID. */ __( 'Missing target: %s', '5d-powers-for-divi' ), $target_id ), $reference['node_id'], $reference['label'], $target_id ); } elseif ( count( $state['definitions'][ $target_id ] ) > 1 ) { $this->add_issue( $state, 'warning', sprintf( /* translators: %s: ambiguous target Power ID. */ __( 'Ambiguous target reference: %s', '5d-powers-for-divi' ), $target_id ), $reference['node_id'], $reference['label'], $target_id ); } } foreach ( $state['definitions'] as $power_id => $definitions ) { $used = false; foreach ( $state['references'] as $reference ) { if ( $power_id === $reference['target_id'] ) { $used = true; break; } } if ( ! $used ) { foreach ( $state['entry_points'] as $entry_point ) { if ( $power_id === $entry_point['target_id'] ) { $used = true; break; } } } if ( ! $used ) { $definition = reset( $definitions ); $this->add_issue( $state, 'info', sprintf( /* translators: %s: Power ID without mapped references. */ __( 'Power ID has no mapped references: %s', '5d-powers-for-divi' ), $power_id ), isset( $definition['node_id'] ) ? $definition['node_id'] : '', isset( $definition['label'] ) ? $definition['label'] : '', $power_id ); } } foreach ( $state['groups'] as &$group ) { $group['controls'] = array_values( $group['controls'] ); $group['targets'] = array_values( $group['targets'] ); $group['issues'] = array(); foreach ( $group['targets'] as $target_id ) { if ( ! isset( $state['definitions'][ $target_id ] ) ) { $group['issues'][] = sprintf( /* translators: %s: missing target Power ID. */ __( 'Missing target: %s', '5d-powers-for-divi' ), $target_id ); } } } unset( $group ); } /** * Add a deduplicated issue. * * @param array $state Scan state. * @param string $severity Severity. * @param string $message Message. * @param string $node_id Node ID. * @param string $label Label. * @param string $power_id Power ID. * @return void */ private function add_issue( &$state, $severity, $message, $node_id, $label, $power_id ) { $key = md5( $severity . '|' . $message . '|' . $node_id . '|' . $power_id ); $state['issues'][ $key ] = array( 'id' => 'issue-' . substr( $key, 0, 12 ), 'severity' => $severity, 'severity_label' => $this->severity_label( $severity ), 'message' => $message, 'node_id' => $node_id, 'label' => $label, 'power_id' => $power_id, ); } /** * Return a localized relationship kind without changing its stable payload value. * * @param string $kind Relationship kind. * @return string */ private function reference_kind_label( $kind ) { $labels = array( 'Trigger Target' => __( 'Trigger Target', '5d-powers-for-divi' ), 'Watched Power ID' => __( 'Watched Power ID', '5d-powers-for-divi' ), 'A/B Content A' => __( 'A/B Content A', '5d-powers-for-divi' ), 'A/B Content B' => __( 'A/B Content B', '5d-powers-for-divi' ), 'Content Group Target' => __( 'Content Group Target', '5d-powers-for-divi' ), 'Scroll Navigation Target' => __( 'Scroll Navigation Target', '5d-powers-for-divi' ), ); return isset( $labels[ $kind ] ) ? $labels[ $kind ] : $kind; } /** * Return a localized group type without changing its stable key. * * @param string $type Group type. * @return string */ private function group_type_label( $type ) { $labels = array( 'A/B Toggle' => __( 'A/B Toggle', '5d-powers-for-divi' ), 'Content Group' => __( 'Content Group', '5d-powers-for-divi' ), 'Scroll Navigation' => __( 'Scroll Navigation', '5d-powers-for-divi' ), ); return isset( $labels[ $type ] ) ? $labels[ $type ] : $type; } /** * Return a localized issue severity without changing its stable code. * * @param string $severity Severity code. * @return string */ private function severity_label( $severity ) { $labels = array( 'error' => __( 'Error', '5d-powers-for-divi' ), 'warning' => __( 'Warning', '5d-powers-for-divi' ), 'info' => __( 'Info', '5d-powers-for-divi' ), ); return isset( $labels[ $severity ] ) ? $labels[ $severity ] : $severity; } /** * Return a localized presentation-mode label. * * @param string $presentation Presentation mode. * @return string */ private function presentation_label( $presentation ) { $labels = array( 'modal' => __( 'Modal', '5d-powers-for-divi' ), 'drawer-left' => __( 'Left Drawer', '5d-powers-for-divi' ), 'drawer-right' => __( 'Right Drawer', '5d-powers-for-divi' ), 'bottom-sheet' => __( 'Bottom Sheet', '5d-powers-for-divi' ), 'tooltip' => __( 'Tooltip', '5d-powers-for-divi' ), 'popover' => __( 'Popover', '5d-powers-for-divi' ), ); return isset( $labels[ $presentation ] ) ? $labels[ $presentation ] : $this->humanize_slug( $presentation ); } /** * Return a localized automatic-trigger event label. * * @param string $event Automatic-trigger event. * @return string */ private function automatic_event_label( $event ) { $labels = array( 'load' => __( 'On Page Load', '5d-powers-for-divi' ), 'delay' => __( 'After a Delay', '5d-powers-for-divi' ), 'scroll' => __( 'At a Scroll Percentage', '5d-powers-for-divi' ), 'viewport' => __( 'When a Power ID Enters the Viewport', '5d-powers-for-divi' ), 'pass' => __( 'After Passing a Power ID', '5d-powers-for-divi' ), 'inactivity' => __( 'After Visitor Inactivity', '5d-powers-for-divi' ), 'exit' => __( 'Desktop Exit Intent', '5d-powers-for-divi' ), 'hash' => __( 'When URL Hash Matches', '5d-powers-for-divi' ), 'custom' => __( 'Custom JavaScript Event', '5d-powers-for-divi' ), ); return isset( $labels[ $event ] ) ? $labels[ $event ] : $this->humanize_slug( $event ); } /** * Recursively replace Power ID fields in parsed blocks. * * @param array> $blocks Parsed blocks. * @param string $old_id Old ID. * @param string $new_id New ID. * @param int $changed Changed count. * @return void */ private function rename_in_blocks( &$blocks, $old_id, $new_id, &$changed ) { foreach ( $blocks as &$block ) { if ( ! is_array( $block ) ) { continue; } if ( isset( $block['attrs']['diviPowersDismissible']['innerContent']['desktop']['value'] ) && is_array( $block['attrs']['diviPowersDismissible']['innerContent']['desktop']['value'] ) ) { $value =& $block['attrs']['diviPowersDismissible']['innerContent']['desktop']['value']; foreach ( self::POWER_ID_FIELDS as $field ) { if ( isset( $value[ $field ] ) && is_string( $value[ $field ] ) && $old_id === trim( $value[ $field ] ) ) { $value[ $field ] = $new_id; $changed++; } } unset( $value ); } if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) { $this->rename_in_blocks( $block['innerBlocks'], $old_id, $new_id, $changed ); } } unset( $block ); } /** * Return the shared Divi Powers value object. * * @param array $attrs Block attributes. * @return array */ private function get_power_config( $attrs ) { $value = isset( $attrs['diviPowersDismissible']['innerContent']['desktop']['value'] ) ? $attrs['diviPowersDismissible']['innerContent']['desktop']['value'] : array(); return is_array( $value ) ? $value : array(); } /** * Return a trimmed string setting. * * @param array $config Config. * @param string $key Key. * @param string $fallback Fallback. * @return string */ private function string_value( $config, $key, $fallback = '' ) { if ( ! isset( $config[ $key ] ) || ! is_scalar( $config[ $key ] ) ) { return $fallback; } return trim( (string) $config[ $key ] ); } /** * Return the module admin label. * * @param array $attrs Block attributes. * @param string $block_name Block name. * @return string */ private function get_admin_label( $attrs, $block_name ) { $label = isset( $attrs['module']['meta']['adminLabel']['desktop']['value'] ) ? trim( (string) $attrs['module']['meta']['adminLabel']['desktop']['value'] ) : ''; return '' !== $label ? $label : $this->humanize_module( $block_name ); } /** * Humanize a Divi block name. * * @param string $block_name Block name. * @return string */ private function humanize_module( $block_name ) { if ( class_exists( 'WP_Block_Type_Registry' ) ) { $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name ); if ( $block_type && ! empty( $block_type->title ) ) { return (string) $block_type->title; } } $name = preg_replace( '/^divi\//', '', $block_name ); $name = str_replace( array( '-', '_' ), ' ', (string) $name ); return ucwords( $name ); } /** * Humanize a slug. * * @param string $slug Slug. * @return string */ private function humanize_slug( $slug ) { return ucwords( str_replace( array( '-', '_' ), ' ', $slug ) ); } }