405]); } // Rate limiting check $this->checkRateLimit(); // Validate required parameter exists and sanitize // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public endpoint uses hash validation instead of nonces if (!isset($_REQUEST['fs_view'])) { wp_die('Missing required parameter', 'Bad Request', ['response' => 400]); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public endpoint uses hash validation instead of nonces $route = isset($_REQUEST['fs_view']) ? sanitize_text_field(wp_unslash($_REQUEST['fs_view'])) : ''; if (empty($route)) { wp_die('Missing required parameter', 'Bad Request', ['response' => 400]); } // Validate route value $methodMaps = [ 'ticket' => 'handleTicketView' ]; if (isset($methodMaps[$route])) { // For public endpoints, verify security using ticket hash validation instead of nonces // This is appropriate for public endpoints that must work without user authentication $this->verifyPublicEndpointSecurity($route); $this->{$methodMaps[$route]}(); } else { wp_die('Invalid route', 'Not Found', ['response' => 404]); } } public function handleTicketView() { if (!Helper::isPublicSignedTicketEnabled()) { $this->handleInvalidTicket(); } else { $this->handleValidTicket(); } } /** * Display the attachment. * * Uses the new rewrite endpoint to get an attachment ID * and display the attachment if the currently logged in user * has the authorization to. * * @return void * @since 3.2.0 */ public function view_attachment() { // Verify this is a GET request for security // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- REQUEST_METHOD is server-controlled, sanitized for comparison only $requestMethod = isset($_SERVER['REQUEST_METHOD']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_METHOD'])) : ''; if ($requestMethod !== 'GET') { wp_die('Invalid request method', 'Method Not Allowed', ['response' => 405]); } // Rate limiting check $this->checkRateLimit(); // Validate required parameter exists and sanitize // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public endpoint uses signature validation instead of nonces if (!isset($_REQUEST['fst_file'])) { wp_die('Missing required parameter', 'Bad Request', ['response' => 400]); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public endpoint uses signature validation instead of nonces $attachmentHash = isset($_REQUEST['fst_file']) ? sanitize_text_field(wp_unslash($_REQUEST['fst_file'])) : ''; if (empty($attachmentHash)) { wp_die('Invalid Attachment Hash', 'Bad Request', ['response' => 400]); } $attachment = $this->getAttachmentByHash($attachmentHash); if (!$attachment) { wp_die('Invalid Attachment Hash', 'Not Found', ['response' => 404]); } // Inline attachments (paste images embedded in ticket/email content) are publicly accessible // without a signature because they are already shared with customers via email. // Other attachments require HMAC signature validation. if ($attachment->status !== 'inline' && !$this->validateAttachmentSignature($attachment)) { $dieMessage = esc_html__('Sorry, Your secure sign is invalid, Please reload the previous page and get new signed url', 'fluent-support'); wp_die(esc_html($dieMessage), 'Forbidden', ['response' => 403]); } //If external file, redirect to the secure download URL if ('local' !== $attachment->driver) { $fileUrl = apply_filters('fluent_support/external_attachment_url', $attachment->full_url, $attachment); if (!empty($fileUrl)) { $this->redirectToExternalAttachment($fileUrl); } else { die('File could not be found'); } } //Handle Local file if (!file_exists($attachment->file_path)) { die('File could not be found'); } $this->serveLocalAttachment($attachment); } private function getAttachmentByHash($attachmentHash) { return Attachment::where('file_hash', $attachmentHash)->first(); } private function validateAttachmentSignature($attachment) { // Sanitize and validate secure_sign input - don't trust any input // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public endpoint uses signature validation instead of nonces if (!isset($_REQUEST['secure_sign'])) { return false; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public endpoint uses signature validation instead of nonces $secureSign = isset($_REQUEST['secure_sign']) ? sanitize_text_field(wp_unslash($_REQUEST['secure_sign'])) : ''; if (empty($secureSign)) { return false; } // Use HMAC-SHA256 for secure signature verification $sign = hash_hmac('sha256', $attachment->id . '|' . gmdate('YmdH'), wp_salt('secure_auth')); return hash_equals($sign, $secureSign); } private function handleInvalidTicket() { // Validate required parameter exists and sanitize // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public endpoint uses hash validation instead of nonces if (!isset($_REQUEST['ticket_id'])) { wp_die('Missing ticket ID parameter', 'Bad Request', ['response' => 400]); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public endpoint uses hash validation instead of nonces $ticketId = isset($_REQUEST['ticket_id']) ? absint($_REQUEST['ticket_id']) : 0; // Validate ticket ID is positive integer if ($ticketId <= 0) { wp_die('Invalid ticket ID', 'Bad Request', ['response' => 400]); } $ticket = Ticket::wherePublicIdentifier($ticketId)->first(); if (!$ticket) { $this->showInvalidPortalMessage(); } else { $this->redirectToTicketView($ticket); } } private function handleValidTicket() { // Validate required parameters exist and sanitize // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public endpoint uses hash validation instead of nonces if (!isset($_REQUEST['support_hash']) || !isset($_REQUEST['ticket_id'])) { wp_die('Missing required parameters', 'Bad Request', ['response' => 400]); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public endpoint uses hash validation instead of nonces $ticketHash = isset($_REQUEST['support_hash']) ? sanitize_text_field(wp_unslash($_REQUEST['support_hash'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public endpoint uses hash validation instead of nonces $ticketId = isset($_REQUEST['ticket_id']) ? absint($_REQUEST['ticket_id']) : 0; // Validate hash format (should be alphanumeric) if (empty($ticketHash) || !preg_match('/^[a-zA-Z0-9]+$/', $ticketHash)) { wp_die('Invalid ticket hash format', 'Bad Request', ['response' => 400]); } // Validate ticket ID is positive integer if ($ticketId <= 0) { wp_die('Invalid ticket ID', 'Bad Request', ['response' => 400]); } $ticket = Ticket::where('hash', $ticketHash) ->wherePublicIdentifier($ticketId) ->first(); if (!$ticket) { $this->showInvalidPortalMessage(); } elseif (get_current_user_id()) { // Only redirect if user is logged in (to clean up URL) $this->redirectToTicketView($ticket); } // If not logged in, let the page load normally with the hash parameters // The frontend will handle displaying the ticket based on the URL } private function showInvalidPortalMessage() { echo '