..., 'message' => ...] to show/block with, or null * if nothing blocks this visitor. `type` lets a caller render a * restriction-specific icon/tone/settings-hint (see Shortcode::render()) * without duplicating the checks above it — the message alone doesn't * say which kind of restriction it is. */ public static function check( array $form ) { $s = $form['settings'] ?: []; if ( ! empty( $s['require_login'] ) && ! is_user_logged_in() ) { return [ 'type' => 'login', 'message' => $s['require_login_message'] ?? __( 'You must be logged in to submit this form.', 'chargeforms' ), ]; } if ( ! empty( $s['restrict_ip_enabled'] ) && self::ip_is_listed( self::client_ip(), $s['restrict_ip_list'] ?? '' ) ) { return [ 'type' => 'restricted_ip', 'message' => $s['restrict_ip_message'] ?? __( 'Submissions from your network are not accepted on this form.', 'chargeforms' ), ]; } if ( ! empty( $s['schedule_enabled'] ) ) { $now = current_time( 'timestamp' ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp if ( ! empty( $s['schedule_start'] ) && $now < strtotime( $s['schedule_start'] ) ) { return [ 'type' => 'schedule_pending', 'message' => $s['schedule_pending_message'] ?? __( 'This form is not open yet.', 'chargeforms' ), ]; } if ( ! empty( $s['schedule_end'] ) && $now > strtotime( $s['schedule_end'] ) ) { return [ 'type' => 'schedule_expired', 'message' => $s['schedule_expired_message'] ?? __( 'This form is no longer accepting submissions.', 'chargeforms' ), ]; } } if ( ! empty( $s['limit_entries_enabled'] ) ) { $max = (int) ( $s['limit_entries_count'] ?? 0 ); if ( $max > 0 && Entry::count_for_form( $form['id'], true ) >= $max ) { return [ 'type' => 'limit_entries', 'message' => $s['limit_entries_message'] ?? __( 'This form is no longer accepting submissions — the entry limit has been reached.', 'chargeforms' ), ]; } } return null; } /** True if every submitted value is empty — used for the "deny empty submission" setting. */ public static function is_entirely_empty( array $values ) { foreach ( $values as $value ) { if ( is_array( $value ) ? ! empty( $value ) : '' !== trim( (string) $value ) ) { return false; } } return true; } /** * Keyword-based restriction — checked separately from check() because it * needs the posted field values, which don't exist yet at render time; * Submission_Handler calls this itself, right after the (value-free) * check() above and the deny-empty-submission check. A submission is * blocked when ANY submitted value contains ANY listed keyword, * case-insensitively — the same all-values/any-keyword shape the reference product * Forms' own keyword restriction uses, matching a spammer's payload * wherever it landed rather than requiring it in one specific field. * * @return string|null The configured block message, or null if nothing matched. */ public static function check_content( array $form, array $values ) { $s = $form['settings'] ?: []; if ( empty( $s['restrict_keyword_enabled'] ) || empty( $s['restrict_keyword_list'] ) ) { return null; } $keywords = self::split_list( $s['restrict_keyword_list'] ); if ( ! $keywords ) { return null; } $haystack = self::flatten_values_to_text( $values ); foreach ( $keywords as $keyword ) { if ( '' !== $keyword && false !== stripos( $haystack, $keyword ) ) { return $s['restrict_keyword_message'] ?? __( 'Your submission could not be accepted.', 'chargeforms' ); } } return null; } /** * "Advanced Form Validation" (the reference product parity) — a form-wide rule * group (All/Any of up to N field conditions) checked at submit time, * separate from check_content() above because it needs a full * All/Any-combined group evaluation rather than a flat "any keyword * anywhere" scan. Built as the exact 'group' LogicNode shape * (Includes\Logic\Evaluator) field-level Conditional Logic already * uses — combinator/not/children — so this is one more caller of that * one engine, not a second condition-matching implementation. * * @return string|null The configured error message, or null if the submission should proceed. */ public static function check_validation( array $form, array $values ) { $s = $form['settings'] ?: []; if ( empty( $s['validation_enabled'] ) || empty( $s['validation_rules'] ) ) { return null; } $children = []; foreach ( $s['validation_rules'] as $rule ) { if ( empty( $rule['field_id'] ) ) { continue; } $children[] = [ 'type' => 'condition', 'source' => 'field', 'ref' => $rule['field_id'], 'operator' => $rule['operator'] ?? 'is', 'value' => $rule['value'] ?? '', ]; } if ( ! $children ) { return null; } // `validation_invert` mirrors the reference product's own "Fail the submission if // conditions met" (block when the group is true) vs "Let submit if // conditions are met" (block when the group is FALSE) — the second // is just the first with `not` on the outer group, so the "should // this submission be blocked" question is always simply "is the // (possibly negated) group true". $group = [ 'type' => 'group', 'combinator' => 'any' === ( $s['validation_match'] ?? 'all' ) ? 'OR' : 'AND', 'not' => ! empty( $s['validation_invert'] ), 'children' => $children, ]; $blocked = Evaluator::evaluate_logic_node( $group, [ 'field_values' => $values ] ); return $blocked ? ( $s['validation_message'] ?? __( 'Your submission could not be accepted.', 'chargeforms' ) ) : null; } /** One value/keyword per line (also tolerates comma-separated) — matches the textarea UI on the Restrictions tab, trimmed and emptied of blank lines. */ private static function split_list( $raw ) { $parts = preg_split( '/[\r\n,]+/', (string) $raw ); return array_values( array_filter( array_map( 'trim', $parts ), 'strlen' ) ); } /** Every submitted value (including nested array values, e.g. a Checkbox field) joined into one lowercase string to scan once rather than per-field. */ private static function flatten_values_to_text( array $values ) { $flat = []; array_walk_recursive( $values, function ( $value ) use ( &$flat ) { $flat[] = (string) $value; } ); return strtolower( implode( ' ', $flat ) ); } /** Exact-match against a newline/comma-separated IP list — deliberately simple (no CIDR ranges), matching the scope of the reference product's own "IP Based Restriction" field. */ private static function ip_is_listed( $ip, $raw_list ) { if ( '' === $ip ) { return false; } return in_array( $ip, self::split_list( $raw_list ), true ); } /** Same $_SERVER['REMOTE_ADDR'] source Entry/Draft/Form_Start already use for IP logging — not trusting X-Forwarded-For here, since that header is client-controllable and this value gates access, not just an audit log. */ private static function client_ip() { return isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; } }