true, 'id' => true, 'style' => true, 'title' => true, 'role' => true, 'aria-label' => true, 'aria-hidden' => true, 'tabindex' => true, 'data-*' => true, ); $extra = array( 'iframe' => array( 'src' => true, 'width' => true, 'height' => true, 'frameborder' => true, 'allow' => true, 'allowfullscreen' => true, 'allowtransparency' => true, 'loading' => true, 'referrerpolicy' => true, 'sandbox' => true, 'name' => true, ), 'img' => array( 'srcset' => true, 'sizes' => true, 'decoding' => true, 'loading' => true, 'fetchpriority' => true, ), 'svg' => array( 'viewbox' => true, 'xmlns' => true, 'width' => true, 'height' => true, 'fill' => true, 'stroke' => true, 'focusable' => true, ), 'path' => array( 'd' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, ), 'rect' => array( 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, 'fill' => true, 'stroke' => true, ), 'circle' => array( 'cx' => true, 'cy' => true, 'r' => true, 'fill' => true, 'stroke' => true, ), 'form' => array( 'action' => true, 'method' => true, ), 'input' => array( 'type' => true, 'name' => true, 'value' => true, 'checked' => true, 'required' => true, 'min' => true, 'max' => true, 'maxlength' => true, 'autocomplete' => true, 'placeholder' => true, 'list' => true, ), 'select' => array( 'name' => true, 'required' => true, ), 'option' => array( 'value' => true, 'selected' => true, ), 'textarea' => array( 'name' => true, 'rows' => true, 'maxlength' => true, 'required' => true, ), 'button' => array( 'type' => true, 'name' => true, ), 'label' => array( 'for' => true, ), 'datalist' => array(), 'time' => array( 'datetime' => true, ), ); foreach ( $extra as $tag => $attrs ) { $allowed[ $tag ] = array_merge( isset( $allowed[ $tag ] ) ? $allowed[ $tag ] : array(), $attrs ); } foreach ( $allowed as $tag => $attrs ) { $allowed[ $tag ] = array_merge( is_array( $attrs ) ? $attrs : array(), $global ); } return $allowed; } /* ---------- Settings ---------- */ function clanfront_default_settings() { return array( 'clan_name' => 'Your Clan', 'tagline' => 'Play together. Win together.', 'mission' => 'We are a community of players who love competing and having fun together. Write your mission statement here: who you are, what you stand for, and who you are looking for.', 'logo' => '', 'hero_image' => '', // Empty = the banner that ships with the plugin. 'hero_video' => '', // Optional .mp4 address. Plays silently behind the headline. 'color' => '#7c5cff', 'layout' => 'centered', // centered or full (edge to edge). 'join_label' => 'Join us', 'values' => '', 'games' => '', 'achievements' => '', 'recruiting' => '1', 'requirements' => '', 'min_age' => 0, 'application_url' => '', 'application_email' => '', 'rules' => '', 'discord_invite' => '', 'discord_server_id' => '', 'twitch_client_id' => '', 'twitch_client_secret' => '', 'twitch_videos' => '1', // Latest Twitch videos on the homepage and member pages (needs the Twitch keys). 'show_tournaments' => '0', // Tournaments stay hidden from visitors until switched on. 'email' => '', 'instagram' => '', 'x' => '', 'youtube_channel' => '', 'tiktok' => '', 'twitch_channel' => '', 'support_url' => '', // Optional "Support us" link, for example a Buy Me a Coffee page. 'support_label' => 'Support us', ); } /** Settings that are long text and keep their line breaks. */ function clanfront_textarea_keys() { return array( 'mission', 'values', 'games', 'achievements', 'requirements', 'rules' ); } function clanfront_settings() { $saved = get_option( 'clanfront_settings', array() ); return wp_parse_args( is_array( $saved ) ? $saved : array(), clanfront_default_settings() ); } function clanfront_setting( $key ) { $s = clanfront_settings(); $v = isset( $s[ $key ] ) ? $s[ $key ] : ''; if ( 'hero_image' === $key ) { return '' === $v ? CLANFRONT_URL . 'assets/img/hero.jpg' : $v; } if ( 'clan_name' === $key && '' === $v ) { $d = clanfront_default_settings(); return $d['clan_name']; } if ( 'color' === $key ) { return preg_match( '/^#[0-9a-fA-F]{6}$/', (string) $v ) ? $v : '#7c5cff'; } if ( 'application_email' === $key && '' === $v ) { return (string) get_option( 'admin_email' ); } return $v; } function clanfront_name() { return clanfront_setting( 'clan_name' ); } /** * Make an image address from this site work from anywhere. * An address saved as http://192.168.x.x/wp-content/... (or any other host) * becomes /wp-content/..., so the browser loads it from whatever address the * visitor used: local IP, domain, http or https. Links to other sites are left alone. */ function clanfront_local_url( $url ) { $url = (string) $url; $path = (string) wp_parse_url( $url, PHP_URL_PATH ); $base = (string) wp_parse_url( content_url(), PHP_URL_PATH ); // Usually "/wp-content". if ( $path && $base && 0 === strpos( $path, rtrim( $base, '/' ) . '/' ) ) { $query = (string) wp_parse_url( $url, PHP_URL_QUERY ); return $path . ( $query ? '?' . $query : '' ); } return $url; } /** A setting with one item per line, as a list of non-empty trimmed lines. */ function clanfront_lines( $key ) { return array_values( array_filter( array_map( 'trim', preg_split( '/\r\n|\r|\n/', (string) clanfront_setting( $key ) ) ), 'strlen' ) ); } /** "Title | text" lines as a list of array( title, text ). */ function clanfront_pairs( $key ) { $out = array(); foreach ( clanfront_lines( $key ) as $line ) { $parts = array_map( 'trim', explode( '|', $line, 2 ) ); $out[] = array( $parts[0], isset( $parts[1] ) ? $parts[1] : '' ); } return $out; } /** The games the clan plays: the game pages (Clanfront → Games) and the "Games" setting (the name is the part before a |). */ function clanfront_game_names() { $names = array(); if ( function_exists( 'clanfront_get_games' ) ) { foreach ( clanfront_get_games( 'active' ) as $g ) { $names[] = get_the_title( $g ); } } foreach ( clanfront_pairs( 'games' ) as $p ) { $names[] = $p[0]; } return array_values( array_unique( $names ) ); } /** The game page with this name, or null. */ function clanfront_game_by_name( $name ) { $name = trim( (string) $name ); if ( '' === $name || ! function_exists( 'clanfront_get_games' ) ) { return null; } foreach ( clanfront_get_games( 'all' ) as $g ) { if ( 0 === strcasecmp( trim( get_the_title( $g ) ), $name ) ) { return $g; } } return null; } /** The optional "Support us" button (a Buy Me a Coffee page or similar), or ''. */ function clanfront_support_link( $class = 'clanfront-btn' ) { $url = clanfront_clean_url( clanfront_setting( 'support_url' ) ); if ( ! $url ) { return ''; } $label = trim( (string) clanfront_setting( 'support_label' ) ); return '' . esc_html( '' !== $label ? $label : 'Support us' ) . ''; } /* ---------- Social and link helpers ---------- */ /** The footer social icons: setting key => label. */ function clanfront_social_fields() { return array( 'twitch_channel' => 'Twitch', 'youtube_channel' => 'YouTube', 'x' => 'X', 'instagram' => 'Instagram', 'tiktok' => 'TikTok', ); } /** A Twitch channel name from a name or a link such as https://twitch.tv/name. */ function clanfront_twitch_login( $value ) { $value = trim( (string) $value ); if ( '' === $value ) { return ''; } if ( preg_match( '#twitch\.tv/([A-Za-z0-9_]{2,25})#i', $value, $m ) ) { return strtolower( $m[1] ); } return preg_match( '/^[A-Za-z0-9_]{2,25}$/', $value ) ? strtolower( $value ) : ''; } /** Only web links. Anything else becomes an empty string. */ function clanfront_clean_url( $url ) { $url = trim( (string) $url ); if ( '' === $url ) { return ''; } return esc_url_raw( $url, array( 'http', 'https' ) ); } /* ---------- Dates ---------- */ function clanfront_today() { return current_time( 'Y-m-d' ); } function clanfront_format_date( $ymd ) { if ( ! $ymd ) { return ''; } $ts = strtotime( $ymd . ' 12:00:00' ); return $ts ? date_i18n( 'M j, Y', $ts ) : ''; } function clanfront_format_time( $hm ) { if ( ! $hm || ! preg_match( '/^\d{2}:\d{2}$/', $hm ) ) { return ''; } $ts = strtotime( '2000-01-01 ' . $hm . ':00' ); return $ts ? date_i18n( get_option( 'time_format', 'H:i' ), $ts ) : ''; } /* ---------- Members ---------- */ /** Roles offered in the member form. Any other text also works. */ function clanfront_roles() { return array( 'Leader', 'Co-Leader', 'Captain', 'Coach', 'Recruiter', 'Officer', 'Player', 'Member' ); } /** Active members, in the order set in the admin (lowest "Order" first). */ function clanfront_get_members( $opts = array() ) { $o = wp_parse_args( $opts, array( 'limit' => -1 ) ); $all = get_posts( array( 'post_type' => 'clanfront_member', 'post_status' => 'publish', 'posts_per_page' => -1, 'orderby' => array( 'menu_order' => 'ASC', 'title' => 'ASC' ), 'no_found_rows' => true, ) ); $members = array(); foreach ( $all as $m ) { if ( '0' !== (string) get_post_meta( $m->ID, 'clanfront_active', true ) ) { $members[] = $m; } } return ( (int) $o['limit'] > 0 ) ? array_slice( $members, 0, (int) $o['limit'] ) : $members; } /** A member's own accent colour (#rrggbb), or '' to use the clan's colour. */ function clanfront_member_color( $member ) { $c = (string) get_post_meta( $member->ID, 'clanfront_color', true ); return preg_match( '/^#[0-9a-fA-F]{6}$/', $c ) ? $c : ''; } function clanfront_member_role( $member ) { return (string) get_post_meta( $member->ID, 'clanfront_role', true ); } /** The member's links as a list of array( label, url ). */ function clanfront_member_links( $member ) { $links = array(); $login = clanfront_twitch_login( get_post_meta( $member->ID, 'clanfront_twitch', true ) ); if ( $login ) { $links[] = array( 'Twitch', 'https://www.twitch.tv/' . $login, clanfront_status_tag( $login, clanfront_live_streams() ) ); } $map = array( 'clanfront_youtube' => 'YouTube', 'clanfront_x' => 'X', 'clanfront_instagram' => 'Instagram', 'clanfront_stats_url' => 'Stats', ); foreach ( $map as $key => $label ) { $url = clanfront_clean_url( get_post_meta( $member->ID, $key, true ) ); if ( $url ) { $links[] = array( $label, $url ); } } return $links; } function clanfront_buttons( $links, $class = 'clanfront-btn clanfront-btn--small' ) { $html = ''; foreach ( $links as $l ) { $tag = isset( $l[2] ) ? (string) $l[2] : ''; // Already escaped by clanfront_status_tag(). $html .= sprintf( '%s%s', esc_attr( $class ), esc_url( $l[1] ), esc_html( $l[0] ), $tag ? ' ' . $tag : '' ); } return $html; } /** Picture for a member: the uploaded image, or a tile with their initials. */ function clanfront_avatar( $member, $size = 'medium_large' ) { if ( has_post_thumbnail( $member ) ) { return '
' . get_the_post_thumbnail( $member, $size, array( 'alt' => get_the_title( $member ), 'loading' => 'lazy', ) ) . '
'; } $initials = function_exists( 'mb_substr' ) ? mb_strtoupper( mb_substr( get_the_title( $member ), 0, 2 ) ) : strtoupper( substr( get_the_title( $member ), 0, 2 ) ); return ''; } function clanfront_member_card( $member, $live = array() ) { $role = clanfront_member_role( $member ); $games = (string) get_post_meta( $member->ID, 'clanfront_games', true ); $login = clanfront_twitch_login( get_post_meta( $member->ID, 'clanfront_twitch', true ) ); $is_live = $login && isset( $live[ $login ] ); $color = clanfront_member_color( $member ); ob_start(); ?> > LiveOffline ' . esc_html( $empty ) . '

'; } $html = '
'; foreach ( $members as $m ) { $html .= clanfront_member_card( $m, $live ); } return $html . '
'; } /* ---------- Matches and events ---------- */ function clanfront_event_kinds() { return array( 'match' => 'Match', 'tournament' => 'Tournament', 'scrim' => 'Scrim', 'practice' => 'Practice', 'game_night' => 'Game night', 'event' => 'Event', ); } function clanfront_event_results() { return array( '' => 'Not played yet', 'win' => 'Win', 'loss' => 'Loss', 'draw' => 'Draw', ); } function clanfront_event_date( $event ) { return (string) get_post_meta( $event->ID, 'clanfront_date', true ); } function clanfront_event_time( $event ) { return (string) get_post_meta( $event->ID, 'clanfront_time', true ); } /** The start of the event as a DateTime in the site's timezone (a missing time means the start of the day). */ function clanfront_event_start( $event ) { $date = clanfront_event_date( $event ); if ( ! $date ) { return null; } $time = clanfront_event_time( $event ); try { return new DateTime( $date . ' ' . ( $time ? $time : '00:00' ), wp_timezone() ); } catch ( Exception $e ) { return null; } } /** Upcoming until the end of its day, or until its start time when it has one. */ function clanfront_is_upcoming( $event ) { $date = clanfront_event_date( $event ); if ( ! $date ) { return false; } $time = clanfront_event_time( $event ); try { $cut = new DateTime( $date . ' ' . ( $time ? $time : '23:59' ), wp_timezone() ); return $cut >= new DateTime( 'now', wp_timezone() ); } catch ( Exception $e ) { return false; } } /** * Events (matches, scrims, game nights…). $opts: when ('upcoming'|'past'|'all'), game, kind, limit. * Upcoming events come soonest first; past events newest first. */ function clanfront_get_events( $opts = array() ) { $o = wp_parse_args( $opts, array( 'when' => 'all', 'game' => '', 'kind' => '', 'limit' => -1, ) ); // A clan has at most a few hundred events, so they are all loaded and sorted by date here. $posts = get_posts( array( 'post_type' => 'clanfront_match', 'post_status' => 'publish', 'posts_per_page' => -1, 'meta_key' => 'clanfront_date', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- small, clan-sized data set. 'orderby' => 'meta_value', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- sort by event date. 'order' => 'ASC', 'no_found_rows' => true, ) ); $out = array(); foreach ( $posts as $p ) { if ( ! clanfront_event_date( $p ) ) { continue; } $upcoming = clanfront_is_upcoming( $p ); if ( 'upcoming' === $o['when'] && ! $upcoming ) { continue; } if ( 'past' === $o['when'] && $upcoming ) { continue; } if ( $o['game'] && (string) get_post_meta( $p->ID, 'clanfront_game', true ) !== $o['game'] ) { continue; } if ( $o['kind'] && (string) get_post_meta( $p->ID, 'clanfront_kind', true ) !== $o['kind'] ) { continue; } $out[] = $p; } usort( $out, function ( $a, $b ) { $ka = clanfront_event_date( $a ) . ' ' . ( clanfront_event_time( $a ) ? clanfront_event_time( $a ) : '00:00' ); $kb = clanfront_event_date( $b ) . ' ' . ( clanfront_event_time( $b ) ? clanfront_event_time( $b ) : '00:00' ); return strcmp( $ka, $kb ); } ); if ( 'past' === $o['when'] || ( 'all' === $o['when'] ) ) { // Newest first for results; "all" lists are only used for counting. $out = ( 'past' === $o['when'] ) ? array_reverse( $out ) : $out; } return ( (int) $o['limit'] > 0 ) ? array_slice( $out, 0, (int) $o['limit'] ) : $out; } /** Win/loss/draw counts for finished events that have a result. */ function clanfront_record( $game = '' ) { $rec = array( 'win' => 0, 'loss' => 0, 'draw' => 0 ); foreach ( clanfront_get_events( array( 'when' => 'past', 'game' => $game ) ) as $e ) { $r = (string) get_post_meta( $e->ID, 'clanfront_result', true ); if ( isset( $rec[ $r ] ) ) { ++$rec[ $r ]; } } $rec['played'] = $rec['win'] + $rec['loss'] + $rec['draw']; $rec['rate'] = $rec['played'] ? (int) round( 100 * $rec['win'] / $rec['played'] ) : 0; return $rec; } function clanfront_event_kind_label( $event ) { $k = (string) get_post_meta( $event->ID, 'clanfront_kind', true ); $kinds = clanfront_event_kinds(); return isset( $kinds[ $k ] ) ? $kinds[ $k ] : 'Event'; } /** "Fri, Oct 9 · 19:00" */ function clanfront_event_when( $event ) { $date = clanfront_event_date( $event ); if ( ! $date ) { return ''; } $ts = strtotime( $date . ' 12:00:00' ); $out = $ts ? date_i18n( 'D, M j, Y', $ts ) : ''; $time = clanfront_format_time( clanfront_event_time( $event ) ); return $time ? $out . ' · ' . $time : $out; } function clanfront_event_row( $event ) { $date = clanfront_event_date( $event ); $ts = strtotime( $date . ' 12:00:00' ); $game = (string) get_post_meta( $event->ID, 'clanfront_game', true ); $opp = (string) get_post_meta( $event->ID, 'clanfront_opponent', true ); $result = (string) get_post_meta( $event->ID, 'clanfront_result', true ); $score = (string) get_post_meta( $event->ID, 'clanfront_score', true ); $place = (string) get_post_meta( $event->ID, 'clanfront_placement', true ); $upcoming = clanfront_is_upcoming( $event ); $results = clanfront_event_results(); ob_start(); ?> Upcoming ' . esc_html( $empty ) . '

'; } $html = '
'; foreach ( $events as $e ) { $html .= clanfront_event_row( $e ); } return $html . '
'; } /* ---------- Gallery ---------- */ function clanfront_get_media( $limit = -1 ) { return get_posts( array( 'post_type' => 'clanfront_media', 'post_status' => 'publish', 'posts_per_page' => (int) $limit > 0 ? (int) $limit : -1, 'orderby' => array( 'menu_order' => 'ASC', 'date' => 'DESC' ), 'no_found_rows' => true, ) ); } function clanfront_media_grid( $items, $empty = 'No media yet.' ) { if ( ! $items ) { return '

' . esc_html( $empty ) . '

'; } $html = ''; } /* ---------- Servers ---------- */ function clanfront_get_servers() { return get_posts( array( 'post_type' => 'clanfront_server', 'post_status' => 'publish', 'posts_per_page' => -1, 'orderby' => array( 'menu_order' => 'ASC', 'title' => 'ASC' ), 'no_found_rows' => true, ) ); } function clanfront_server_cards( $servers ) { if ( ! $servers ) { return '

No game servers listed yet.

'; } $html = '
'; foreach ( $servers as $s ) { $address = (string) get_post_meta( $s->ID, 'clanfront_address', true ); $game = (string) get_post_meta( $s->ID, 'clanfront_game', true ); $note = (string) get_post_meta( $s->ID, 'clanfront_note', true ); ob_start(); ?>

'; } /* ---------- Markup ---------- */ function clanfront_part( $name, $vars = array() ) { $file = CLANFRONT_DIR . 'templates/parts/' . $name . '.php'; if ( file_exists( $file ) ) { extract( $vars, EXTR_SKIP ); // phpcs:ignore WordPress.PHP.DontExtract include $file; } } function clanfront_page_url( $path ) { $p = get_page_by_path( $path ); return $p ? get_permalink( $p ) : home_url( '/' . $path . '/' ); } /** The "Join us" address: the page with the application form, or the external form when one is set. */ function clanfront_join_url() { return clanfront_page_url( 'join' ); } /** Discord invite link, only when it is a Discord address. */ function clanfront_discord_invite() { $u = clanfront_clean_url( clanfront_setting( 'discord_invite' ) ); return $u && preg_match( '#^https://(www\.)?(discord\.gg|discord\.com|discordapp\.com)/#i', $u ) ? $u : ''; } function clanfront_discord_server_id() { $id = (string) clanfront_setting( 'discord_server_id' ); return preg_match( '/^\d{15,25}$/', $id ) ? $id : ''; } /** The Discord box: a "Join" button and, with a server ID, the live widget. */ function clanfront_discord_block() { $invite = clanfront_discord_invite(); $id = clanfront_discord_server_id(); if ( ! $invite && ! $id ) { return ''; } ob_start(); ?>

Community

Hang out with us on Discord

See who is online, find a team and join the conversation.

'clanfront_news', 'post_status' => 'publish', 'posts_per_page' => (int) $limit, 'orderby' => 'date', 'order' => 'DESC', 'no_found_rows' => true, ) ); } function clanfront_has_news() { return (bool) clanfront_get_news( 1 ); } /** A short text for the article card: the excerpt, or the first words of the article. */ function clanfront_news_summary( $article ) { $text = has_excerpt( $article ) ? $article->post_excerpt : wp_strip_all_tags( strip_shortcodes( $article->post_content ) ); return wp_trim_words( $text, 28 ); } function clanfront_news_cards( $articles, $empty = '' ) { if ( ! $articles ) { return $empty ? '

' . esc_html( $empty ) . '

' : ''; } ob_start(); echo '
'; foreach ( $articles as $article ) { ?> '', 'loading' => 'lazy' ) ) ); ?> '; return ob_get_clean(); }