post_content, 'advk_calendar' ) || has_block( 'adventura/calendar', $post ); if ( ! $has ) { return; } $opts = ADVK_Settings::get_options(); $title = trim( (string) $opts['title_line1'] . ' ' . (string) $opts['title_line2'] ); if ( '' === $title ) { $title = get_the_title( $post ); } $desc = (string) $opts['subtitle']; $image = ''; if ( ! empty( $opts['share_image_id'] ) ) { $image = (string) wp_get_attachment_image_url( (int) $opts['share_image_id'], 'large' ); } if ( '' === $image && ! empty( $opts['share_image_url'] ) ) { $image = (string) $opts['share_image_url']; // legacy fallback } if ( '' === $image && function_exists( 'get_site_icon_url' ) ) { $image = get_site_icon_url( 512 ); } $url = get_permalink( $post ); echo "\n"; printf( '%s', "\n" ); printf( '%s', esc_attr( $title ), "\n" ); if ( '' !== $desc ) { printf( '%s', esc_attr( $desc ), "\n" ); } printf( '%s', esc_url( $url ), "\n" ); if ( '' !== $image ) { printf( '%s', esc_url( $image ), "\n" ); } printf( '%s', '' !== $image ? 'summary_large_image' : 'summary', "\n" ); } /** * `[advk_winners]` — a public, privacy-friendly winners list. Shows the day, * the winner's first name + last initial, and the prize. Attributes: `day` * (0 = all), `limit` (0 = no limit). * * @param array $atts Shortcode attributes. * @return string */ public function render_winners( $atts ) { $atts = shortcode_atts( array( 'day' => 0, 'limit' => 0 ), $atts, 'advk_winners' ); $day = max( 0, min( 24, (int) $atts['day'] ) ); $limit = max( 0, (int) $atts['limit'] ); $winners = ADVK_Entries::query( array( 'per_page' => $limit > 0 ? $limit : 100000, 'offset' => 0, 'day' => $day, 'winners_only' => true, 'orderby' => 'day', 'order' => 'ASC', ) ); $opts = ADVK_Settings::get_options(); $theme = in_array( $opts['theme'], array( 'beere', 'tanne', 'amber', 'winter', 'halloween', 'ostern', 'custom' ), true ) ? $opts['theme'] : 'beere'; $vars = $this->layout_css_vars( $opts ); if ( 'custom' === $theme ) { $color_vars = $this->custom_css_vars( $opts ); if ( '' !== $color_vars ) { $vars .= ';' . $color_vars; } } $style_attr = '' !== $vars ? ' style="' . esc_attr( $vars ) . '"' : ''; wp_enqueue_style( 'advk-frontend' ); ob_start(); echo '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- attrs escaped above. echo '
'; echo '

' . esc_html__( 'Unsere Gewinner', 'adventura-interactive-advent-calendar' ) . '

'; if ( empty( $winners ) ) { echo '

' . esc_html__( 'Die Gewinner werden hier bekannt gegeben, sobald die Auslosung erfolgt ist.', 'adventura-interactive-advent-calendar' ) . '

'; } else { echo ''; } echo '
'; return ob_get_clean(); } public function register_assets() { // Fonts are bundled locally (no Google CDN request) for DSGVO / // WordPress.org compliance. wp_register_style( 'advk-fonts', ADVK_URL . 'assets/css/fonts.css', array(), ADVK_VERSION ); wp_register_style( 'advk-frontend', ADVK_URL . 'assets/css/adventura.css', array( 'advk-fonts' ), ADVK_VERSION ); wp_register_script( 'advk-frontend', ADVK_URL . 'assets/js/adventura.js', array(), ADVK_VERSION, true ); } /** * Build the inline CSS-variable overrides for the "custom" theme. * * @param array $opts Plugin options. * @return string Inline CSS (without the surrounding selector). */ private function custom_css_vars( $opts ) { $map = array( '--bg' => $opts['custom_bg'], '--bg-2' => $opts['custom_bg'], '--ink' => $opts['custom_ink'], '--accent' => $opts['custom_accent'], '--accent-2' => $opts['custom_accent2'], ); $parts = array(); foreach ( $map as $var => $val ) { $hex = sanitize_hex_color( (string) $val ); if ( $hex ) { $parts[] = $var . ':' . $hex; } } return implode( ';', $parts ); } /** * Layout CSS custom properties (columns per row + calendar max-width) that * apply to every theme, not just the custom palette. Rendered inline on the * .advk-app wrapper so the frontend CSS can pick them up. * * @param array $opts Plugin options. * @return string Inline CSS (without the surrounding selector). */ private function layout_css_vars( $opts ) { $cols = (int) ( $opts['grid_columns'] ?? 6 ); if ( ! in_array( $cols, array( 3, 4, 6, 8 ), true ) ) { $cols = 6; } // Fewer columns on small screens so tiles never get too tiny. $cols_m = min( $cols, 4 ); $widths = array( 'compact' => '820px', 'normal' => '1060px', 'wide' => '1320px', 'full' => 'none', ); $wkey = isset( $opts['calendar_width'] ) ? (string) $opts['calendar_width'] : 'normal'; $max = isset( $widths[ $wkey ] ) ? $widths[ $wkey ] : $widths['normal']; return '--advk-cols:' . $cols . ';--advk-cols-m:' . $cols_m . ';--advk-shell-max:' . $max; } /** * Prepare an admin-authored rich-text field for display in the frontend. * The content is saved with wp_kses_post (only safe HTML), so here we just * turn blank lines into paragraphs (wpautop) and re-sanitise defensively. * The JS renders the result as HTML, so sponsors/prizes/donation copy may * use standard formatting (paragraphs, links, bold, lists …). * * @param string $raw Stored text (may contain HTML or plain line breaks). * @return string Display-ready HTML, or '' when empty. */ public static function rich_text( $raw ) { $raw = (string) $raw; if ( '' === trim( $raw ) ) { return ''; } return wp_kses_post( wpautop( $raw ) ); } /** * Strip the hidden `correct` index from quiz data before it reaches the * browser — participants never see which answer is right. * * @param array $doors Calendar doors. * @return array */ private function public_doors( $doors ) { $out = array(); foreach ( $doors as $d ) { $quiz = array(); foreach ( (array) $d['quiz'] as $q ) { $quiz[] = array( 'q' => $q['q'], 'options' => array_values( $q['options'] ), ); } // Only expose URLs for channels actually offered on this door. $offered = array_values( $d['social'] ); $social_urls = array(); $all_urls = isset( $d['socialUrls'] ) && is_array( $d['socialUrls'] ) ? $d['socialUrls'] : array(); foreach ( $offered as $skey ) { if ( ! empty( $all_urls[ $skey ] ) ) { $social_urls[ $skey ] = esc_url_raw( $all_urls[ $skey ] ); } } // Prizes are public display data; expose the whole list. $prizes = array(); if ( isset( $d['prizes'] ) && is_array( $d['prizes'] ) ) { foreach ( $d['prizes'] as $p ) { $prizes[] = array( 'title' => isset( $p['title'] ) ? $p['title'] : '', 'text' => self::rich_text( isset( $p['text'] ) ? $p['text'] : '' ), 'value' => isset( $p['value'] ) ? $p['value'] : '', 'qty' => isset( $p['qty'] ) ? max( 1, (int) $p['qty'] ) : 1, 'image' => isset( $p['image'] ) ? $p['image'] : '', ); } } $out[] = array( 'day' => (int) $d['day'], 'sponsor' => $d['sponsor'], 'sponsorText' => self::rich_text( $d['sponsorText'] ), 'sponsorUrl' => isset( $d['sponsorUrl'] ) ? esc_url_raw( $d['sponsorUrl'] ) : '', 'prize' => $d['prize'], 'prizeText' => self::rich_text( $d['prizeText'] ), 'value' => $d['value'], 'prizes' => $prizes, 'prizeTotal' => isset( $d['winnerCount'] ) ? (int) $d['winnerCount'] : count( $prizes ), 'logo' => $d['logo'], 'prizeImage' => $d['prizeImage'], 'image' => isset( $d['image'] ) ? $d['image'] : '', 'donateUrl' => isset( $d['donateUrl'] ) ? esc_url_raw( $d['donateUrl'] ) : '', 'donateText' => self::rich_text( isset( $d['donateText'] ) ? $d['donateText'] : '' ), 'social' => $offered, 'socialUrls' => $social_urls, 'quiz' => $quiz, ); } return $out; } public function render( $atts ) { $opts = ADVK_Settings::get_options(); $atts = shortcode_atts( array( 'theme' => $opts['theme'], 'layout' => $opts['layout'], 'door_style' => $opts['door_style'], 'quiz_mode' => $opts['quiz_mode'], ), $atts, 'advk_calendar' ); // Validate attribute overrides against known values. $theme = in_array( $atts['theme'], array( 'beere', 'tanne', 'amber', 'winter', 'halloween', 'ostern', 'custom' ), true ) ? $atts['theme'] : $opts['theme']; $layout = in_array( $atts['layout'], array( 'grid', 'scene' ), true ) ? $atts['layout'] : $opts['layout']; $door_style = in_array( $atts['door_style'], array( 'classic', 'ornament', 'gift' ), true ) ? $atts['door_style'] : $opts['door_style']; $quiz_mode = in_array( $atts['quiz_mode'], array( 'guided', 'single' ), true ) ? $atts['quiz_mode'] : $opts['quiz_mode']; $doors = ADVK_Data::get_calendar(); $data = array( 'doors' => $this->public_doors( $doors ), 'today' => ADVK_Data::today_day(), 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'advk_submit' ), 'settings' => array( 'layout' => $layout, 'doorStyle' => $door_style, 'quizMode' => $quiz_mode, 'count' => ADVK_Data::door_count(), 'itemSingular' => (string) $opts['item_singular'], 'itemPlural' => (string) $opts['item_plural'], 'periodLabel' => (string) $opts['period_label'], 'particles' => (string) $opts['particles'], 'year' => (int) $opts['year'], 'eyebrow' => ( isset( $opts['eyebrow'] ) && '' !== trim( (string) $opts['eyebrow'] ) ) ? $opts['eyebrow'] : 'Adventura', 'title1' => $opts['title_line1'], 'title2' => $opts['title_line2'], 'subtitle' => $opts['subtitle'], 'sound' => ! empty( $opts['celebrate_sound'] ), 'melody' => isset( $opts['celebrate_melody'] ) ? $opts['celebrate_melody'] : 'chime', 'newsletter' => ! empty( $opts['newsletter_enabled'] ) ? array( 'label' => $opts['newsletter_label'], ) : false, 'reminder' => ! empty( $opts['reminder_enabled'] ) ? array( 'label' => $opts['reminder_label'], ) : false, 'referral' => ! empty( $opts['referral_enabled'] ), 'credit' => ! empty( $opts['credit_badge'] ), 'showSponsor' => ! empty( $opts['show_sponsor'] ), 'showPrizes' => ! empty( $opts['show_prizes'] ), 'share' => ! empty( $opts['share_enabled'] ), // Logged-in admins may open and play through every door regardless // of the date (preview/testing). The server mirrors this bypass. 'adminPreview' => current_user_can( 'manage_options' ), 'turnstile' => ( ! empty( $opts['turnstile_enabled'] ) && '' !== trim( (string) $opts['turnstile_site_key'] ) ) ? array( 'enabled' => true, 'siteKey' => (string) $opts['turnstile_site_key'] ) : false, ), 'i18n' => self::ui_strings(), ); wp_enqueue_style( 'advk-frontend' ); wp_enqueue_script( 'advk-frontend' ); // The calendar data is emitted as a JSON data island inside the markup // (below) rather than via wp_localize_script. The frontend reads it with // textContent + JSON.parse, so it keeps working even when a consent // manager (e.g. Complianz) or a cache/optimizer rewrites inline '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON encoded with JSON_HEX_TAG|JSON_HEX_AMP, safe inside a