id(); $summary = 'link' === $style ? 'rooster-lms-events__cta-link' : 'rooster-lms-button rooster-lms-button--outline rooster-lms-button--sm'; $html = '
' . esc_html__( 'Notify me', 'rooster-lms' ) . '' . '
' . '' . '' . '' /* translators: %s: lesson title. */ . '

' . esc_html( sprintf( __( 'We will e-mail you when "%s" or the next lessons of its course open for invitations.', 'rooster-lms' ), $title ) ) . '

'; \RoosterLMS\UI\Assets::enqueue_part( \RoosterLMS\UI\Assets::SCHEDULE ); $html .= wp_nonce_field( self::NONCE, '_rl_ev_nonce', false, false ); if ( $user_id <= 0 ) { $html .= '

' . '

' . ''; $html .= (string) apply_filters( 'rooster_lms_events_notify_captcha', '', $e->id() ); } $html .= '

' . '

' . '
'; return $html; } public static function notice( int $occurrence_id, int $course_id = 0 ): string { if ( \RoosterLMS\Blocks\Request::int( self::FIELD_OCCURRENCE ) !== $occurrence_id ) { return ''; } $outcome = \RoosterLMS\Blocks\Request::key( self::ARG_OUTCOME ); $text = match ( $outcome ) { 'sent' => __( 'Check your inbox: we sent you a link to confirm.', 'rooster-lms' ), 'added' => __( 'Done: we will e-mail you when this course sends its next invitation.', 'rooster-lms' ), 'consent' => __( 'Tick the box to let us e-mail you.', 'rooster-lms' ), 'invalid' => __( 'Enter a valid e-mail address.', 'rooster-lms' ), 'captcha' => __( 'Please complete the check and try again.', 'rooster-lms' ), 'limited' => __( 'Too many tries. Please try again later.', 'rooster-lms' ), 'expired' => __( 'The form expired. Please try again.', 'rooster-lms' ), 'unavailable' => __( 'This lesson is not open for notices.', 'rooster-lms' ), default => '', }; if ( '' === $text ) { return ''; } $good = in_array( $outcome, array( 'sent', 'added' ), true ); $stop = ''; $user = get_current_user_id(); if ( 'added' === $outcome && $user > 0 ) { $course = $course_id; if ( $course <= 0 ) { $o = Services::live_session_repo()->occurrence( $occurrence_id ); $s = null === $o ? null : Services::live_session_repo()->get( $o->session_id ); $course = null === $s ? 0 : $s->course_id; } if ( $course > 0 ) { $stop = ' ' . esc_html__( 'Stop these notices', 'rooster-lms' ) . ''; } } return '

' . esc_html( $text ) . $stop . '

