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;
}
}
}