register( 'adpresso-shortcode-wrong-type-' . $id, sprintf( /* translators: 1: shortcode tag, 2: post ID, 3: expected entity type (Ad/Group). */ __( '[%1$s id="%2$d"] does not point to a valid %3$s - double-check the ID.', 'adpresso' ), $shortcode_tag, $id, $expected_type ), [ 'location' => 'admin_bar', 'context' => 'adpresso-notifications', 'type' => 'warning', ] ); } /** * Renders a physical, manual placement. * * @param int $placement_id The ID of the Placements. * @param string $trigger Where it has been triggered. * * @return string The rendered HTML. */ public static function render_manual_placement( int $placement_id, string $trigger, array $shortcode_atts = [] ): string { if ( ! $placement_id ) { return ''; } if ( Runtime::is_globally_disabled() ) { RuntimeDebugger::add_server_error_log_entry( 'AdPresso: Manual placement #' . $placement_id . ' skipped - ' . Runtime::get_global_disable_reason() ); return ''; } $placement = adpresso_get_placement( $placement_id ); if ( ! $placement ) { return ''; } self::apply_shortcode_overrides( $placement, $shortcode_atts ); $processed_data = Runtime::get_processed_data(); $placement_data = $processed_data[ $placement_id ] ?? null; $item = $placement_data['item'] ?? $placement->get_item(); // Prepare debugging. $snapshot = RuntimeDebugger::build_snapshot( $placement, $item ); $snapshot['execution']['trigger'] = $trigger; $snapshot['execution']['hook_fired'] = true; if ( $trigger === 'shortcode' && $placement->get_type() !== 'manual' ) { // Wir erlauben das Rendern von nicht-manuellen Placements via Shortcode (z.B. für A2 Takeover), // warnen aber im Debugger, da dies nicht der Standardweg ist. $snapshot['execution']['warning'] = __( 'Rendering a non-manual placement via shortcode', 'adpresso' ); } elseif ( $trigger !== 'shortcode' && $placement->get_type() !== 'manual' ) { $snapshot['execution']['status'] = 'error'; $snapshot['execution']['skip_reason'] = __( 'Wrong usage of manual placement', 'adpresso' ); RuntimeDebugger::add_log_entry( $placement_id, $snapshot ); RuntimeDebugger::mark_output_triggered( $placement_id ); NotificationManager::get()->register( 'adpresso-manual-placement', __( 'Wrong usage of manual placement', 'adpresso' ), [ 'location' => 'admin_bar', 'context' => 'adpresso-notifications', 'type' => 'info', ] ); return ''; } // RENDERING LOGIK if ( ! $placement_data ) { // Fallback-Loading for physical manual placements that haven't been processed yet. // This is also the routine path for a manual placement whose item is invalid with // no fallback configured, since _prepare_placement_output() returns null for that // case - so $item being a non-null object here does NOT mean it's displayable. $item = $placement->get_item(); $is_item_valid = ( $item instanceof Ad || $item instanceof Group ) && $item->can_be_displayed(); RuntimeDebugger::set_current_placement( $placement_id ); $requires_frontend_logic = apply_filters( 'adpresso_requires_frontend_logic', $placement->requires_frontend_logic || ( $is_item_valid && $item->requires_frontend_logic ), $placement, $item ); RuntimeDebugger::set_current_placement( null ); if ( $requires_frontend_logic ) { $output = $placement->get_placeholder_html(); $snapshot['execution']['output_mode'] = 'js'; $snapshot['execution']['js_reasons'] = self::collect_js_reasons( $placement, $item, $is_item_valid, $placement_id ); // get_uid() returns the literal string '0' for every physical (post-backed) placement. $uid = $placement->get_id() ?: $placement->get_uid(); $config = $placement->frontend_config_data; $config['uid'] = $uid; // Placement::processBackendData() already added the consent flag to its copy of the item config. $config['item'] = $is_item_valid ? ( $placement->frontend_config_data['item'] ?? $item->frontend_config_data ) : null; Runtime::add_virtual_placement_config( $uid, $config ); // A valid item shipped here still needs to tell Ad_Rendered_Tracker it was rendered if ( $is_item_valid ) { $item->mark_as_rendered(); } } else { $renderer = PlacementRenderer::get_instance(); $output = $renderer->render( $placement, '' ); $snapshot['execution']['output_mode'] = 'fallback'; } } else { $snapshot['execution']['output_mode'] = $placement_data['output_mode'] ?? 'unknown'; if ( ! empty( $placement_data['skip_reason'] ) ) { $snapshot['execution']['skip_reason'] = $placement_data['skip_reason']; if ( $placement_data['output_mode'] === 'skipped' ) { $snapshot['execution']['status'] = 'skipped'; } } $output = match ( $placement_data['output_mode'] ) { 'ssr' => $placement->render_with_wrapper( self::get_ssr_content( $placement, $placement_data ) ), 'js' => $placement->get_placeholder_html(), default => '', }; if ( ( $placement_data['output_mode'] ?? '' ) === 'js' ) { Assets::enqueue_render_chain(); } } RuntimeDebugger::add_log_entry( $placement_id, $snapshot ); RuntimeDebugger::mark_output_triggered( $placement_id ); self::$has_rendered_output = self::$has_rendered_output || $output !== ''; return $output; } /** * The processor pre-renders placements before any shortcode attributes are known. * An inline="true" shortcode re-renders the same entity inline, so a block-level
doesn't end up inside the wrapper. * * @param Placement $placement * @param array $placement_data Processor result for this placement. * * @return string */ private static function get_ssr_content( Placement $placement, array $placement_data ): string { $content = (string) $placement_data['content']; $entity = $placement_data['rendered_entity'] ?? null; if ( ! $placement->is_inline_override || ! ( $entity instanceof Ad || $entity instanceof Group ) ) { return $content; } $inline_content = $entity->render_ssr( [ 'placement_id' => $placement->get_id(), 'placement' => $placement, 'is_raw_output' => $placement->outputs_raw_items(), 'is_inline_output' => true, ] ); return $inline_content ?: $content; } /** * "Why JS?" reasons for the debug bar, for placements rendered outside the pipeline processor. * * @param Placement $placement * @param mixed $item * @param bool $is_item_valid * @param int|string|null $placement_key Key the placement's module reasons were noted under. * * @return string[] */ private static function collect_js_reasons( Placement $placement, $item, bool $is_item_valid, $placement_key ): array { if ( ! RuntimeDebugger::is_active( 1 ) || ! ( $item instanceof Ad || $item instanceof Group ) ) { return []; } $reasons = AdPipelineProcessor::collect_js_reasons( $placement, $item, $is_item_valid ); return array_values( array_unique( array_merge( $reasons, RuntimeDebugger::get_placement_js_reasons( $placement_key ) ) ) ); } /** * Renders an ad or group with a virtual placement. * * @param Entity $entity The Ad- or Group-Object. * @param string $trigger The origin of the call (e.g. 'shortcode', 'manual_block'). * * @return string The rendered HTML. */ public static function render_virtual_entity( Entity $entity, string $trigger, array $shortcode_atts = [] ): string { if ( ! ( $entity instanceof Ad || $entity instanceof Group ) ) { return ''; } if ( Runtime::is_globally_disabled() ) { RuntimeDebugger::add_server_error_log_entry( 'AdPresso: Virtual entity #' . $entity->get_id() . ' skipped - ' . Runtime::get_global_disable_reason() ); return ''; } $prefix = $entity instanceof Ad ? 'ad' : 'group'; $placement = PlacementFactory::make_virtual( $prefix, $entity->get_id() ); self::apply_shortcode_overrides( $placement, $shortcode_atts ); $placement->processBackendData(); $uid = $placement->get_uid(); $placement->frontend_config_data['uid'] = $uid; $context = [ // A virtual placement has no real ID (get_id() returns its uniqid); 0 = shown without a placement. 'placement_id' => 0, 'placement' => $placement, 'is_raw_output' => $placement->outputs_raw_items(), 'is_inline_output' => $placement->is_inline_override, ]; if ( ! $entity->can_be_displayed() ) { $entity->is_backend_valid = false; $entity->skip_reason = 'Display once limit reached (Virtual Placement)'; } $output = ''; if ( $entity->is_backend_valid ) { RuntimeDebugger::set_current_placement( $uid ); $requires_frontend_logic = apply_filters( 'adpresso_requires_frontend_logic', $entity->requires_frontend_logic, $placement, $entity ); RuntimeDebugger::set_current_placement( null ); if ( $requires_frontend_logic ) { $js_reasons = self::collect_js_reasons( $placement, $entity, true, $uid ); $output = $placement->get_placeholder_html(); Runtime::add_virtual_placement_config( $uid, $placement->frontend_config_data ); $entity->mark_as_rendered(); } else { $renderer = PlacementRenderer::get_instance(); $type_object = $entity->get_type_object(); try { if ( ! $type_object || ! method_exists( $type_object, 'output' ) ) { throw new \RuntimeException( sprintf( 'Type "%s" is not registered.', $entity->get_type() ) ); } // Same entry point as the pipeline processor: render_ssr() adds the group context and display-once filtering. $inner_content = (string) $entity->render_ssr( $context ); $output = $renderer->render( $placement, $inner_content ); if ( ! empty( $inner_content ) ) { $entity->mark_as_rendered(); } } catch ( \Throwable $e ) { $output = ''; $entity->skip_reason = 'Rendering exception: ' . $e->getMessage(); } } } $snapshot = RuntimeDebugger::build_snapshot( $placement, $entity ); $snapshot['execution']['trigger'] = $trigger . '_virtual'; $snapshot['execution']['output_mode'] = ( $requires_frontend_logic ?? $entity->requires_frontend_logic ) ? 'js' : 'ssr'; $snapshot['execution']['hook_fired'] = true; if ( ! empty( $js_reasons ) ) { $snapshot['execution']['js_reasons'] = $js_reasons; } RuntimeDebugger::add_log_entry( $uid, $snapshot ); RuntimeDebugger::mark_output_triggered( $uid ); self::$has_rendered_output = self::$has_rendered_output || $output !== ''; return $output; } /** * Applies shortcode attributes (margin, alignment, inline) to the placement at runtime. */ public static function apply_shortcode_overrides( Placement $placement, array $atts ): void { if ( empty( $atts ) ) { return; } if ( ! empty( $atts['class'] ) ) { $placement->classes_override = sanitize_text_field( $atts['class'] ); } $css = []; if ( ! empty( $atts['margin'] ) ) { $css[] = 'margin: ' . esc_attr( $atts['margin'] ) . ' !important;'; } if ( ! empty( $atts['align'] ) ) { $align = strtolower( sanitize_text_field( $atts['align'] ) ); // Accepts both the plain values ('center'/'left'/'right') and the admin Positioning field's vocabulary ('center_nofloat', 'left_float'/'left_nofloat', 'right_float'/'right_nofloat'). if ( in_array( $align, [ 'center', 'center_nofloat' ], true ) ) { $css[] = 'margin-left: auto !important; margin-right: auto !important; display: block !important;'; $placement->classes_override .= ' adpresso-align-center'; } elseif ( in_array( $align, [ 'left', 'left_float', 'left_nofloat' ], true ) ) { $css[] = 'float: left !important;'; $placement->classes_override .= ' adpresso-align-left'; } elseif ( in_array( $align, [ 'right', 'right_float', 'right_nofloat' ], true ) ) { $css[] = 'float: right !important;'; $placement->classes_override .= ' adpresso-align-right'; } } if ( ! empty( $css ) ) { $placement->css_override = implode( ' ', $css ); } // A bare `inline` attribute (no value) parses as a numeric-indexed. $inline_value = isset( $atts['inline'] ) ? strtolower( trim( (string) $atts['inline'] ) ) : null; if ( $inline_value === 'true' || in_array( 'inline', $atts, true ) ) { $placement->is_inline_override = true; } } }