value) and fire `careback:bot-check-reset` on the box after every * answer. '' (no add-on): no box. */ const MARKUP_FILTER = 'careback_bot_check_markup'; /** * Filter: an add-on's verdict on a lookup of either form ( true|\WP_Error * $verdict, array $fields, string $form, string $remote_ip, Settings * $settings ), $fields the `bot_check` values the browser sent. A \WP_Error * refuses the lookup: logged as `bot_check_failed`, counted as a failure, * and on the returns form its message is the answer. True (no add-on) lets * it through. */ const VERIFY_FILTER = 'careback_bot_check_verify'; private $settings; public function __construct( Settings $settings ) { $this->settings = $settings; } /* --------------------------------------------------------------- * * Signed time token * --------------------------------------------------------------- */ /** A signed "form rendered at" token handed to the browser. */ public function time_token() { $ts = time(); $sig = Hash::hmac( 'careback-time:' . $ts ); return $ts . ':' . $sig; } /** Verdicts of the timing check. */ const TIME_OK = 'ok'; const TIME_BAD = 'bad'; const TIME_FAST = 'fast'; const TIME_STALE = 'stale'; /** * What the render-time token says about a submission. * * Three failures, and they do not mean the same thing. A forged or * unreadable token, and a submit that arrives faster than a person can * read the form, are both automated software. A token that is merely old * is not: the commonest cause is a page cache handing the same rendered * HTML — token and all — to every visitor for hours. Treating that as a * bot was the plugin's worst failure mode, because the customer was * refused with the same wording as a wrong order number, and the refusal * counted towards the auto-block, so trying again made it worse. * * Since 0.66.0 the script asks for a fresh token when the portal loads, * which takes the cache out of the picture entirely; this verdict is the * safety net for the browser that could not. * * @param string $token Token as submitted. * @return string One of the TIME_* constants. */ public function time_token_verdict( $token ) { if ( empty( $this->settings->get( 'enable_time_token' ) ) ) { return self::TIME_OK; } $parts = explode( ':', (string) $token, 2 ); if ( 2 !== count( $parts ) ) { return self::TIME_BAD; } $ts = (int) $parts[0]; $expected = Hash::hmac( 'careback-time:' . $ts ); if ( ! Hash::equals( $expected, $parts[1] ) ) { return self::TIME_BAD; } $age = time() - $ts; $min = (int) $this->settings->get( 'min_submit_seconds', 2 ); $max = (int) $this->settings->get( 'max_submit_seconds', 86400 ); // A token from the future is a clock that moved, not a customer. Read // as fast rather than as stale, which is the cautious side. if ( $age < $min ) { return self::TIME_FAST; } // Zero switches the ceiling off, for a shop that would rather never // refuse an old form than catch a replayed one. if ( $max > 0 && $age > $max ) { return self::TIME_STALE; } return self::TIME_OK; } /** * True when the token is authentic and the submit is neither implausibly * fast nor stale. When the check is disabled, everything passes. * * Kept as the plain yes/no reading of the verdict above. */ public function time_token_ok( $token ) { return self::TIME_OK === $this->time_token_verdict( $token ); } /* --------------------------------------------------------------- * * An add-on's bot check (optional) * --------------------------------------------------------------- */ /** * The markup an add-on's bot check puts into a form (MARKUP_FILTER), boxed * and through wp_kses_post(); '' when none does. * * @param string $form 'portal' or 'track'. * @return string HTML. */ public function bot_check_markup( $form ) { $html = apply_filters( self::MARKUP_FILTER, '', (string) $form, $this->settings ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- careback_bot_check_markup, the final prefix. $html = is_string( $html ) ? trim( $html ) : ''; return '' === $html ? '' : '