tickets = $tickets;
$this->messages = $messages;
$this->attachments = $attachments;
$this->activity = $activity;
}
public function init() {
add_shortcode( 'dpsupport_form', array( $this, 'render_shortcode' ) );
add_action( 'template_redirect', array( $this, 'handle_guest_download' ), 0 );
add_action( 'template_redirect', array( $this, 'protect_guest_view' ), 1 );
add_action( 'template_redirect', array( $this, 'render_guest_endpoint' ), 2 );
add_action( 'template_redirect', array( $this, 'handle_post' ), 5 );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
}
public function protect_guest_view() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Presence check for a signed, read-only guest link; token validation happens before ticket data is shown.
if ( empty( $_GET['dpsupport_guest_ticket'] ) || empty( $_GET['dpsupport_guest_key'] ) ) {
return;
}
$this->send_private_guest_headers();
}
/**
* Serve an attachment only after a fresh ticket-token authorization.
* The attachment must belong to this ticket and to a public message.
*/
public function handle_guest_download() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only download selector; signed ticket token and attachment ownership are validated below.
$attachment_id = isset( $_GET['dpsupport_guest_download'] ) ? absint( $_GET['dpsupport_guest_download'] ) : 0;
if ( ! $attachment_id ) {
return;
}
$this->send_private_guest_headers();
$guest_request = $this->guest_ticket_request();
if ( is_wp_error( $guest_request ) || ! $guest_request ) {
$this->guest_download_error( 403, __( 'Der Download konnte nicht autorisiert werden.', '24laser-support-tickets' ) );
}
$ticket = $guest_request['ticket'];
if ( ! $this->guest_attachment_is_public( $ticket->id, $attachment_id ) ) {
$this->guest_download_error( 403, __( 'Dieser Anhang gehört nicht zu einem öffentlichen Verlauf dieses Tickets.', '24laser-support-tickets' ) );
}
$file = get_attached_file( $attachment_id );
if ( ! $file || ! is_file( $file ) || ! is_readable( $file ) ) {
$this->guest_download_error( 404, __( 'Die angeforderte Datei wurde nicht gefunden.', '24laser-support-tickets' ) );
}
$allowed_types = array( 'image/jpeg', 'image/png', 'image/webp', 'application/pdf' );
$mime = sanitize_mime_type( (string) get_post_mime_type( $attachment_id ) );
if ( ! in_array( $mime, $allowed_types, true ) ) {
$this->guest_download_error( 403, __( 'Dieser Dateityp darf über den Gastzugang nicht ausgeliefert werden.', '24laser-support-tickets' ) );
}
$filename = sanitize_file_name( wp_basename( $file ) );
if ( '' === $filename ) {
$filename = 'attachment-' . $attachment_id;
}
if ( ! headers_sent() ) {
status_header( 200 );
header( 'Content-Type: ' . $mime, true );
header( 'Content-Disposition: attachment; filename="' . str_replace( '"', '', $filename ) . '"; filename*=UTF-8\'\'' . rawurlencode( $filename ), true );
header( 'Content-Length: ' . (string) filesize( $file ), true );
header( 'X-Content-Type-Options: nosniff', true );
}
while ( ob_get_level() ) {
ob_end_clean();
}
require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
$filesystem = new WP_Filesystem_Direct( false );
$contents = $filesystem->get_contents( $file );
if ( false === $contents ) {
$this->guest_download_error( 500, __( 'Die Datei konnte nicht geöffnet werden.', '24laser-support-tickets' ) );
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Authorized binary attachment bytes must be sent unchanged.
echo $contents;
exit;
}
private function send_private_guest_headers() {
nocache_headers();
if ( ! headers_sent() ) {
header( 'Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0', true );
header( 'Pragma: no-cache', true );
header( 'Expires: Wed, 11 Jan 1984 05:00:00 GMT', true );
header( 'CDN-Cache-Control: no-store', true );
header( 'Cloudflare-CDN-Cache-Control: no-store', true );
header( 'Surrogate-Control: no-store', true );
header( 'X-Robots-Tag: noindex, nofollow, noarchive, nosnippet', true );
header( 'Referrer-Policy: no-referrer', true );
header( 'X-Content-Type-Options: nosniff', true );
}
}
/**
* The public support form contains nonces and user-specific fields.
* Never let a page/CDN cache mix a logged-in form with a guest form.
*/
private function send_dynamic_form_headers() {
nocache_headers();
if ( ! headers_sent() ) {
header( 'Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0', true );
header( 'Pragma: no-cache', true );
header( 'Expires: Wed, 11 Jan 1984 05:00:00 GMT', true );
header( 'CDN-Cache-Control: no-store', true );
header( 'Cloudflare-CDN-Cache-Control: no-store', true );
header( 'Surrogate-Control: no-store', true );
header( 'Vary: Cookie', false );
}
}
private function guest_download_error( $status, $message ) {
$status = absint( $status );
status_header( $status ?: 403 );
$this->send_private_guest_headers();
$response = $status ?: 403;
wp_die( esc_html( $message ), esc_html__( '24laser Support Download', '24laser-support-tickets' ), array( 'response' => absint( $response ) ) );
}
private function guest_attachment_is_public( $ticket_id, $attachment_id ) {
$ticket_id = absint( $ticket_id );
$attachment_id = absint( $attachment_id );
if ( ! $ticket_id || ! $attachment_id ) {
return false;
}
$public_message_ids = array();
foreach ( (array) $this->messages->get_public_for_ticket( $ticket_id ) as $message ) {
$public_message_ids[ absint( $message->id ?? 0 ) ] = true;
}
foreach ( (array) $this->attachments->get_for_ticket( $ticket_id ) as $attachment ) {
if ( absint( $attachment->attachment_id ?? 0 ) !== $attachment_id ) {
continue;
}
$message_id = absint( $attachment->message_id ?? 0 );
return $message_id && ! empty( $public_message_ids[ $message_id ] );
}
return false;
}
/**
* Render a valid signed guest ticket before WooCommerce/account-page redirects.
* This makes the personal ticket link independent of the page on which the
* frontend form shortcode lives and prevents WooCommerce from requiring login.
*/
public function render_guest_endpoint() {
/*
* Never consume a guest reply POST here. The reply handler runs later on
* template_redirect and must get the request before the standalone guest
* renderer outputs the page and exits. Without this guard the browser only
* reloaded the guest view and the submitted reply was never persisted.
*/
$request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_key( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
if ( 'POST' === strtoupper( $request_method ) ) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Presence check for a signed, read-only guest link; token validation happens before ticket data is shown.
if ( empty( $_GET['dpsupport_guest_ticket'] ) || empty( $_GET['dpsupport_guest_key'] ) ) {
return;
}
$guest_request = $this->guest_ticket_request();
if ( is_wp_error( $guest_request ) ) {
return;
}
if ( ! $guest_request ) {
return;
}
wp_enqueue_style(
'dpsupport-frontend-support-form',
DPSUPPORT_URL . 'assets/css/frontend-form.css',
array(),
DPSUPPORT_VERSION
);
status_header( 200 );
$this->send_private_guest_headers();
if ( function_exists( 'get_header' ) ) {
get_header();
}
echo '';
echo $this->render_guest_ticket( $guest_request['ticket'], $guest_request['key'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- renderer escapes its dynamic output.
echo '';
if ( function_exists( 'get_footer' ) ) {
get_footer();
}
exit;
}
public function enqueue_assets() {
if ( is_admin() || ! $this->form_enabled() || ! $this->tickets_available() ) {
return;
}
wp_enqueue_style(
'dpsupport-frontend-support-form',
DPSUPPORT_URL . 'assets/css/frontend-form.css',
array(),
DPSUPPORT_VERSION
);
}
public function render_shortcode( $atts = array() ) {
/*
* Gutenberg/Post-REST responses also calculate rendered post content.
* Do not execute the live support form (customer/session lookups,
* nonce generation, etc.) while WordPress is constructing a REST JSON
* response. The shortcode remains stored unchanged and renders normally
* on the public frontend.
*/
if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
return '
' . esc_html__( '24laser Support Formular', '24laser-support-tickets' ) . '
';
}
$this->send_dynamic_form_headers();
if ( ! $this->tickets_available() ) {
return $this->message_box( __( 'Das Supportformular ist derzeit nicht verfügbar.', '24laser-support-tickets' ), 'notice' );
}
DPSUPPORT_Guest_Ticket_Access::remember_current_page( $this->current_url() );
$guest_request = $this->guest_ticket_request();
if ( is_wp_error( $guest_request ) ) {
return $this->message_box( $guest_request->get_error_message(), 'error' );
}
if ( $guest_request ) {
return $this->render_guest_ticket( $guest_request['ticket'], $guest_request['key'] );
}
if ( ! $this->form_enabled() ) {
return $this->message_box( __( 'Das Supportformular ist derzeit deaktiviert.', '24laser-support-tickets' ), 'notice' );
}
if ( ! is_user_logged_in() && ! $this->guest_allowed() ) {
$message = __( 'Bitte melde dich an, um eine Support-Anfrage zu senden.', '24laser-support-tickets' );
if ( function_exists( 'wp_login_url' ) ) {
$message .= ' ' . esc_html__( 'Jetzt anmelden', '24laser-support-tickets' ) . '';
}
return '' . wp_kses_post( $message ) . '
';
}
wp_enqueue_style(
'dpsupport-frontend-support-form',
DPSUPPORT_URL . 'assets/css/frontend-form.css',
array(),
DPSUPPORT_VERSION
);
$atts = shortcode_atts(
array(
'title' => '',
),
(array) $atts,
'dpsupport_form'
);
$title = sanitize_text_field( (string) $atts['title'] );
if ( '' === $title ) {
$title = (string) get_option( 'dpsupport_frontend_form_title', __( 'Wie können wir helfen?', '24laser-support-tickets' ) );
}
$intro = (string) get_option(
'dpsupport_frontend_form_intro',
__( 'Beschreibe deine Frage oder dein Problem. Unser Support-Team erhält daraus direkt ein neues Ticket.', '24laser-support-tickets' )
);
$is_logged_in = is_user_logged_in();
$user = $is_logged_in ? wp_get_current_user() : null;
$customer = $this->customer_defaults( $user );
$name = $is_logged_in ? $customer['name'] : ( isset( $this->old['customer_name'] ) ? $this->old['customer_name'] : '' );
$email = $is_logged_in ? $customer['email'] : ( isset( $this->old['customer_email'] ) ? $this->old['customer_email'] : '' );
$subject = isset( $this->old['subject'] ) ? $this->old['subject'] : '';
$message = isset( $this->old['message'] ) ? $this->old['message'] : '';
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only success/status parameters generated by this plugin after a safe redirect.
$success = isset( $_GET['dpsupport_support_sent'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['dpsupport_support_sent'] ) );
$ticket_number = $success && isset( $_GET['dpsupport_support_ticket'] ) ? sanitize_text_field( wp_unslash( $_GET['dpsupport_support_ticket'] ) ) : '';
$upload_warning = $success && isset( $_GET['dpsupport_support_upload'] ) && 'warning' === sanitize_key( wp_unslash( $_GET['dpsupport_support_upload'] ) );
// phpcs:enable WordPress.Security.NonceVerification.Recommended
$guard_time = time();
$guard_token = $this->guard_token( $guard_time );
ob_start();
?>
handle_guest_reply();
return;
}
if ( 'create_ticket' !== $action ) {
return;
}
if ( ! $this->tickets_available() || ! $this->form_enabled() ) {
$this->errors[] = __( 'Das Supportformular ist derzeit nicht verfügbar.', '24laser-support-tickets' );
return;
}
if ( ! is_user_logged_in() && ! $this->guest_allowed() ) {
$this->errors[] = __( 'Bitte melde dich an, um eine Support-Anfrage zu senden.', '24laser-support-tickets' );
return;
}
if ( ! isset( $_POST['dpsupport_frontend_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['dpsupport_frontend_nonce'] ) ), 'dpsupport_frontend_create_ticket' ) ) {
$this->errors[] = __( 'Die Anfrage konnte nicht sicher geprüft werden. Bitte lade die Seite neu und versuche es erneut.', '24laser-support-tickets' );
return;
}
$this->capture_old_values();
if ( ! empty( $_POST['dpsupport_company_site'] ) ) {
$this->errors[] = __( 'Die Anfrage konnte nicht verarbeitet werden.', '24laser-support-tickets' );
return;
}
if ( ! $this->valid_guard() ) {
$this->errors[] = __( 'Die Formularsitzung ist abgelaufen oder ungültig. Bitte lade die Seite neu und versuche es erneut.', '24laser-support-tickets' );
return;
}
$is_logged_in = is_user_logged_in();
$user = $is_logged_in ? wp_get_current_user() : null;
$defaults = $this->customer_defaults( $user );
$name = $is_logged_in ? $defaults['name'] : sanitize_text_field( wp_unslash( $_POST['customer_name'] ?? '' ) );
$email = $is_logged_in ? $defaults['email'] : sanitize_email( wp_unslash( $_POST['customer_email'] ?? '' ) );
$subject = sanitize_text_field( wp_unslash( $_POST['subject'] ?? '' ) );
$message = wp_kses_post( wp_unslash( $_POST['message'] ?? '' ) );
$category = $this->default_category();
if ( '' === trim( $name ) ) {
$this->errors[] = __( 'Bitte gib deinen Namen ein.', '24laser-support-tickets' );
}
if ( ! $email || ! is_email( $email ) ) {
$this->errors[] = __( 'Bitte gib eine gültige E-Mail-Adresse ein.', '24laser-support-tickets' );
}
if ( '' === trim( $subject ) ) {
$this->errors[] = __( 'Bitte gib einen Betreff ein.', '24laser-support-tickets' );
}
if ( '' === trim( wp_strip_all_tags( $message ) ) ) {
$this->errors[] = __( 'Bitte beschreibe deine Anfrage.', '24laser-support-tickets' );
}
if ( strlen( wp_strip_all_tags( $message ) ) > 20000 ) {
$this->errors[] = __( 'Die Nachricht ist zu lang. Bitte kürze sie auf maximal 20.000 Zeichen.', '24laser-support-tickets' );
}
if ( $this->errors ) {
return;
}
if ( ! $this->pass_rate_limit( $email, ! $is_logged_in ) ) {
$this->errors[] = __( 'Bitte warte einen kurzen Moment, bevor du eine weitere Support-Anfrage sendest.', '24laser-support-tickets' );
return;
}
$ticket_id = $this->tickets->create(
array(
'customer_id' => $is_logged_in ? $user->ID : 0,
'customer_name' => $name,
'customer_email' => $email,
'subject' => $subject,
'category' => $category,
'status' => 'new',
'priority' => get_option( 'dpsupport_default_priority', 'normal' ),
)
);
if ( is_wp_error( $ticket_id ) ) {
$this->errors[] = $ticket_id->get_error_message();
return;
}
$author_name = $name;
$message_id = $this->messages->create(
$ticket_id,
array(
'author_type' => 'customer',
'author_user_id' => $is_logged_in ? $user->ID : 0,
'author_name' => $author_name,
'body' => $message,
'is_internal' => 0,
)
);
if ( is_wp_error( $message_id ) ) {
$this->tickets->delete_permanently( $ticket_id );
$this->errors[] = $message_id->get_error_message();
return;
}
$this->activity->add(
$ticket_id,
'frontend_created',
$is_logged_in
? __( 'Kunde hat das Ticket über das Frontend-Supportformular erstellt.', '24laser-support-tickets' )
: __( 'Gast hat das Ticket über das Frontend-Supportformular erstellt.', '24laser-support-tickets' ),
$is_logged_in ? $user->ID : 0
);
$upload_errors = $this->handle_uploads( $ticket_id, $message_id );
if ( ! $is_logged_in ) {
DPSUPPORT_Guest_Ticket_Access::remember_current_page( $this->current_url() );
}
DPSUPPORT_Notifications::notify_staff_new_ticket( $ticket_id, $message_id );
DPSUPPORT_Notifications::notify_customer_ticket_created( $ticket_id, $message_id, true );
do_action( 'dpsupport_ticket_created', $ticket_id, $is_logged_in ? 'frontend_customer' : 'frontend_guest' );
$ticket = $this->tickets->get( $ticket_id );
$ticket_number = $ticket ? (string) $ticket->ticket_number : '';
if ( ! $is_logged_in && $ticket ) {
$redirect = DPSUPPORT_Guest_Ticket_Access::ticket_url( $ticket );
} else {
$redirect = remove_query_arg( array( 'dpsupport_support_sent', 'dpsupport_support_ticket', 'dpsupport_support_upload' ), $this->current_url() );
}
$redirect = $redirect ?: $this->current_url();
$redirect = add_query_arg(
array(
'dpsupport_support_sent' => '1',
'dpsupport_support_ticket' => $ticket_number,
'dpsupport_support_upload' => $upload_errors ? 'warning' : 'ok',
),
$redirect
);
wp_safe_redirect( $redirect );
exit;
}
private function guest_ticket_request() {
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Signed guest-link credentials are read-only and verified cryptographically below.
$ticket_id = isset( $_GET['dpsupport_guest_ticket'] ) ? absint( $_GET['dpsupport_guest_ticket'] ) : 0;
$key = isset( $_GET['dpsupport_guest_key'] ) ? sanitize_text_field( wp_unslash( $_GET['dpsupport_guest_key'] ) ) : '';
// phpcs:enable WordPress.Security.NonceVerification.Recommended
if ( ! $ticket_id && '' === $key ) {
return null;
}
$ticket = $ticket_id ? $this->tickets->get( $ticket_id ) : null;
if ( ! $ticket || ! DPSUPPORT_Guest_Ticket_Access::verify( $ticket, $key ) ) {
return new WP_Error(
'dpsupport_invalid_guest_link',
__( 'Dieser Ticket-Link ist ungültig oder nicht mehr gültig. Bitte verwende den Link aus deiner Support-E-Mail.', '24laser-support-tickets' )
);
}
return array(
'ticket' => $ticket,
'key' => $key,
);
}
private function render_guest_ticket( $ticket, $key ) {
wp_enqueue_style(
'dpsupport-frontend-support-form',
DPSUPPORT_URL . 'assets/css/frontend-form.css',
array(),
DPSUPPORT_VERSION
);
$messages = $this->messages->get_public_for_ticket( $ticket->id );
$attachment_map = array();
foreach ( (array) $this->attachments->get_for_ticket( $ticket->id ) as $attachment ) {
$message_id = absint( $attachment->message_id ?? 0 );
if ( $message_id ) {
$attachment_map[ $message_id ][] = absint( $attachment->attachment_id ?? 0 );
}
}
$statuses = $this->tickets->statuses();
$categories = $this->tickets->categories();
$can_reply = empty( $ticket->archived_at ) && 'closed' !== $ticket->status;
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only redirect/result flags; no state changes are performed from these values.
$replied = isset( $_GET['dpsupport_guest_replied'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['dpsupport_guest_replied'] ) );
$sent = isset( $_GET['dpsupport_support_sent'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['dpsupport_support_sent'] ) );
$upload_warning = isset( $_GET['dpsupport_support_upload'] ) && 'warning' === sanitize_key( wp_unslash( $_GET['dpsupport_support_upload'] ) );
$error_code = isset( $_GET['dpsupport_guest_error'] ) ? sanitize_key( wp_unslash( $_GET['dpsupport_guest_error'] ) ) : '';
// phpcs:enable WordPress.Security.NonceVerification.Recommended
$error_text = $this->guest_error_message( $error_code );
ob_start();
?>
ticket_number ) );
?>
ticket_number ); ?>
status ] ?? $ticket->status ); ?>
category ] ?? $ticket->category ); ?>
updated_at ) ); ?>
author_type; ?>
author_name ?: __( 'Support-Team', '24laser-support-tickets' ) ) ); ?>
created_at ) ); ?>
body ) ); ?>
id ] ) ) : ?>
id ] as $attachment_id ) : ?>
tickets->get( $ticket_id ) : null;
if ( ! $this->tickets_available() || ! $ticket || ! DPSUPPORT_Guest_Ticket_Access::verify( $ticket, $key ) ) {
$this->redirect_guest_reply( $ticket, $key, array( 'dpsupport_guest_error' => 'invalid' ) );
}
if ( ! isset( $_POST['dpsupport_guest_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['dpsupport_guest_nonce'] ) ), 'dpsupport_guest_reply_' . $ticket_id ) ) {
$this->redirect_guest_reply( $ticket, $key, array( 'dpsupport_guest_error' => 'security' ) );
}
if ( ! empty( $ticket->archived_at ) || 'closed' === $ticket->status ) {
$this->redirect_guest_reply( $ticket, $key, array( 'dpsupport_guest_error' => 'closed' ) );
}
if ( ! $this->pass_guest_reply_rate_limit( $ticket_id ) ) {
$this->redirect_guest_reply( $ticket, $key, array( 'dpsupport_guest_error' => 'rate' ) );
}
$message = isset( $_POST['message'] ) ? wp_kses_post( wp_unslash( $_POST['message'] ) ) : '';
if ( '' === trim( wp_strip_all_tags( $message ) ) || strlen( wp_strip_all_tags( $message ) ) > 20000 ) {
$this->redirect_guest_reply( $ticket, $key, array( 'dpsupport_guest_error' => 'message' ) );
}
$message_id = $this->messages->create(
$ticket_id,
array(
'author_type' => 'customer',
'author_user_id' => 0,
'author_name' => $ticket->customer_name ?: __( 'Gast', '24laser-support-tickets' ),
'body' => $message,
'is_internal' => 0,
)
);
if ( is_wp_error( $message_id ) ) {
$this->redirect_guest_reply( $ticket, $key, array( 'dpsupport_guest_error' => 'save' ) );
}
$customer_reply_status = DPSUPPORT_Workflow::status_after_customer_reply();
if ( 'keep' !== $customer_reply_status && $customer_reply_status !== $ticket->status ) {
$this->tickets->update( $ticket_id, array( 'status' => $customer_reply_status ) );
$statuses = $this->tickets->statuses();
$this->activity->add(
$ticket_id,
'auto_status',
sprintf(
/* translators: %s: new ticket status label. */
__( 'Status nach Kundenantwort automatisch auf %s gesetzt.', '24laser-support-tickets' ),
$statuses[ $customer_reply_status ] ?? $customer_reply_status
),
0
);
} else {
$this->tickets->touch( $ticket_id );
}
$this->activity->add( $ticket_id, 'customer_reply', __( 'Gast hat über den persönlichen Frontend-Ticketlink geantwortet.', '24laser-support-tickets' ), 0 );
$upload_errors = $this->handle_uploads( $ticket_id, $message_id );
DPSUPPORT_Notifications::notify_staff_customer_reply( $ticket_id, $message_id );
do_action( 'dpsupport_customer_replied', $ticket_id, $message_id );
$this->redirect_guest_reply(
$this->tickets->get( $ticket_id ),
$key,
array(
'dpsupport_guest_replied' => '1',
'dpsupport_support_upload' => $upload_errors ? 'warning' : 'ok',
)
);
}
private function redirect_guest_reply( $ticket, $key, array $args ) {
$url = $ticket && DPSUPPORT_Guest_Ticket_Access::verify( $ticket, $key ) ? DPSUPPORT_Guest_Ticket_Access::ticket_url( $ticket ) : DPSUPPORT_Guest_Ticket_Access::base_url();
$url = $url ?: home_url( '/' );
wp_safe_redirect( add_query_arg( $args, $url ) );
exit;
}
private function pass_guest_reply_rate_limit( $ticket_id ) {
$ip = $this->remote_ip_key();
$ticket_id = absint( $ticket_id );
return $this->consume_rate_limit_buckets(
array(
array( 'dpsupport_gr_5_' . md5( $ticket_id . '|' . $ip ), 1, 5 ),
array( 'dpsupport_gr_60_' . md5( $ticket_id . '|' . $ip ), 6, MINUTE_IN_SECONDS ),
array( 'dpsupport_gr_3600_' . md5( $ticket_id . '|' . $ip ), 30, HOUR_IN_SECONDS ),
)
);
}
private function guest_error_message( $code ) {
$messages = array(
'invalid' => __( 'Der persönliche Ticket-Link konnte nicht bestätigt werden.', '24laser-support-tickets' ),
'security' => __( 'Die Antwort konnte nicht sicher geprüft werden. Bitte lade die Seite neu und versuche es erneut.', '24laser-support-tickets' ),
'closed' => __( 'Auf dieses geschlossene Ticket kann nicht mehr geantwortet werden.', '24laser-support-tickets' ),
'rate' => __( 'Bitte warte einen kurzen Moment, bevor du erneut antwortest.', '24laser-support-tickets' ),
'message' => __( 'Bitte gib eine Nachricht mit maximal 20.000 Zeichen ein.', '24laser-support-tickets' ),
'save' => __( 'Die Antwort konnte nicht gespeichert werden. Bitte versuche es erneut.', '24laser-support-tickets' ),
);
return isset( $messages[ $code ] ) ? $messages[ $code ] : '';
}
private function tickets_available() {
return true;
}
private function form_enabled() {
return 'no' !== get_option( 'dpsupport_frontend_form_enabled', 'yes' );
}
private function guest_allowed() {
return 'no' !== get_option( 'dpsupport_frontend_form_guests', 'yes' );
}
private function default_category() {
return $this->tickets->sanitize_category( get_option( 'dpsupport_default_category', 'allgemein' ) );
}
private function customer_defaults( $user ) {
if ( ! $user || empty( $user->ID ) ) {
return array( 'name' => '', 'email' => '' );
}
$name = trim( get_user_meta( $user->ID, 'billing_first_name', true ) . ' ' . get_user_meta( $user->ID, 'billing_last_name', true ) );
if ( '' === $name ) {
$name = (string) $user->display_name;
}
return array(
'name' => sanitize_text_field( $name ),
'email' => sanitize_email( $user->user_email ),
);
}
private function handle_uploads( $ticket_id, $message_id ) {
return DPSUPPORT_Upload_Handler::handle( $ticket_id, $message_id, $this->attachments ) ? array() : array( 'upload_error' );
}
private function guard_token( $timestamp ) {
return hash_hmac( 'sha256', (string) absint( $timestamp ), wp_salt( 'nonce' ) );
}
private function valid_guard() {
// phpcs:disable WordPress.Security.NonceVerification.Missing -- This helper is called only after dpsupport_frontend_nonce has been verified in handle_post().
$timestamp = isset( $_POST['dpsupport_form_time'] ) ? absint( $_POST['dpsupport_form_time'] ) : 0;
$token = isset( $_POST['dpsupport_form_token'] ) ? sanitize_text_field( wp_unslash( $_POST['dpsupport_form_token'] ) ) : '';
// phpcs:enable WordPress.Security.NonceVerification.Missing
if ( ! $timestamp || ! $token || ! hash_equals( $this->guard_token( $timestamp ), $token ) ) {
return false;
}
$age = time() - $timestamp;
return $age >= 2 && $age <= ( 2 * DAY_IN_SECONDS );
}
private function pass_rate_limit( $email, $is_guest = false ) {
$email = strtolower( sanitize_email( $email ) );
$ip = $this->remote_ip_key();
$buckets = array(
array( 'dpsupport_fr_pair_' . md5( $email . '|' . $ip ), 1, 15 ),
);
if ( $is_guest ) {
$buckets[] = array( 'dpsupport_fr_ip10_' . md5( $ip ), 5, 10 * MINUTE_IN_SECONDS );
$buckets[] = array( 'dpsupport_fr_ip60_' . md5( $ip ), 20, HOUR_IN_SECONDS );
$buckets[] = array( 'dpsupport_fr_mail10_' . md5( $email ), 3, 10 * MINUTE_IN_SECONDS );
$buckets[] = array( 'dpsupport_fr_mail60_' . md5( $email ), 10, HOUR_IN_SECONDS );
}
return $this->consume_rate_limit_buckets( $buckets );
}
private function consume_rate_limit_buckets( array $buckets ) {
$states = array();
foreach ( $buckets as $bucket ) {
$key = sanitize_key( (string) ( $bucket[0] ?? '' ) );
$limit = max( 1, absint( $bucket[1] ?? 1 ) );
$ttl = max( 1, absint( $bucket[2] ?? 1 ) );
$count = absint( get_transient( $key ) );
if ( $count >= $limit ) {
return false;
}
$states[] = array( $key, $count + 1, $ttl );
}
foreach ( $states as $state ) {
set_transient( $state[0], $state[1], $state[2] );
}
return true;
}
private function remote_ip_key() {
$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? trim( sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ) : '';
return filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : 'unknown';
}
private function capture_old_values() {
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Called only after dpsupport_frontend_nonce verification in handle_post().
$this->old = array(
'customer_name' => sanitize_text_field( wp_unslash( $_POST['customer_name'] ?? '' ) ),
'customer_email' => sanitize_email( wp_unslash( $_POST['customer_email'] ?? '' ) ),
'subject' => sanitize_text_field( wp_unslash( $_POST['subject'] ?? '' ) ),
'message' => wp_kses_post( wp_unslash( $_POST['message'] ?? '' ) ),
'category' => sanitize_key( wp_unslash( $_POST['category'] ?? '' ) ),
);
// phpcs:enable WordPress.Security.NonceVerification.Missing
}
private function current_url() {
$scheme = is_ssl() ? 'https://' : 'http://';
$host = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : wp_parse_url( home_url( '/' ), PHP_URL_HOST );
$uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '/';
$url = $scheme . $host . $uri;
return wp_validate_redirect( $url, home_url( '/' ) );
}
private function message_box( $message, $type = 'notice' ) {
return '' . esc_html( $message ) . '
';
}
}