| @@ -66,9 +66,9 @@ | ||
| 66 | 66 | $businessSettings = (new \FluentSupport\App\Services\EmailNotification\Settings())->globalBusinessSettings(); |
| 67 | 67 | $maxFileSize = absint($businessSettings['max_file_size']); |
| 68 | 68 | |
| 69 | 69 | $portalSettings = [ |
| 70 | - 'support_products' => \FluentSupport\App\Models\Product::select(['id', 'title'])->get(), | |
| 70 | + 'support_products' => \FluentSupport\App\Models\Product::select(['id', 'title'])->orderedByTitle()->get(), | |
| 71 | 71 | 'customer_ticket_priorities' => Helper::customerTicketPriorities(), |
| 72 | 72 | 'has_file_upload' => !!Helper::ticketAcceptedFileMiles(), |
| 73 | 73 | 'has_rich_text_editor' => true, |
| 74 | 74 | 'max_file_size' => $maxFileSize, |
| @@ -883,9 +883,9 @@ | ||
| 883 | 883 | |
| 884 | 884 | $propName = $request->getSafe('prop_name', 'sanitize_text_field'); |
| 885 | 885 | $propValue = $request->getSafe('prop_value', 'sanitize_text_field'); |
| 886 | 886 | |
| 887 | - // FS-SEC-007: this generic endpoint may only touch a fixed set of | |
| 887 | + // This generic endpoint may only touch a fixed set of | |
| 888 | 888 | // ticket columns. Previously prop_name was assigned straight onto the |
| 889 | 889 | // model ($ticket->{$propName} = $propValue), letting a caller rewrite |
| 890 | 890 | // ownership, mailbox, privacy, hash, serial_number, created_by and |
| 891 | 891 | // other sensitive columns and bypass $fillable entirely. Every |
| @@ -962,9 +962,9 @@ | ||
| 962 | 962 | * The only ticket columns that may be changed through updateTicketProperty. |
| 963 | 963 | * This mirrors exactly what the admin UI edits (agent, title, mailbox, |
| 964 | 964 | * product, status and the two priority fields). Ownership, audit, |
| 965 | 965 | * public-identifier and other sensitive columns are intentionally absent |
| 966 | - * and must go through their dedicated workflows (FS-SEC-007). | |
| 966 | + * and must go through their dedicated workflows. | |
| 967 | 967 | * |
| 968 | 968 | * @return array |
| 969 | 969 | */ |
| 970 | 970 | protected function updatableTicketProperties() |
| @@ -983,9 +983,9 @@ | ||
| 983 | 983 | /** |
| 984 | 984 | * Validate and normalize a single ticket-property update. Each allowlisted |
| 985 | 985 | * property is checked against its own value domain and capability, so a |
| 986 | 986 | * caller can neither set an out-of-range value nor perform a change the UI |
| 987 | - * gates behind a stronger permission (FS-SEC-007). | |
| 987 | + * gates behind a stronger permission. | |
| 988 | 988 | * |
| 989 | 989 | * @param Ticket $ticket |
| 990 | 990 | * @param string $propName Already confirmed to be in the allowlist. |
| 991 | 991 | * @param string $propValue Raw (text-sanitized) value from the request. |
| @@ -1019,8 +1019,16 @@ | ||
| 1019 | 1019 | |
| 1020 | 1020 | if (!in_array($propValue, $allowedStatuses, true)) { |
| 1021 | 1021 | throw new \Exception(esc_html__('Invalid ticket status.', 'fluent-support'), 422); |
| 1022 | 1022 | } |
| 1023 | + | |
| 1024 | + // This route only assigns the column and saves, so closing or | |
| 1025 | + // reopening here would skip TicketService's closure fields, hooks and cleanup. | |
| 1026 | + // Use closeTicket() / reOpenTicket(); the status dropdown already does. | |
| 1027 | + if ($propValue === 'closed' || $ticket->status === 'closed') { | |
| 1028 | + throw new \Exception(esc_html__('Closing or reopening a ticket must use the dedicated close and re-open actions.', 'fluent-support'), 422); | |
| 1029 | + } | |
| 1030 | + | |
| 1023 | 1031 | return $propValue; |
| 1024 | 1032 | |
| 1025 | 1033 | case 'priority': |
| 1026 | 1034 | if (!array_key_exists($propValue, Helper::adminTicketPriorities())) { |
| @@ -1549,9 +1557,11 @@ | ||
| 1549 | 1557 | esc_html__('Sorry, you do not have permission to update this response.', 'fluent-support') |
| 1550 | 1558 | ); |
| 1551 | 1559 | } |
| 1552 | 1560 | |
| 1553 | - $content = wp_unslash(wp_kses_post($request->getSafe('content', 'wp_kses_post'))); | |
| 1561 | + // Request input is already unslashed at the boundary; unslashing again | |
| 1562 | + // would strip literal backslashes out of the edited reply. | |
| 1563 | + $content = wp_kses_post($request->getSafe('content', 'wp_kses_post')); | |
| 1554 | 1564 | $response->content = $content; |
| 1555 | 1565 | |
| 1556 | 1566 | if ($isDraft && !$isAuthor && $canApproveDraft) { |
| 1557 | 1567 | $response = $this->approveDraftConversation($ticket, $response, $agent, $content); |
| @@ -1593,9 +1603,9 @@ | ||
| 1593 | 1603 | $response = $this->approveDraftConversation( |
| 1594 | 1604 | $ticket, |
| 1595 | 1605 | $response, |
| 1596 | 1606 | $person, |
| 1597 | - wp_unslash(wp_kses_post($request->getSafe('content', 'wp_kses_post'))) | |
| 1607 | + wp_kses_post($request->getSafe('content', 'wp_kses_post')) | |
| 1598 | 1608 | ); |
| 1599 | 1609 | |
| 1600 | 1610 | return [ |
| 1601 | 1611 | 'message' => __('Draft response has been successfully approved.', 'fluent-support'), |