'; } public static function post(): void { if ( ! \RoosterLMS\Admin\Request::posted_nonce_valid( '_rl_ev_nonce', self::NONCE ) ) { self::back( 'expired', \RoosterLMS\Blocks\Request::int( self::FIELD_OCCURRENCE ) ); } $fields = array(); foreach ( array( self::FIELD_OCCURRENCE, self::FIELD_EMAIL, self::FIELD_CONSENT, self::FIELD_HONEYPOT, '_rl_ev_nonce' ) as $key ) { $fields[ $key ] = \RoosterLMS\Admin\Request::posted_text( $key, 254 ); } $extra = array(); foreach ( self::CAPTCHA_FIELDS as $key ) { if ( \RoosterLMS\Admin\Request::posted_has( $key ) ) { $extra[ $key ] = \RoosterLMS\Admin\Request::posted_text( $key, 4000 ); } } self::back( self::handle( $fields, get_current_user_id(), $extra ), absint( $fields[ self::FIELD_OCCURRENCE ] ) ); } private static function back( string $outcome, int $occurrence ): void { $back = (string) wp_get_referer(); $back = '' !== $back ? $back : home_url( '/' ); $back = add_query_arg( array( self::ARG_OUTCOME => $outcome, self::FIELD_OCCURRENCE => $occurrence, ), remove_query_arg( array( self::ARG_OUTCOME, self::FIELD_OCCURRENCE ), $back ) ) . '#rl-ev-' . $occurrence; wp_safe_redirect( \RoosterLMS\Admin\View::sign( $back ), 303, 'Rooster LMS' ); exit; } public static function handle( array $f, int $user_id, array $extra = array() ): string { $nonce = sanitize_text_field( (string) ( $f['_rl_ev_nonce'] ?? '' ) ); if ( ! wp_verify_nonce( $nonce, self::NONCE ) ) { return 'expired'; } $occurrence = absint( $f[ self::FIELD_OCCURRENCE ] ?? 0 ); if ( $user_id > 0 ) { if ( '1' !== (string) ( $f[ self::FIELD_CONSENT ] ?? '' ) ) { return 'consent'; } $event = self::visible( $occurrence, $user_id ); if ( null === $event ) { return 'unavailable'; } $user = get_userdata( $user_id ); if ( ! $user instanceof \WP_User || ! is_email( (string) $user->user_email ) ) { return 'invalid'; } try { Services::consent()->set_offers( $user_id, true, ConsentSource::EVENTS, Actor::learner( $user_id ) ); } catch ( DomainError $e ) { unset( $e ); return 'unavailable'; } if ( ! Interest::add( $event->course_id, $user_id, '', Switcher::current() ) ) { return 'unavailable'; } return 'added'; } if ( '' !== trim( (string) ( $f[ self::FIELD_HONEYPOT ] ?? '' ) ) ) { return 'sent'; } $email = sanitize_email( (string) ( $f[ self::FIELD_EMAIL ] ?? '' ) ); if ( '' === $email || ! is_email( $email ) ) { return 'invalid'; } if ( '1' !== (string) ( $f[ self::FIELD_CONSENT ] ?? '' ) ) { return 'consent'; } $verdict = apply_filters( 'rooster_lms_events_notify_captcha_verify', true, $extra, $occurrence ); if ( true !== $verdict ) { return 'captcha'; } if ( ! FeedRate::count( 'notify', self::LIMIT_IP ) || ! FeedRate::count( 'notify-mail', self::LIMIT_EMAIL, RsvpTokens::email_hash( $email ) ) ) { return 'limited'; } $e = self::visible( $occurrence, 0 ); if ( null === $e ) { return 'unavailable'; } self::send_confirmation( $email, $e ); return 'sent'; } private static function visible( int $occurrence_id, int $user_id ): ?Event { if ( $occurrence_id <= 0 ) { return null; } $q = new EventQuery( array(), array(), '', '', 0, '', EventQuery::MAX_MONTHS, 1, 1, false, 0, 0, $occurrence_id ); return EventRepository::find( $q, $user_id, new \DateTimeImmutable( '@' . Clock::timestamp() ), $user_id > 0 ? EventRepository::VIEWER : EventRepository::GUEST, false, 1, 0 )[0] ?? null; } private static function send_confirmation( string $email, Event $e ): void { $lang = Switcher::current(); $token = Crypto::seal( (string) wp_json_encode( array( 'e' => $email, 'o' => $e->id(), 'l' => $lang, 'x' => Clock::timestamp() + self::TTL_HOURS * HOUR_IN_SECONDS, ) ) ); $url = add_query_arg( self::ARG_CONFIRM, rawurlencode( $token ), home_url( '/' ) ); $title = (string) ( EventPosts::of( array( $e ), $lang )[ $e->id() ]['title'] ?? '' ); $site = wp_specialchars_decode( (string) get_bloginfo( 'name' ), ENT_QUOTES ); /* translators: %s: site name. */ $subject = sprintf( __( 'Confirm your e-mail for %s', 'rooster-lms' ), $site ); $p = static fn( string $text, string $style = 'margin:0 0 12px;' ): string => '

' . esc_html( $text ) . '

'; $body = $p( /* translators: %s: lesson title. */ sprintf( __( 'You asked us to e-mail you about "%s". Confirm your address to get the invitation:', 'rooster-lms' ), $title ) ) . '

' . esc_html__( 'Confirm my e-mail', 'rooster-lms' ) . '

