get_var($wpdb->prepare("SELECT COUNT(*) FROM {$wpdb->prefix}pcwp_rsvps WHERE event_id=%d AND status='confirmed'", $eventId)); } /** @return array{id:int,duplicate:bool}|\WP_Error */ public function register(int $eventId, string $name, string $email): array|\WP_Error { $name = sanitize_text_field($name); $email = strtolower(sanitize_email($email)); if ($name === '' || mb_strlen($name) > 150 || strlen($email) > 254 || ! is_email($email)) { return new \WP_Error('pcwp_rsvp_fields', __('Enter your name and a valid email address.', 'proeventswp'), ['status' => 400]); } global $wpdb; $table = $wpdb->prefix . 'pcwp_rsvps'; $lock = 'pcwp_rsvp_' . md5($wpdb->prefix . $eventId); if ((string) $wpdb->get_var($wpdb->prepare('SELECT GET_LOCK(%s,5)', $lock)) !== '1') { return new \WP_Error('pcwp_rsvp_busy', __('Please try again in a moment.', 'proeventswp'), ['status' => 409]); } $rowId = 0; $token = ''; try { wp_cache_delete($eventId, 'post_meta'); $event = \ProCalendarWP\Plugin::instance()->events()->find($eventId); if ($event === null || ! \ProCalendarWP\Events\EventVisibility::isPublic($eventId) || ! get_post_meta($eventId, '_pcwp_rsvp_enabled', true) || in_array($event->status, ['canceled', 'postponed'], true) || $event->endLocal->getTimestamp() < time()) { return new \WP_Error('pcwp_rsvp_closed', __('Registration is closed for this event.', 'proeventswp'), ['status' => 403]); } $hash = hash('sha256', $email); $existing = $wpdb->get_row($wpdb->prepare("SELECT id,status FROM {$table} WHERE event_id=%d AND (email_hash=%s OR email=%s)", $eventId, $hash, $email), ARRAY_A); if (is_array($existing) && $existing['status'] === 'confirmed') { return ['id' => (int) $existing['id'], 'duplicate' => true]; } $capacity = absint(get_post_meta($eventId, '_pcwp_rsvp_capacity', true)); if ($capacity > 0 && self::activeCount($eventId) >= $capacity) { return new \WP_Error('pcwp_rsvp_full', __('This event is fully booked.', 'proeventswp'), ['status' => 409]); } $token = wp_generate_password(64, false, false); $data = ['event_id' => $eventId, 'email_hash' => $hash, 'name' => $name, 'email' => $email, 'user_id' => get_current_user_id(), 'status' => 'confirmed', 'token_hash' => hash('sha256', $token), 'schedule_hash' => self::scheduleHash($eventId), 'mail_status' => 'pending', 'updated_at' => current_time('mysql', true)]; if (is_array($existing)) { $rowId = (int) $existing['id']; $saved = $wpdb->update($table, $data, ['id' => $rowId]); } else { $data['created_at'] = current_time('mysql', true); $saved = $wpdb->insert($table, $data); $rowId = (int) $wpdb->insert_id; } if ($saved === false) { return new \WP_Error('pcwp_rsvp_storage', __('Your place could not be reserved. Please try again.', 'proeventswp'), ['status' => 500]); } } finally { $wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock)); } $this->confirmation($rowId, $token); return ['id' => $rowId, 'duplicate' => false]; } public function cancellation(int $id, string $token, bool $admin = false): bool { global $wpdb; $row = $this->row($id); if ($row === null || (! $admin && ! hash_equals((string) $row['token_hash'], hash('sha256', $token)))) { return false; } $lock = 'pcwp_rsvp_' . md5($wpdb->prefix . $row['event_id']); if ((string) $wpdb->get_var($wpdb->prepare('SELECT GET_LOCK(%s,5)', $lock)) !== '1') { return false; } try { $where = ['id' => $id, 'status' => 'confirmed']; if (! $admin) { $where['token_hash'] = hash('sha256', $token); } return $wpdb->update($wpdb->prefix . 'pcwp_rsvps', ['status' => 'cancelled', 'updated_at' => current_time('mysql', true)], $where) === 1; } finally { $wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock)); } } /** @return array|null */ public function row(int $id): ?array { global $wpdb; $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->prefix}pcwp_rsvps WHERE id=%d", $id), ARRAY_A); return is_array($row) ? $row : null; } public function confirmation(int $id, string $token = '', bool $changed = false): bool { global $wpdb; $row = $this->row($id); if ($row === null || $row['status'] !== 'confirmed' || $row['email'] === '') { return false; } $rotated = $token === ''; if ($rotated) { $token = wp_generate_password(64, false, false); $wpdb->update($wpdb->prefix . 'pcwp_rsvps', ['token_hash' => hash('sha256', $token)], ['id' => $id]); } $event = \ProCalendarWP\Plugin::instance()->events()->find((int) $row['event_id']); if ($event === null) { return false; } $cancel = add_query_arg(['pcwp_rsvp_cancel' => $id, 'token' => $token], home_url('/')); $changed = $changed || in_array($event->status, ['canceled', 'postponed'], true); $heading = $event->status === 'canceled' ? __('Your event has been cancelled', 'proeventswp') : ($changed ? __('Your event has been updated', 'proeventswp') : __('Your place is reserved', 'proeventswp')); $dateFormat = get_option('date_format') . ($event->allDay || ($event->schedule['type'] ?? '') === 'daily' ? '' : ' ' . get_option('time_format')); $when = wp_date($dateFormat, $event->startLocal->getTimestamp(), $event->startLocal->getTimezone()) . ' – ' . wp_date($dateFormat, $event->endLocal->getTimestamp(), $event->endLocal->getTimezone()); $body = '

' . esc_html(get_bloginfo('name')) . '

' . esc_html($heading) . '

' . esc_html($row['name']) . ',

' . esc_html($event->title) . '

' . esc_html($when . ' · ' . $event->timezone) . '

' . esc_html(ucwords(str_replace('-', ' ', $event->status))) . '

' . esc_html__('View event details', 'proeventswp') . '

' . esc_html__('Cancel my registration', 'proeventswp') . '

'; // translators: %s: event title. $sent = wp_mail((string) $row['email'], sprintf($changed ? __('Event updated: %s', 'proeventswp') : __('Registration confirmed: %s', 'proeventswp'), $event->title), $body, ['Content-Type: text/html; charset=UTF-8']); if (! $sent && $rotated) { // Keep the last delivered cancellation link working when a resend fails. $wpdb->update($wpdb->prefix . 'pcwp_rsvps', ['token_hash' => $row['token_hash']], ['id' => $id, 'token_hash' => hash('sha256', $token)]); } $wpdb->update($wpdb->prefix . 'pcwp_rsvps', ['mail_status' => $sent ? 'sent' : 'failed'], ['id' => $id]); return $sent; } public static function scheduleHash(int $eventId): string { return hash('sha256', (string) wp_json_encode(array_map(static fn (string $key): mixed => get_post_meta($eventId, $key, true), ['_pcwp_start', '_pcwp_end', '_pcwp_timezone', '_pcwp_status', '_pcwp_schedule', '_pcwp_recurrence_time']))); } }