array( label, type, required, max length ). */ function clanfront_application_fields() { return array( 'handle' => array( 'In-game name', 'text', true, 60 ), 'age' => array( 'Age', 'number', false, 3 ), 'region' => array( 'Region or time zone', 'text', false, 60 ), 'game' => array( 'Main game', 'game', false, 80 ), 'rank' => array( 'Rank or skill level', 'text', false, 80 ), 'experience' => array( 'Experience in clans or teams', 'textarea', false, 600 ), 'contact' => array( 'Discord name or email', 'text', true, 120 ), 'why' => array( 'Why do you want to join us?', 'textarea', true, 1200 ), ); } /* ---------- Tokens and limits ---------- */ function clanfront_form_token() { $t = (string) time(); return $t . '.' . hash_hmac( 'sha256', $t, wp_salt( 'auth' ) ); } function clanfront_form_token_ok( $token ) { $parts = explode( '.', (string) $token ); if ( 2 !== count( $parts ) || ! ctype_digit( $parts[0] ) ) { return false; } if ( ! hash_equals( hash_hmac( 'sha256', $parts[0], wp_salt( 'auth' ) ), $parts[1] ) ) { return false; } $age = time() - (int) $parts[0]; return $age >= 4 && $age <= 7 * DAY_IN_SECONDS; } /** Count one more attempt for this visitor in the last hour. Returns false when over the limit. */ function clanfront_rate_ok() { $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; $keys = array( 'clanfront_rl_' . md5( $ip . wp_salt( 'auth' ) ) => 3, 'clanfront_rl_all' => 30, ); foreach ( $keys as $key => $max ) { $n = (int) get_transient( $key ); if ( $n >= $max ) { return false; } } foreach ( array_keys( $keys ) as $key ) { set_transient( $key, (int) get_transient( $key ) + 1, HOUR_IN_SECONDS ); } return true; } function clanfront_apply_messages() { return array( 'ok' => 'Thanks! Your application is in. We will get back to you.', 'missing' => 'Please fill in the required fields.', 'age' => 'Sorry, you need to meet the minimum age to apply.', 'links' => 'Please leave links out of your answers.', 'rules' => 'Please confirm that you have read the rules.', 'spam' => 'That did not go through. Please wait a few seconds and try again.', 'expired' => 'The form had been open too long. Please reload the page and send it again.', 'limit' => 'Too many applications from your connection. Please try again later.', 'closed' => 'We are not taking applications right now.', 'failed' => 'Something went wrong saving your application. Please try again, or contact us directly.', ); } /* ---------- Form ---------- */ function clanfront_application_form( $old = array() ) { $fields = clanfront_application_fields(); $games = clanfront_game_names(); $min = (int) clanfront_setting( 'min_age' ); ob_start(); ?>
$code ); if ( 'ok' !== $code && $old ) { $id = wp_generate_password( 12, false ); set_transient( 'clanfront_old_' . $id, $old, 10 * MINUTE_IN_SECONDS ); $args['clanfront_old'] = $id; } wp_safe_redirect( add_query_arg( $args, clanfront_page_url( 'join' ) ) . '#apply' ); exit; } function clanfront_handle_application() { // Visitors don't need to be logged in to apply. The nonce, signed timestamp, hidden field and limits below protect the form. if ( ! isset( $_POST['clanfront_apply_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['clanfront_apply_nonce'] ) ), 'clanfront_apply' ) ) { clanfront_apply_back( 'expired' ); } $post = map_deep( wp_unslash( $_POST ), 'sanitize_textarea_field' ); if ( '1' !== (string) clanfront_setting( 'recruiting' ) || clanfront_clean_url( clanfront_setting( 'application_url' ) ) ) { clanfront_apply_back( 'closed' ); } if ( ! empty( $post['clanfront_website'] ) || ! clanfront_form_token_ok( isset( $post['clanfront_token'] ) ? $post['clanfront_token'] : '' ) ) { clanfront_apply_back( 'spam' ); } $clean = array(); foreach ( clanfront_application_fields() as $key => $f ) { $raw = isset( $post[ 'clanfront_' . $key ] ) && is_scalar( $post[ 'clanfront_' . $key ] ) ? (string) $post[ 'clanfront_' . $key ] : ''; $value = ( 'textarea' === $f[1] ) ? sanitize_textarea_field( $raw ) : sanitize_text_field( $raw ); if ( 'age' === $key ) { $value = $value ? (string) absint( $value ) : ''; } $clean[ $key ] = function_exists( 'mb_substr' ) ? mb_substr( $value, 0, $f[3] ) : substr( $value, 0, $f[3] ); } $min = (int) clanfront_setting( 'min_age' ); foreach ( clanfront_application_fields() as $key => $f ) { if ( ( $f[2] || ( 'age' === $key && $min > 0 ) ) && '' === $clean[ $key ] ) { clanfront_apply_back( 'missing', $clean ); } } if ( empty( $post['clanfront_rules'] ) ) { clanfront_apply_back( 'rules', $clean ); } if ( $min > 0 && (int) $clean['age'] < $min ) { clanfront_apply_back( 'age', array() ); // Nothing is kept for someone under the minimum age. } // Spam usually comes with links; real applicants rarely need one. if ( preg_match_all( '#https?://|www\.#i', implode( ' ', array( $clean['experience'], $clean['why'], $clean['handle'], $clean['rank'], $clean['region'] ) ) ) > 1 ) { clanfront_apply_back( 'links', $clean ); } if ( ! clanfront_rate_ok() ) { clanfront_apply_back( 'limit', $clean ); } $id = wp_insert_post( array( 'post_type' => 'clanfront_applicant', 'post_status' => 'publish', 'post_title' => $clean['handle'], ), true ); if ( is_wp_error( $id ) || ! $id ) { clanfront_apply_back( 'failed', $clean ); } foreach ( $clean as $key => $value ) { update_post_meta( $id, 'clanfront_app_' . $key, $value ); } update_post_meta( $id, 'clanfront_app_email', is_email( $clean['contact'] ) ? sanitize_email( $clean['contact'] ) : '' ); update_post_meta( $id, 'clanfront_app_status', 'new' ); $to = sanitize_email( clanfront_setting( 'application_email' ) ); if ( $to ) { $lines = array( 'New application to join ' . clanfront_name() . ':', '' ); foreach ( clanfront_application_fields() as $key => $f ) { $lines[] = $f[0] . ': ' . $clean[ $key ]; } $lines[] = ''; $lines[] = 'Review it here: ' . admin_url( 'post.php?post=' . (int) $id . '&action=edit' ); wp_mail( $to, '[' . wp_specialchars_decode( clanfront_name(), ENT_QUOTES ) . '] New application from ' . $clean['handle'], implode( "\n", $lines ) ); } clanfront_apply_back( 'ok' ); } add_action( 'admin_post_nopriv_clanfront_apply', 'clanfront_handle_application' ); add_action( 'admin_post_clanfront_apply', 'clanfront_handle_application' ); /* ---------- Privacy: export and erase by email address (Tools → Export / Erase Personal Data) ---------- */ function clanfront_applications_by_email( $email ) { return get_posts( array( 'post_type' => 'clanfront_applicant', 'post_status' => 'any', 'posts_per_page' => -1, 'meta_key' => 'clanfront_app_email', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- looked up only when exporting or erasing personal data. 'meta_value' => sanitize_email( $email ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- same. 'no_found_rows' => true, ) ); } add_filter( 'wp_privacy_personal_data_exporters', function ( $exporters ) { $exporters['clanfront'] = array( 'exporter_friendly_name' => 'Clanfront applications', 'callback' => function ( $email ) { $items = array(); foreach ( clanfront_applications_by_email( $email ) as $app ) { $data = array(); foreach ( clanfront_application_fields() as $key => $f ) { $data[] = array( 'name' => $f[0], 'value' => (string) get_post_meta( $app->ID, 'clanfront_app_' . $key, true ) ); } $items[] = array( 'group_id' => 'clanfront-applications', 'group_label' => 'Clan applications', 'item_id' => 'clanfront-application-' . $app->ID, 'data' => $data, ); } return array( 'data' => $items, 'done' => true ); }, ); return $exporters; } ); add_filter( 'wp_privacy_personal_data_erasers', function ( $erasers ) { $erasers['clanfront'] = array( 'eraser_friendly_name' => 'Clanfront applications', 'callback' => function ( $email ) { $removed = false; foreach ( clanfront_applications_by_email( $email ) as $app ) { wp_delete_post( $app->ID, true ); $removed = true; } return array( 'items_removed' => $removed, 'items_retained' => false, 'messages' => array(), 'done' => true ); }, ); return $erasers; } );