' /* translators: %d: hours. */ . $p( sprintf( _n( 'The link works for %d hour.', 'The link works for %d hours.', self::TTL_HOURS, 'rooster-lms' ), self::TTL_HOURS ) ) . $p( __( 'If you did not ask for this, ignore this message: nothing is stored and you will not get e-mails from us.', 'rooster-lms' ), 'margin:16px 0 0;font-size:12px;color:#646970;' ); Services::notifier()->deliver( $email, $subject, $body, 0, $lang ); } public static function confirm_nonce_action( string $token ): string { return 'rooster_lms_events_confirm_' . $token; } public static function confirm_route(): void { $raw = \RoosterLMS\UI\TableRequest::get( self::ARG_CONFIRM ); if ( ! is_string( $raw ) || '' === $raw ) { return; } $token = substr( preg_replace( '/[^A-Za-z0-9_:\-]/', '', $raw ) ?? '', 0, 600 ); $method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_key( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET'; $post = 'POST' === $method && \RoosterLMS\Admin\Request::posted_nonce_valid( RsvpPage::NONCE_FIELD, self::confirm_nonce_action( $token ) ); $secret = \RoosterLMS\Admin\Request::posted_key( 'rl_secret' ); $page = self::confirm( $token, $post, $secret ); status_header( 200 ); foreach ( RsvpPage::HEADERS as $name => $value ) { header( $name . ': ' . $value ); } header( 'Content-Type: text/html; charset=utf-8' ); RsvpPage::output( $page ); exit; } public static function confirm( string $token, bool $post, string $secret ): string { $data = self::open( $token ); if ( null === $data ) { return '

' . esc_html__( 'This link does not work', 'rooster-lms' ) . '

' . esc_html__( 'The link may have expired or be incomplete. Ask again from the lesson\'s page.', 'rooster-lms' ) . '

'; } return (string) Switcher::with( (string) $data['l'], static function () use ( $data, $token, $post, $secret ): string { $head = '

' . esc_html__( 'Confirm your e-mail', 'rooster-lms' ) . '

'; if ( ! $post || ! hash_equals( self::secret( $token ), $secret ) ) { return $head . '

' . esc_html__( 'Confirm to get an e-mail invitation to the lesson you chose and related lessons.', 'rooster-lms' ) . '

' . '
' . RsvpPage::nonce_input( self::confirm_nonce_action( $token ) ) . '
'; } $email = (string) $data['e']; $user = get_user_by( 'email', $email ); $o = Services::live_session_repo()->occurrence( (int) $data['o'] ); $s = null === $o ? null : Services::live_session_repo()->get( $o->session_id ); $course = null === $s ? 0 : $s->course_id; if ( $course <= 0 ) { return $head . '

' . esc_html__( 'This lesson is not open for notices.', 'rooster-lms' ) . '

'; } if ( $user instanceof \WP_User ) { try { Services::consent()->set_offers( (int) $user->ID, true, ConsentSource::EVENTS, Actor::system() ); } catch ( DomainError $e ) { unset( $e ); } } $uid = $user instanceof \WP_User ? (int) $user->ID : 0; Interest::add( $course, $uid, $email, (string) $data['l'] ); return $head . '

' . esc_html__( 'Thank you. We will e-mail you when this course sends its next invitation.', 'rooster-lms' ) . '

' . '

' . esc_html__( 'Changed your mind? Stop these notices', 'rooster-lms' ) . '

'; } ); } public static function open( string $token ): ?array { $json = Crypto::open( $token ); $data = '' === $json ? null : json_decode( $json, true ); if ( ! is_array( $data ) || ! is_email( (string) ( $data['e'] ?? '' ) ) || (int) ( $data['o'] ?? 0 ) <= 0 || (int) ( $data['x'] ?? 0 ) < Clock::timestamp() ) { return null; } return array( 'e' => (string) $data['e'], 'o' => (int) $data['o'], 'l' => sanitize_text_field( (string) ( $data['l'] ?? '' ) ), 'x' => (int) $data['x'], ); } public static function secret( string $token ): string { return substr( hash_hmac( 'sha256', 'rooster-lms-events-confirm|' . $token, wp_salt( 'nonce' ) ), 0, 32 ); } }