JFIF $ d d 7 * Z qn )ZP 0 !1 A "#a$2Qr D8 a Ri [f \m I ykI w0 c u F c Rı?lO7 к _f˓ [C $殷WF< _W ԣ s Kc ëI zyQy /_LK ℂ ;C",p F A : /] =H ~ , l s / 9 ć: [=/#f ; ) x { ٛE Q )~ = 𘙲r*2~ a _V = ' ku m F D } KYYC )( { * g& f `툪 r y ` = ^ c J. I ]( * `wq 1dđ #̩ ͑ 0 ; H ] u 搂 @:~ ו K L N s h } O I R* 8 :2 ! lD JV o ( 3 = M ( z Ȱ+i *N A r6K n S l ) ! J J Ӂ* % ݉?|D } d5: eP0 R; {$X' x F @ .ÊB {,WJ uQ ɲRI ;9 Q E琯 6 2fT .DU J ;*c P A\IL Nj! J ۱+ O\ ͔ ] ޒ S J ȧ c%A N ol ՎprU L Zԛe r E2= X D Xg VQeӓ k y P7 U* o m Q I s ,K `) 6\G3t?pgjr m ۛجw luGt f h 9uyP 0D;Uڽ"OXl i f $ ) &|ML 0 Z rm1 [ H XP l P R0 'G=i 2N +0 e 2 ]] 9 VT P O 7 h(F * 癈 ' = Q VZD F,d ߬ ~ T X G[ `le 69C R(!S2! P < 0 x < !1AQ "Raq 02Br #S CTb ? Ζ "]mH5WR 7 k.ۛ ! } Q~ + yԏ z |@ T20 S~ K e k *zF f^2X *( @ 8 r ? C I uI|֓>^Ex LgNU Y+{.R Ѫ τV YT D I 6 2 '8Y2 7'\T P.6 d &˦ @V q i |8 -O Ε ] ʔ U= T L 8= ;6c | !qfF3 a ů& ~$ l} 'NW U s$U k^S V : U#6w + +s &r+n ڐ { @ 29gL u " T Ù M=6 (^ "7 r}=6Yݾ lC u hquymp Ǧ G jhs ǜNl ɻ} o 7 #S6 a w 4 ! OS r D 5 7 % | ?x> L | / nD 6?/8 w # [ ) L 7 +6〼T A T g ! %5 Mm Z /c -{1_Je "| ^$ 'O & ޱմ < H vNqϡQ$ < 6 &Nݛ ֨tǻN (9 n ru~'όN sMG& ,uH Kw *Ī 5@ u6 BԒ i YN * f 0 _ GhKW u lT <nj- 0{ M L~ | /a v\ E = ?1 \ɟuq Ђ Y O a K ޡP q $ H8o Q˼EV ~ k 0 h' L Pp M -M" m^ !6l nմ 쮝C U T . ! :vMǨb h ډ Vc r @~p # !1QAa q ? AL0L x LU 4 d ; x " 16ad0 7.\ߋ 0 = P l v/ " c A Ay A|2pO8"d MV #1] = \ " ˡ ] k v I9 d ۓ & q ) ǐq E ꞅ v "P )q X &Q З non]я u ג./ ; gZ$ M QР 2M ]h ni 0 P $ێ 3CɏGjώy : m c # S_ l@ '_ \m|C 2`K _ 1 cIvGc $? PCQ S+D ? K aײ k Fr b 8 ύ NEدg ĝ U 3 6 #Ik+ v { v i ( ވ ? a e) X xy/*@Q Ǽ Ծ ) $ IWO l c G; !i k{ gOŀ L ('x T rb $ w) R $ & N1E td U3Uȉ1p M"N* (DNy d96.(jQ) X 5 c QɎMy W ? Q *!R > 6=7)Xj 5`J ] e 8%t ! + ' ! 1Q 5 !1 AQaq ё #2 "0BRb ? G t^ ## .l lQ T $ v , ,m 㵜5u bV =sY+ @d { N ! dn O < .- B; _w J t 6;Q Jd. Q c%p{ 1 ,sND dF H I0 Гo X ш e黅X ۢ F :)[ F G X ƹ / w _c MeD ,ʡc c . W D tA $ j@ : ) - #u c1< @ۗ9F ) KJ- hpP] _ x [ q B lb p ʖ w q " LFGd ƶ * s +ډ_Zc " ? % t[I P 6 J ]# =ɺ V v v CGs Gh1 > )6 |e y ? L ӣ m,4 G W Ui ` ] uJVo VDG< SB 6ϏQ @ TiU l y O U 0 kfV ~ ~}S Z @ *WU Ui ## ; s /[ = ! 7}" W N ] ' (L ! ~ y5 g9T̅J kbM' +s :S +B) v @ M j e C f j E 0 Y\Qn z G 1 д~ W o{T 9?` Rm yhsy3 !HAD ] m c1 ~ 2 L S u7x T ;j$ `} 4-> L# vzŏ I L S ֭ T { r jGK C ; b pU= - ` Bs K. S F w4 M q] Z dH S 0 ) tLg
<?php
namespace WPForms\Forms;
use WPForms\Helpers\Crypto;
/**
* Class Token.
*
* This token class generates tokens that are used in our Anti-Spam checking mechanism.
*
* @since 1.6.2
*/
class Token {
/**
* Initialise the actions for the Anti-spam.
*
* @since 1.6.2
*/
public function init() {
$this->hooks();
}
/**
* Register hooks.
*
* @since 1.6.2
*/
public function hooks() {
add_filter( 'wpforms_frontend_form_atts', [ $this, 'add_token_to_form_atts' ], 10, 2 );
add_filter( 'wpforms_frontend_strings', [ $this, 'add_frontend_strings' ] );
add_action( 'wp_ajax_nopriv_wpforms_get_token', [ $this, 'ajax_get_token' ] );
add_action( 'wp_ajax_wpforms_get_token', [ $this, 'ajax_get_token' ] );
}
/**
* Return a valid token.
*
* @since 1.6.2
* @since 1.7.1 Added the $form_data argument.
*
* @param mixed $current True to use current time, otherwise a timestamp string.
* @param array $form_data Form data and settings.
*
* @return string Token.
*/
public function get( $current = true, $form_data = [] ) {
// If $current was not passed, or it is true, we use the current timestamp.
// If $current was passed in as a string, we'll use that passed in timestamp.
if ( $current !== true ) {
$time = $current;
} else {
$time = time();
}
// Format the timestamp to be less exact, as we want to deal in days.
// June 19th, 2020 would get formatted as: 1906202017125.
// Day of the month, month number, year, day number of the year, week number of the year.
$token_data = gmdate( 'dmYzW', $time );
if ( ! empty( $form_data['id'] ) ) {
$token_data .= "::{$form_data['id']}";
}
// Combine our token date and our token salt, and md5 it.
return md5( $token_data . Crypto::get_secret_key() ); // NOSONAR.
}
/**
* Generate the array of valid tokens to check for. These include two days
* before the current date to account for long cache times.
*
* These two filters are available if a user wants to extend the times.
* 'wpforms_form_token_check_before_today'
* 'wpforms_form_token_check_after_today'
*
* @since 1.6.2
* @since 1.7.1 Added the $form_data argument.
*
* @param array $form_data Form data and settings.
*
* @return array Array of all valid tokens to check against.
*/
public function get_valid_tokens( $form_data = [] ) {
$current_date = time();
$valid_token_times_before = [];
$days_in_5_years = 5 * 365;
// Create an array of 5 years worth of days.
for ( $i = 1; $i <= $days_in_5_years; $i++ ) {
$valid_token_times_before[] = $i * DAY_IN_SECONDS;
}
// Create our array of times to check before today. A user with a longer
// cache time can extend this. A user with a shorter cache time can remove times.
$valid_token_times_before = apply_filters(
'wpforms_form_token_check_before_today',
$valid_token_times_before
);
// Mostly to catch edge cases like the form page loading and submitting on two different days.
// This probably won't be filtered by users too much, but they could extend it.
$valid_token_times_after = apply_filters(
'wpforms_form_token_check_after_today',
[
( 45 * MINUTE_IN_SECONDS ), // Add in 45 minutes past today to catch some midnight edge cases.
]
);
// Built up our valid tokens.
$valid_tokens = [];
// Add in all the previous times we check.
foreach ( $valid_token_times_before as $time ) {
$valid_tokens[] = $this->get( $current_date - $time, $form_data );
}
// Add in our current date.
$valid_tokens[] = $this->get( $current_date, $form_data );
// Add in the times after our check.
foreach ( $valid_token_times_after as $time ) {
$valid_tokens[] = $this->get( $current_date + $time, $form_data );
}
return $valid_tokens;
}
/**
* Check if the given token is valid or not.
*
* Tokens are valid for some period of time (see wpforms_token_validity_in_hours
* and wpforms_token_validity_in_days to extend the validation period).
* By default tokens are valid for day.
*
* @since 1.6.2
* @since 1.7.1 Added the $form_data argument.
*
* @param string $token Token to validate.
* @param array $form_data Form data and settings.
*
* @return bool Whether the token is valid or not.
*/
public function verify( string $token, array $form_data = [] ): bool {
// Check to see if our token is inside the valid tokens.
return in_array( $token, $this->get_valid_tokens( $form_data ), true );
}
/**
* Add the token to the form attributes.
*
* @since 1.6.2
* @since 1.7.1 Added the $form_data argument.
*
* @param array $attrs Form attributes.
* @param array $form_data Form data and settings.
*
* @return array Form attributes.
*/
public function add_token_to_form_atts( array $attrs, array $form_data ) {
$attrs['atts']['data-token'] = $this->get( true, $form_data );
$attrs['atts']['data-token-time'] = time();
return $attrs;
}
/**
* Get the signed render time token.
*
* The token carries the server-side render timestamp along with its signature,
* so the elapsed submission time cannot be forged on the client.
*
* @since 2.0.2
*
* @param array $form_data Form data and settings.
*
* @return string Token in the `{time}.{signature}` format.
*/
public function get_time_token( array $form_data ): string {
$time = time();
$signature = $this->sign_time( $time, absint( $form_data['id'] ?? 0 ) );
return "{$time}.{$signature}";
}
/**
* Verify the signed render time token and return the render timestamp it carries.
*
* @since 2.0.2
*
* @param string $token Token to verify.
* @param int $form_id Form ID the token has to be bound to.
*
* The token has no maximum age, and that is a deliberate limit on what this check can do. A page
* cache serves the markup it stored, so the render time inside it is the cache fill time rather
* than the moment this visitor received the form, and the minimum time to submit check therefore
* has no effect on a cached page older than its own duration. Expiring the token instead would
* reject legitimate entries it cannot tell apart: the server sees the same old signed timestamp
* whether the visitor has been reading the page or the markup came out of a cache. Telling those
* two apart needs proof that the same client has been present since the render, which a single
* stateless timestamp cannot carry.
*
* @return int Render timestamp, or 0 when the token carries no usable one: missing, malformed,
* wrongly signed, or dated ahead of the server clock.
*/
public function verify_time_token( string $token, int $form_id ): int {
$parts = explode( '.', $token );
if ( count( $parts ) !== 2 ) {
return 0;
}
[ $time, $signature ] = $parts;
if ( ! ctype_digit( $time ) ) {
return 0;
}
$render_time = (int) $time;
if ( ! hash_equals( $this->sign_time( $render_time, $form_id ), $signature ) ) {
return 0;
}
// Only this site signs these tokens and it only ever signs the present, so a render time
// ahead of the clock means the clock moved backwards. Report no render time rather than let
// the caller subtract into a negative elapsed time it cannot tell apart from a fast submission.
if ( $render_time > time() ) {
return 0;
}
return $render_time;
}
/**
* Sign the render timestamp for the given form.
*
* An HMAC is used rather than `Crypto::encrypt()`: it is deterministic, short enough for an HTML
* attribute, and can be compared in constant time.
*
* @since 2.0.2
*
* @param int $time Render timestamp.
* @param int $form_id Form ID.
*
* @return string Signature.
*/
private function sign_time( int $time, int $form_id ): string {
return hash_hmac( 'sha256', "{$time}|{$form_id}", Crypto::get_secret_key() );
}
/**
* Validate Anti-spam if enabled.
*
* @since 1.6.2
*
* @param array $form_data Form data.
* @param array $fields Fields.
* @param array $entry Form entry.
*
* @return bool|string True or a string with the error.
*/
public function validate( array $form_data, array $fields, array $entry ) {
// Bail out if we don't have the antispam setting.
if ( empty( $form_data['settings']['antispam'] ) ) {
return true;
}
// Bail out if the antispam setting isn't enabled.
if ( $form_data['settings']['antispam'] !== '1' ) {
return true;
}
$is_valid_token = isset( $entry['token'] ) && $this->verify( (string) $entry['token'], $form_data );
if ( $this->process_antispam_filter_wrapper( $is_valid_token, $fields, $entry, $form_data ) ) {
return true;
}
// Prepare the log data.
$form_title = $form_data['settings']['form_title'] ?? '';
$form_id = $form_data['id'] ?? 'unknown';
if ( $is_valid_token ) {
// Token is OK, but antispam filter is not passed.
$log_message = 'Filter is not passed';
$error_message = $this->get_antispam_filter_message();
} else {
// Invalid token.
$log_message = 'Token is invalid';
$error_message = $this->get_invalid_token_message();
}
wpforms_log(
'Antispam: ' . $log_message,
[
'message' => $error_message,
'referer' => esc_url_raw( (string) wp_get_referer() ),
'form' => ! empty( $form_title ) ? $form_title . ' (ID: ' . $form_id . ')' : 'ID: ' . $form_id,
'token' => $entry['token'] ?? '',
'user_ip' => wpforms_get_ip(),
'entry_data' => ! wpforms_setting( 'gdpr' ) ? $entry : 'Not logged',
],
[
'type' => [ 'spam', 'error' ],
'form_id' => $form_data['id'],
'force' => true,
]
);
return $error_message;
}
/**
* Helper to run our filter on all the responses for the antispam checks.
*
* @since 1.6.2
*
* @param bool $is_valid_not_spam Is valid entry or not.
* @param array $fields Form Fields.
* @param array $entry Form entry.
* @param array $form_data Form Data.
*
* @return bool Is valid or not.
*/
public function process_antispam_filter_wrapper( bool $is_valid_not_spam, array $fields, array $entry, array $form_data ): bool {
/**
* Allows developers to filter the antispam check result.
*
* @since 1.6.2
*
* @param bool $is_valid_not_spam True if entry valid, false otherwise.
* @param array $fields Fields data.
* @param array $entry Entry data.
* @param array $form_data Form data.
*/
return (bool) apply_filters( 'wpforms_process_antispam', $is_valid_not_spam, $fields, $entry, $form_data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
}
/**
* Helper to get the invalid token message.
*
* @since 1.6.2.1
*
* @return string Invalid token message.
*/
private function get_invalid_token_message(): string {
return $this->get_error_message( esc_html__( 'Antispam token is invalid.', 'wpforms-lite' ) );
}
/**
* Helper to get the antispam filter error message.
*
* @since 1.8.9
*
* @return string Missing token message.
*/
private function get_antispam_filter_message(): string {
return $this->get_error_message( esc_html__( 'Antispam filter did not allow your data to pass through.', 'wpforms-lite' ) );
}
/**
* Get error message depends on user.
*
* @since 1.6.4.1
*
* @param string $text Message text.
*
* @return string
*/
private function get_error_message( string $text ): string {
$text .= ' ' . esc_html__( 'Please reload the page and try submitting the form again.', 'wpforms-lite' );
return wpforms_current_user_can() ? $text . $this->maybe_get_support_text() : $text;
}
/**
* If a user is a super admin, add a support link to the message.
*
* @since 1.6.2.1
*
* @return string Support text if super admin, empty string if not.
*/
private function maybe_get_support_text(): string {
// If a user isn't a super admin, don't return any text.
if ( ! is_super_admin() ) {
return '';
}
// If the user is an admin, return text with a link to support.
// We add a space here to separate the sentences, but outside the localized text to avoid it being removed.
return ' ' . sprintf(
/* translators: placeholders are links. */
esc_html__( 'Please check out our %1$stroubleshooting guide%2$s for details on resolving this issue.', 'wpforms-lite' ),
'<a href="https://wpforms.com/docs/getting-support-wpforms/">',
'</a>'
);
}
/**
* Add token related strings to the frontend.
*
* @since 1.8.8
*
* @param array|mixed $strings Frontend strings.
*
* @return array Frontend strings.
*/
public function add_frontend_strings( $strings ): array {
$strings = (array) $strings;
$strings['error_updating_token'] = esc_html__(
'Error updating token. Please try again or contact support if the issue persists.',
'wpforms-lite'
);
$strings['network_error'] = esc_html__(
'Network error or server is unreachable. Check your connection or try again later.',
'wpforms-lite'
);
// Default token lifetime is 24 hours in seconds.
$token_lifetime = DAY_IN_SECONDS;
/**
* Filter token cache lifetime in seconds.
*
* @since 1.8.8
*
* @param integer $token_lifetime Token lifetime in seconds.
*/
$strings['token_cache_lifetime'] = apply_filters( 'wpforms_forms_token_cache_lifetime', $token_lifetime );
return $strings;
}
/**
* Update token via ajax handler.
*
* @since 1.8.8
* @since 2.0.2 Added the signed render time token to the response, and the form ID validation.
*/
public function ajax_get_token() {
$form_id = filter_input( INPUT_POST, 'formId', FILTER_VALIDATE_INT );
// Both tokens are bound to the form, so neither could ever be verified without a valid ID.
if ( ! is_int( $form_id ) || $form_id < 1 ) {
wp_send_json_error();
}
$form_data = [ 'id' => $form_id ];
$response = [
'token' => $this->get( true, $form_data ),
'time_token' => $this->get_time_token( $form_data ),
];
wp_send_json_success( $response );
}
}