PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.4.5
Fluent Support – Helpdesk & Customer Support Ticket System v2.4.5
2.4.5 2.4.0 2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 All 69 releases
← All changes | app/Services/Tickets/ResponseService.php +122 -35 2.4.0 → 2.4.5 View file →
@@ -1,8 +1,9 @@
1 1 <?php
2 2
3 3 namespace FluentSupport\App\Services\Tickets;
4 4
5 +use FluentSupport\App\Models\Agent;
5 6 use FluentSupport\App\Models\Attachment;
6 7 use FluentSupport\App\Models\Conversation;
7 8 use FluentSupport\App\Services\Helper;
8 9 use FluentSupport\App\Services\Includes\UploadService;
@@ -10,12 +11,14 @@
10 11
11 12 class ResponseService
12 13 {
13 14 /**
14 - * createResponse method is responsible for create responses to ticket by agent or customer
15 - * @param $data
16 - * @param $person
17 - * @param $ticket
15 + * Add an agent or customer response to a ticket.
16 + *
17 + * @param array $data
18 + * @param \FluentSupport\App\Models\Person $person
19 + * @param \FluentSupport\App\Models\Ticket $ticket
20 + * @param bool $silently Skip the assignment/response/close hooks.
18 21 * @return array|false
19 22 */
20 23 public function createResponse($data, $person, $ticket, $silently = false)
21 24 {
@@ -25,9 +28,8 @@
25 28
26 29 $cc_emails = Arr::get($data, 'cc_emails', []);
27 30
28 31 $informationalReply = Arr::get($data, 'informational_reply', false);
29 - // Adding support for shortcode in agent response
30 32 if ($person->person_type == 'agent') {
31 33 $data['content'] = apply_filters('fluent_support/parse_smartcode_data', $data['content'], [
32 34 'customer' => $ticket->customer,
33 35 'agent' => $person
@@ -33,19 +35,12 @@
33 35 'agent' => $person
34 36 ]);
35 37 }
36 38
37 - if ($person->person_type == 'agent' && Arr::get($data, 'conversation_type', 'response') == 'response') {
38 - $data['content'] = $this->maybeAppendSignature($data['content'], $person);
39 - }
40 -
41 39 $convoType = Arr::get($data, 'conversation_type', 'response');
42 40
43 - // Request input is unslashed once at the boundary (the framework Request
44 - // cleaner for web requests, WP core for REST params), and API, MCP and
45 - // webhook callers pass decoded JSON that was never slashed. Sanitize only:
46 - // unslashing again would strip literal backslashes out of Windows paths,
47 - // regexes, JSON and shell snippets.
41 + // Input is already unslashed (or was never slashed, for API/MCP callers).
42 + // Don't unslash again or literal backslashes are lost.
48 43 $content = wp_kses_post($data['content']);
49 44 $resetWaitingSince = apply_filters('fluent_support/reset_waiting_since', true, $content);
50 45 $content = apply_filters('fluent_support/response_content_before_use_anywhere', $content);
51 46 $response = [
@@ -65,12 +60,11 @@
65 60
66 61 $createdResponse = \FluentSupport\App\Models\Conversation::create($response);
67 62
68 63 if (!empty($cc_emails)) {
69 - //Store cc emails in meta settings for the response
70 64 $createdResponse->updateSettingsValue('cc_email', $cc_emails);
71 65
72 - //Store all the carbon copy customers under the ticket
66 + // Keep a running list of every CC'd address on the ticket.
73 67 $existingCcEmails = $ticket->getSettingsValue('all_cc_email', []);
74 68 $newData = array_merge($existingCcEmails, $cc_emails);
75 69 $ticket->updateSettingsValue('all_cc_email', array_unique($newData));
76 70 }
@@ -143,9 +137,9 @@
143 137 $ticket->save();
144 138
145 139 Helper::tempImageMoveUploadDir($ticket->id, 'conversation', $createdResponse->id, $person->id);
146 140
147 - //If file upload failed to local during create response
141 + // Finalize files uploaded ahead of the response (status in-active).
148 142 if ($attachmentHashes = Arr::get($data, 'attachments', [])) {
149 143 $attachments = Attachment::where('ticket_id', $ticket->id)
150 144 ->whereIn('file_hash', $attachmentHashes)
151 145 ->where('status', 'in-active')
@@ -176,26 +170,61 @@
176 170 }
177 171 }
178 172
179 173 if ($silently) {
174 + // The caller fires the hooks itself (fireResponseHooks()), e.g. after
175 + // its transaction commits; 'closed' / 'agent_added' tell it what happened.
180 176 return [
181 - 'response' => $createdResponse,
182 - 'ticket' => $ticket,
183 - 'update_data' => $updateData
177 + 'response' => $createdResponse,
178 + 'ticket' => $ticket,
179 + 'update_data' => $updateData,
180 + 'closed' => $closed,
181 + 'agent_added' => $agentAdded,
182 + 'previous_agent_id' => $previousAgentId,
184 183 ];
185 184 }
186 185
186 + $updateData = $this->fireResponseHooks($createdResponse, $ticket, $person, $closed, $agentAdded, $previousAgentId, $updateData);
187 +
188 + return [
189 + 'response' => $createdResponse,
190 + 'ticket' => $ticket,
191 + 'update_data' => $updateData
192 + ];
193 + }
194 +
195 + /**
196 + * Fire the hooks createResponse() fires after a response is stored.
197 + *
198 + * Split out so a caller that wrapped createResponse(..., $silently = true) in
199 + * a transaction can fire them once the rows are committed: the customer
200 + * email must not leave for a reply that is about to be rolled back.
201 + *
202 + * @param Conversation $createdResponse
203 + * @param \FluentSupport\App\Models\Ticket $ticket
204 + * @param \FluentSupport\App\Models\Person $person
205 + * @param bool $closed The response closed the ticket.
206 + * @param bool $agentAdded The response assigned the ticket to $person.
207 + * @param int $previousAgentId
208 + * @param array $updateData
209 + * @return array $updateData, with 'agent' when the assignment hook fired.
210 + */
211 + public function fireResponseHooks($createdResponse, $ticket, $person, $closed, $agentAdded = false, $previousAgentId = 0, $updateData = [])
212 + {
213 + $convoType = $createdResponse->conversation_type;
214 +
187 215 if ($agentAdded) {
188 216 $assigner = Helper::getCurrentAgent();
189 217 $ticket->load('agent');
190 218
191 219 /*
192 - * Action on ticket assign to an agent
220 + * Fires when a ticket is assigned to an agent.
193 221 *
194 222 * @since v1.0.0
195 223 * @param object $person
196 224 * @param object $ticket
197 225 * @param object $assigner
226 + * @param int $previousAgentId
198 227 */
199 228 do_action('fluent_support/agent_assigned_to_ticket', $person, $ticket, $assigner, $previousAgentId);
200 229 $updateData['agent'] = $ticket->agent;
201 230 }
@@ -200,9 +229,9 @@
200 229 $updateData['agent'] = $ticket->agent;
201 230 }
202 231
203 232 /*
204 - * Action on conversation
233 + * Fires after a response or note is added.
205 234 *
206 235 * @since v1.0.0
207 236 * @param string $convoType
208 237 * @param string $personType
@@ -214,9 +243,9 @@
214 243
215 244
216 245 if ($closed) {
217 246 /*
218 - * Action on ticket close
247 + * Fires when a ticket is closed.
219 248 *
220 249 * @since v1.0.0
221 250 * @param object $ticket
222 251 * @param object $person
@@ -223,9 +252,9 @@
223 252 */
224 253 do_action('fluent_support/ticket_closed', $ticket, $person);
225 254
226 255 /*
227 - * Action on ticket close
256 + * Fires when a ticket is closed, per closer type.
228 257 *
229 258 * @since v1.0.0
230 259 * @param string $personType
231 260 * @param object $ticket
@@ -233,27 +262,85 @@
233 262 */
234 263 do_action('fluent_support/ticket_closed_by_' . $person->person_type, $ticket, $person);
235 264 }
236 265
237 - return [
238 - 'response' => $createdResponse,
239 - 'ticket' => $ticket,
240 - 'update_data' => $updateData
241 - ];
266 + return $updateData;
242 267 }
243 268
269 + /**
270 + * Publish a draft reply: it becomes a sent response and the customer is notified.
271 + * Shared by the admin "Approve" action and MCP's publish-draft-reply.
272 + *
273 + * @param \FluentSupport\App\Models\Ticket $ticket
274 + * @param \FluentSupport\App\Models\Conversation $response The draft.
275 + * @param \FluentSupport\App\Models\Person $person The agent publishing it.
276 + * @param string $content Final content, already sanitized.
277 + * @return \FluentSupport\App\Models\Conversation
278 + */
279 + public function publishDraft($ticket, $response, $person, $content)
280 + {
281 + $resetWaitingSince = apply_filters('fluent_support/reset_waiting_since', true, $content);
282 +
283 + $response->content = $content;
284 + $response->conversation_type = 'response';
285 + $response->created_at = current_time('mysql');
286 + $response->save();
287 +
288 + if ($person->person_type == 'agent' && $ticket->status == 'new') {
289 + $ticket->status = 'active';
290 + if ($ticket->created_at) {
291 + $ticket->first_response_time = strtotime(current_time('mysql')) - strtotime($ticket->created_at);
292 + } else {
293 + $ticket->first_response_time = 300;
294 + }
295 + }
296 +
297 + if ($resetWaitingSince) {
298 + $ticket->last_agent_response = current_time('mysql');
299 + $ticket->waiting_since = current_time('mysql');
300 + }
301 +
302 + $ticket->response_count += 1;
303 + $ticket->save();
304 +
305 + $author = $response->person_id ? Agent::find($response->person_id) : null;
306 + $sender = $author ?: $person;
307 +
308 + do_action('fluent_support/response_added_by_' . $sender->person_type, $response, $ticket, $sender);
309 +
310 + return $response;
311 + }
312 +
313 + /**
314 + * @deprecated 2.4.1 Replies no longer carry the signature. Email templates add it
315 + * with {{agent.signature}}, so the template decides the sign-off.
316 + */
244 317 public function maybeAppendSignature($content, $person)
245 318 {
246 - if ($person->getMeta('agent_signature_enabled', 'no') !== 'yes') {
319 + $signature = self::getAppendedSignature($person);
320 +
321 + if ($signature === '') {
247 322 return $content;
248 323 }
249 324
250 - $signature = $person->getMeta('agent_signature', '');
325 + return $content . '<div class="fs_agent_signature">' . $signature . '</div>';
326 + }
327 +
328 + /**
329 + * The signature appended to this agent's replies, or '' when none is.
330 + *
331 + * @param \FluentSupport\App\Models\Person $person
332 + * @return string HTML
333 + */
334 + public static function getAppendedSignature($person)
335 + {
336 + if (!$person || $person->getMeta('agent_signature_enabled', 'no') !== 'yes') {
337 + return '';
338 + }
339 +
340 + $signature = (string) $person->getMeta('agent_signature', '');
251 341 $hasText = !empty(trim(strip_tags($signature)));
252 342 $hasImage = (bool) preg_match('/<img[\s>]/i', $signature);
253 - if (!$hasText && !$hasImage) {
254 - return $content;
255 - }
256 343
257 - return $content . '<div class="fs_agent_signature">' . $signature . '</div>';
344 + return ($hasText || $hasImage) ? $signature : '';
258 345 }
259 346 }