= 7 ) { return true; } } return false; } /** * Junk / meaningless text such as "dfddffdf fdf dffnjjn jnjn". * * Latin-alphabet words without any vowel count as junk; when half or more * of the words look like that, the text is refused. * * @param string $text Text. * @return bool */ public static function is_junk( $text ) { preg_match_all( '/[a-z]{3,}/i', $text, $m ); $words = $m[0]; if ( ! $words ) { return false; } $junk = 0; foreach ( $words as $word ) { $word = strtolower( $word ); $vowels = preg_match_all( '/[aeiouy]/', $word ); $len = strlen( $word ); if ( 0 === $vowels || ( $len >= 7 && $vowels / $len < 0.15 ) || preg_match( '/[bcdfghjklmnpqrstvwxz]{5,}/', $word ) ) { ++$junk; } } return $junk >= 1 && $junk / count( $words ) >= 0.5; } /** * Count real words (two or more letters, any language). * * @param string $text Text. * @return int */ public static function count_words( $text ) { $count = preg_match_all( '/[\p{L}\p{M}]{2,}/u', $text ); // Languages written without spaces (Chinese, Japanese, Thai): count characters as words. $cjk = preg_match_all( '/[\p{Han}\p{Hiragana}\p{Katakana}\p{Thai}]/u', $text ); return max( (int) $count, (int) $cjk ); } /** * Visitor-facing messages. * * @param string $rule Rule. * @return string */ public static function message( $rule ) { switch ( $rule ) { case 'links': return __( 'Links and website addresses are not allowed here.', 'siteaudithub' ); case 'emails': return __( 'Email addresses are not allowed here.', 'siteaudithub' ); case 'phones': return __( 'Phone numbers are not allowed here.', 'siteaudithub' ); case 'repeat': return __( 'Please remove repeated characters.', 'siteaudithub' ); case 'junk': return __( 'Please write a meaningful message.', 'siteaudithub' ); case 'words': /* translators: %d: minimum number of words */ return sprintf( __( 'Please write at least %d words.', 'siteaudithub' ), self::MIN_WORDS ); } return __( 'This message was blocked as spam.', 'siteaudithub' ); } /** * Count a blocked message. */ private static function blocked() { update_option( self::COUNTER, (int) get_option( self::COUNTER, 0 ) + 1, false ); } /** * Number of messages blocked so far. * * @return int */ public static function blocked_count() { return (int) get_option( self::COUNTER, 0 ); } /** * Check, count and return the error for a submitted value. * * @param mixed $value Value. * @return string */ private static function verdict( $value ) { if ( ! is_string( $value ) ) { return ''; } $error = self::check( $value ); if ( '' !== $error ) { self::blocked(); } return $error; } /* ------------------------------------------------------------------ */ /* Integrations */ /* ------------------------------------------------------------------ */ /** * WordPress comments (also covers comments sent through the REST API). * * @param int|string|\WP_Error $approved Approval status. * @param array $commentdata Comment data. * @return int|string|\WP_Error */ public static function check_comment( $approved, $commentdata ) { if ( is_wp_error( $approved ) || 'spam' === $approved || 'trash' === $approved ) { return $approved; } if ( ! empty( $commentdata['user_id'] ) && user_can( (int) $commentdata['user_id'], 'moderate_comments' ) ) { return $approved; // Site moderators are never blocked. } if ( isset( $commentdata['comment_type'] ) && in_array( $commentdata['comment_type'], array( 'pingback', 'trackback' ), true ) ) { return $approved; } $error = self::verdict( isset( $commentdata['comment_content'] ) ? $commentdata['comment_content'] : '' ); return '' === $error ? $approved : new \WP_Error( 'siteaudithub_comment_blocked', $error, 400 ); } /** * Elementor Pro forms: textarea fields. * * @param object $record Form record. * @param object $ajax_handler Ajax handler. */ public static function check_elementor( $record, $ajax_handler ) { if ( ! is_object( $record ) || ! is_object( $ajax_handler ) || ! method_exists( $record, 'get' ) ) { return; } foreach ( (array) $record->get( 'fields' ) as $id => $field ) { if ( ! isset( $field['type'], $field['value'] ) || 'textarea' !== $field['type'] ) { continue; } $error = self::verdict( $field['value'] ); if ( '' !== $error ) { $ajax_handler->add_error( $id, $error ); } } } /** * Contact Form 7 textarea. * * @param object $result Validation result. * @param object $tag Form tag. * @return object */ public static function check_cf7( $result, $tag ) { $name = is_object( $tag ) && isset( $tag->name ) ? $tag->name : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Contact Form 7 verifies the submission before validation runs. $value = ( $name && isset( $_POST[ $name ] ) && is_string( $_POST[ $name ] ) ) ? sanitize_textarea_field( wp_unslash( $_POST[ $name ] ) ) : ''; $error = self::verdict( $value ); if ( '' !== $error && is_object( $result ) && method_exists( $result, 'invalidate' ) ) { $result->invalidate( $tag, $error ); } return $result; } /** * WPForms paragraph text. * * @param int $field_id Field ID. * @param mixed $field_submit Submitted value. * @param array $form_data Form data. */ public static function check_wpforms_textarea( $field_id, $field_submit, $form_data ) { self::wpforms_error( $field_id, $form_data, self::verdict( $field_submit ) ); } /** * Record a WPForms field error. * * @param int $field_id Field ID. * @param array $form_data Form data. * @param string $error Error. */ private static function wpforms_error( $field_id, $form_data, $error ) { if ( '' !== $error && function_exists( 'wpforms' ) && isset( $form_data['id'] ) ) { wpforms()->process->errors[ $form_data['id'] ][ $field_id ] = $error; } } /** * Gravity Forms paragraph text. * * @param array $result Validation result. * @param mixed $value Value. * @param array $form Form. * @param object $field Field. * @return array */ public static function check_gravity( $result, $value, $form, $field ) { if ( ! is_array( $result ) || empty( $result['is_valid'] ) || ! is_object( $field ) || ! isset( $field->type ) || 'textarea' !== $field->type ) { return $result; } $error = self::verdict( $value ); if ( '' !== $error ) { $result['is_valid'] = false; $result['message'] = $error; } return $result; } /** * Fluent Forms text area. * * @param mixed $error Existing error. * @param array $field Field. * @param array $form_data Submitted data. * @return mixed */ public static function check_fluent_textarea( $error, $field, $form_data ) { return self::fluent( $error, $field, $form_data ); } /** * Shared Fluent Forms check. * * @param mixed $error Existing error. * @param array $field Field. * @param array $form_data Submitted data. * @return mixed */ private static function fluent( $error, $field, $form_data ) { if ( $error ) { return $error; } $name = ''; if ( isset( $field['name'] ) ) { $name = $field['name']; } elseif ( isset( $field['attributes']['name'] ) ) { $name = $field['attributes']['name']; } elseif ( isset( $field['raw']['attributes']['name'] ) ) { $name = $field['raw']['attributes']['name']; } $message = ( $name && isset( $form_data[ $name ] ) ) ? self::verdict( $form_data[ $name ] ) : ''; return '' === $message ? $error : array( $message ); } /** * Ninja Forms paragraph text. * * @param array $form_data Submission. * @return array */ public static function check_ninja( $form_data ) { if ( empty( $form_data['fields'] ) || ! is_array( $form_data['fields'] ) || ! function_exists( 'Ninja_Forms' ) ) { return $form_data; } foreach ( $form_data['fields'] as $key => $field ) { $id = isset( $field['id'] ) ? $field['id'] : $key; $model = Ninja_Forms()->form()->field( $id )->get(); if ( ! $model || 'textarea' !== $model->get_setting( 'type' ) ) { continue; } $error = self::verdict( isset( $field['value'] ) ? $field['value'] : '' ); if ( '' !== $error ) { $form_data['errors']['fields'][ $id ] = $error; } } return $form_data; } /** * Formidable Forms paragraph text. * * @param array $errors Errors. * @param object $posted_field Field. * @param mixed $value Value. * @return array */ public static function check_formidable( $errors, $posted_field, $value ) { if ( is_object( $posted_field ) && isset( $posted_field->type, $posted_field->id ) && 'textarea' === $posted_field->type ) { $error = self::verdict( $value ); if ( '' !== $error ) { $errors[ 'field' . $posted_field->id ] = $error; } } return $errors; } /** * WooCommerce checkout order notes (address and billing fields are left alone). * * @param array $data Posted checkout data. * @param \WP_Error $errors Errors. */ public static function check_woocommerce( $data, $errors ) { if ( ! empty( $data['order_comments'] ) && is_wp_error( $errors ) ) { $error = self::verdict( $data['order_comments'] ); if ( '' !== $error ) { $errors->add( 'validation', $error ); } } } /* ------------------------------------------------------------------ */ /* Browser check for every other form */ /* ------------------------------------------------------------------ */ /** * Load the small front-end checker. */ public static function browser_script() { wp_enqueue_script( 'siteaudithub-form-guard', SITEAUDITHUB_URL . 'assets/js/form-guard.js', array(), SITEAUDITHUB_VERSION, true ); wp_localize_script( 'siteaudithub-form-guard', 'siteaudithubFormGuard', array( 'minWords' => self::MIN_WORDS, 'tlds' => self::TLDS, 'msg' => array( 'links' => self::message( 'links' ), 'emails' => self::message( 'emails' ), 'phones' => self::message( 'phones' ), 'repeat' => self::message( 'repeat' ), 'junk' => self::message( 'junk' ), 'words' => self::message( 'words' ), ), ) ); } }