is_valid_rsvp_comment( $comment, $comment_id ) ) { return; } $this->comment = $comment; } /** * Validates if a comment is a valid RSVP comment. * * @since 1.0.0 * * @param WP_Comment|null $comment The comment object to validate. * @param int $comment_id The comment ID for fallback validation. * * @return bool True if valid RSVP comment, false otherwise. */ private function is_valid_rsvp_comment( ?WP_Comment $comment, int $comment_id ): bool { return $comment instanceof WP_Comment && Rsvp::COMMENT_TYPE === get_comment_type( $comment_id ); } /** * Retrieves the token string for this RSVP comment. * * Returns the cached token if available, otherwise retrieves it from comment meta. * * @since 1.0.0 * * @return string The token string, or empty string if no token exists. */ public function get_token(): string { if ( $this->token ) { return $this->token; } if ( ! $this->comment ) { return ''; } $token = (string) get_comment_meta( (int) $this->comment->comment_ID, $this->get_meta_key(), true ); $this->token = $token; return $this->token; } /** * Approves the RSVP comment. * * Changes the comment status from pending to approved, typically used * when a user clicks a verification link in their email. * * @since 1.0.0 * * @return void */ public function approve_comment(): void { if ( ! $this->comment ) { return; } wp_set_comment_status( (int) $this->comment->comment_ID, 'approve' ); } /** * Gets the meta key for storing the token. * * @since 1.0.0 * * @return string The formatted meta key. */ private function get_meta_key(): string { return sprintf( '%s%s', self::META_KEY_PREFIX, static::NAME ); } /** * Generates a new secure token for this RSVP comment. * * Creates a secure random token and saves it to comment meta. * * @since 1.0.0 * * @return self Returns the current instance for method chaining. */ public function generate_token(): self { if ( ! $this->comment ) { return $this; } $this->token = $this->create_secure_token(); $this->save_token_to_meta(); return $this; } /** * Creates a secure random token. * * @since 1.0.0 * * @return string The generated token. */ private function create_secure_token(): string { return wp_generate_password( self::TOKEN_LENGTH, false ); } /** * Saves the current token to comment meta. * * @since 1.0.0 * * @return void */ private function save_token_to_meta(): void { update_comment_meta( (int) $this->comment->comment_ID, $this->get_meta_key(), $this->token ); } /** * Retrieves the comment object associated with this token. * * @since 1.0.0 * * @return WP_Comment|null The comment object, or null if not set. */ public function get_comment(): ?WP_Comment { return $this->comment; } /** * Retrieves the event post object associated with this token. * * @since 1.0.0 * * @return WP_Post|null The event post object, or null if not set. */ public function get_post(): ?WP_Post { $comment = $this->get_comment(); if ( ! $comment ) { return null; } $post = get_post( (int) $comment->comment_post_ID ); if ( ! $post || Event::POST_TYPE !== get_post_type( $post ) ) { return null; } return $post; } /** * Retrieves the email address from the RSVP comment. * * @since 1.0.0 * * @return string The comment author's email address, or empty string if not available. */ public function get_email(): string { if ( ! $this->comment ) { return ''; } return $this->comment->comment_author_email; } /** * Validates a token against the stored token for this comment. * * @since 1.0.0 * * @param string $token The token to validate. * * @return bool True if the token is valid, false otherwise. */ public function is_valid( string $token ): bool { return ! empty( $token ) && $this->get_token() === $token; } /** * Creates an Rsvp_Token instance from a token string. * * Parses the token string and validates it against the stored token. * Returns null if the token string is invalid or doesn't match. * * @since 1.0.0 * * @param string|null $token_string Token in format "commentId_token". * @return self|null Instance if valid, null otherwise. */ public static function from_token_string( ?string $token_string ): ?self { $parts = self::parse_token_string( $token_string ); if ( empty( $parts ) ) { return null; } $instance = new self( $parts['comment_id'] ); // Validate the token matches. if ( ! $instance->is_valid( $parts['token'] ) ) { return null; } return $instance; } /** * Parse token string into component parts. * * Splits a token string in the format "commentId_token" into its * component parts for validation and instantiation. * * @since 1.0.0 * * @param string|null $token_string Raw token string in format "commentId_token". * @return array Array with 'comment_id' and 'token' keys, or empty array if invalid. */ public static function parse_token_string( ?string $token_string ): array { if ( empty( $token_string ) ) { return array(); } $parts = explode( '_', $token_string, 2 ); if ( 2 !== count( $parts ) || ! is_numeric( $parts[0] ) ) { return array(); } return array( 'comment_id' => (int) $parts[0], 'token' => $parts[1], ); } /** * Creates an Rsvp_Token instance from a URL parameter. * * Retrieves the token from GET request and attempts to create * a valid instance. Useful for handling magic link clicks. * * @since 1.0.0 * * @return self|null Instance if valid token found, null otherwise. */ public static function from_url_parameter(): ?self { $token_param = Utility::get_http_input( INPUT_GET, self::NAME ); return self::from_token_string( $token_param ); } /** * Generates the confirmation URL with the event URL and token parameter. * * @since 1.0.0 * * @return string The confirmation URL, or empty string if post not found. */ public function generate_url(): string { $post = $this->get_post(); $comment = $this->get_comment(); $token = $this->get_token(); if ( ! $this->has_required_url_components( $post, $comment, $token ) ) { return ''; } $event_url = get_permalink( $post ); if ( ! $event_url ) { return ''; } $token_value = $this->format_token_value( (int) $comment->comment_ID, $token ); return add_query_arg( static::NAME, $token_value, $event_url ); } /** * Checks if all required components for URL generation are available. * * @since 1.0.0 * * @param WP_Post|null $post The post object. * @param WP_Comment|null $comment The comment object. * @param string $token The token string. * * @return bool True if all components are available, false otherwise. */ private function has_required_url_components( ?WP_Post $post, ?WP_Comment $comment, string $token ): bool { return $post instanceof WP_Post && $comment instanceof WP_Comment && ! empty( $token ); } /** * Formats the token value for URL inclusion. * * @since 1.0.0 * * @param int $comment_id The comment ID. * @param string $token The token string. * * @return string The formatted token value. */ private function format_token_value( int $comment_id, string $token ): string { return sprintf( '%d_%s', $comment_id, $token ); } /** * Send RSVP confirmation email with magic link. * * Sends an email containing a magic link that allows the user to confirm * their RSVP and change their response if needed. * * @since 1.0.0 * * @return bool True if email was sent successfully, false otherwise. */ public function send_rsvp_confirmation_email(): bool { $to = $this->get_email(); if ( empty( $to ) ) { return false; } $post = $this->get_post(); $email_data = $this->prepare_email_data( $post ); return wp_mail( $to, $email_data['subject'], $email_data['body'], $email_data['headers'] ); } /** * Prepares email data for the confirmation email. * * @since 1.0.0 * * @param WP_Post|null $post The event post object. * * @return array Email data with subject, body, and headers. */ private function prepare_email_data( ?WP_Post $post ): array { $title = $post ? get_the_title( $post ) : __( 'this event', 'gatherpress' ); return array( 'subject' => sprintf( /* translators: %s: Event title. */ __( 'Confirm your RSVP for %s', 'gatherpress' ), $title ), 'body' => $this->render_email_template( $post ), 'headers' => array( 'Content-Type: text/html; charset=UTF-8' ), ); } /** * Renders the email template for the confirmation email. * * @since 1.0.0 * * @param WP_Post|null $post The event post object. * * @return string The rendered email body. */ private function render_email_template( ?WP_Post $post ): string { return Utility::render_template( sprintf( '%s/includes/templates/admin/emails/rsvp-token-confirmation.php', GATHERPRESS_CORE_PATH ), array( 'event_id' => $post ? $post->ID : 0, 'token_url' => $this->generate_url(), ) ); } }