false, 'cap' => null, 'json' => array(), 'html' => array() ), $o, array( 'cb' => $cb ) ); } /** Request keys (at any depth) that hold passwords. Like wp-login.php, these are never altered: they are only hashed or compared. */ const PASSWORD_KEYS = array( 'pwd', 'pass1', 'pass2', 'current', 'user_pass', 'user_pass2' ); /** Field-value arrays from the form engine: HTML is limited to post HTML here, then Fields::process() sanitises each value by its field type. */ const FIELD_KEYS = array( 'frontglut_f', 'frontglut_s', 'frontglut_pv' ); /** * Build the sanitised request for a handler from $_POST. * Nonce and capability are verified by dispatch() before this runs. */ private static function request( array $h ) { $out = array(); foreach ( array_keys( $_POST ) as $key ) { // phpcs:ignore WordPress.Security.NonceVerification -- verified in dispatch(). $k = self::clean_key( $key ); if ( '' === $k || ! isset( $_POST[ $key ] ) || in_array( $k, array( 'action', 'do', '_frontglut_nonce' ), true ) ) { // phpcs:ignore WordPress.Security.NonceVerification -- verified in dispatch(). continue; } $raw = wp_unslash( $_POST[ $key ] ); // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitised just below by clean_value(). if ( in_array( $k, (array) $h['json'], true ) ) { $d = is_string( $raw ) ? json_decode( $raw, true ) : null; $out[ $k ] = is_array( $d ) ? self::clean_value( $d, 'html' ) : array(); continue; } if ( 'redirect_to' === $k ) { $out[ $k ] = is_string( $raw ) ? esc_url_raw( $raw ) : ''; continue; } $mode = in_array( $k, self::FIELD_KEYS, true ) || in_array( $k, (array) $h['html'], true ) ? 'html' : 'text'; $out[ $k ] = self::clean_value( $raw, $mode, $k ); } return $out; } /** Recursively sanitise a request value. $mode: 'text' strips all tags, 'html' keeps post HTML only. */ private static function clean_value( $v, $mode, $key = '' ) { if ( is_array( $v ) ) { $c = array(); foreach ( $v as $k => $x ) { $ck = is_int( $k ) ? $k : self::clean_key( $k ); $c[ $ck ] = self::clean_value( $x, $mode, (string) $ck ); } return $c; } if ( is_bool( $v ) || is_int( $v ) || is_float( $v ) || null === $v ) { return $v; } $v = (string) $v; if ( in_array( $key, self::PASSWORD_KEYS, true ) ) { return $v; } return 'html' === $mode ? wp_kses_post( $v ) : sanitize_textarea_field( $v ); } private static function clean_key( $k ) { return preg_replace( '/[^A-Za-z0-9_\-\.]/', '', (string) $k ); } public static function dispatch() { $name = isset( $_REQUEST['do'] ) ? sanitize_key( wp_unslash( $_REQUEST['do'] ) ) : ''; if ( ! isset( self::$handlers[ $name ] ) ) { self::fail( __( 'Unknown action.', 'frontglut' ), 404 ); } $h = self::$handlers[ $name ]; if ( ! check_ajax_referer( 'frontglut_ajax', '_frontglut_nonce', false ) ) { self::fail( __( 'Security check failed. Please reload the page.', 'frontglut' ), 403 ); } if ( ! $h['public'] && ! is_user_logged_in() ) { self::fail( __( 'Please log in to continue.', 'frontglut' ), 401 ); } if ( $h['cap'] && ! Caps::can( $h['cap'] ) ) { self::fail( __( 'You are not allowed to do that.', 'frontglut' ), 403 ); } $req = self::request( $h ); try { $out = call_user_func( $h['cb'], $req ); } catch ( \Exception $e ) { self::fail( $e->getMessage() ); } do_action( 'frontglut_ajax_done', $name, $out ); wp_send_json_success( $out ); } /** * One entry from $_FILES for the current AJAX request, or null. * dispatch() has already verified the nonce and capability; the nonce is checked again here * so an upload is never read without it. Callers sanitise each part (name, size, error). */ public static function file( $key ) { if ( ! check_ajax_referer( 'frontglut_ajax', '_frontglut_nonce', false ) ) { return null; } if ( empty( $_FILES[ $key ] ) || ! is_array( $_FILES[ $key ] ) ) { return null; } return $_FILES[ $key ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- each part is sanitised by the caller; tmp_name is a server-generated path. } public static function fail( $message, $status = 400, $extra = array() ) { wp_send_json_error( array_merge( array( 'message' => $message ), $extra ), $status ); } public static function nonce() { return wp_create_nonce( 'frontglut_ajax' ); } public static function url() { return admin_url( 'admin-ajax.php' ); } }