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 +136 -23 1.10.5 → 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
@@ -35,9 +37,11 @@
35 37 }
36 38
37 39 $convoType = Arr::get($data, 'conversation_type', 'response');
38 40
39 - $content = wp_unslash(wp_kses_post($data['content']));
41 + // Input is already unslashed (or was never slashed, for API/MCP callers).
42 + // Don't unslash again or literal backslashes are lost.
43 + $content = wp_kses_post($data['content']);
40 44 $resetWaitingSince = apply_filters('fluent_support/reset_waiting_since', true, $content);
41 45 $content = apply_filters('fluent_support/response_content_before_use_anywhere', $content);
42 46 $response = [
43 47 'person_id' => $person->id,
@@ -56,12 +60,11 @@
56 60
57 61 $createdResponse = \FluentSupport\App\Models\Conversation::create($response);
58 62
59 63 if (!empty($cc_emails)) {
60 - //Store cc emails in meta settings for the response
61 64 $createdResponse->updateSettingsValue('cc_email', $cc_emails);
62 65
63 - //Store all the carbon copy customers under the ticket
66 + // Keep a running list of every CC'd address on the ticket.
64 67 $existingCcEmails = $ticket->getSettingsValue('all_cc_email', []);
65 68 $newData = array_merge($existingCcEmails, $cc_emails);
66 69 $ticket->updateSettingsValue('all_cc_email', array_unique($newData));
67 70 }
@@ -77,8 +80,9 @@
77 80 }
78 81 }
79 82
80 83 $agentAdded = false;
84 + $previousAgentId = (int) $ticket->agent_id;
81 85 $updateData = [];
82 86
83 87 if ($person->person_type == 'agent') {
84 88 if (!$ticket->agent_id && $convoType == 'response') {
@@ -131,11 +135,11 @@
131 135 }
132 136
133 137 $ticket->save();
134 138
135 - Helper::tempImageMoveUploadDir($ticket->id, 'conversation', $createdResponse->id);
139 + Helper::tempImageMoveUploadDir($ticket->id, 'conversation', $createdResponse->id, $person->id);
136 140
137 - //If file upload failed to local during create response
141 + // Finalize files uploaded ahead of the response (status in-active).
138 142 if ($attachmentHashes = Arr::get($data, 'attachments', [])) {
139 143 $attachments = Attachment::where('ticket_id', $ticket->id)
140 144 ->whereIn('file_hash', $attachmentHashes)
141 145 ->where('status', 'in-active')
@@ -166,33 +170,68 @@
166 170 }
167 171 }
168 172
169 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.
170 176 return [
171 - 'response' => $createdResponse,
172 - 'ticket' => $ticket,
173 - '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,
174 183 ];
175 184 }
176 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 +
177 215 if ($agentAdded) {
178 216 $assigner = Helper::getCurrentAgent();
179 217 $ticket->load('agent');
180 218
181 219 /*
182 - * Action on ticket assign to an agent
220 + * Fires when a ticket is assigned to an agent.
183 221 *
184 222 * @since v1.0.0
185 223 * @param object $person
186 224 * @param object $ticket
187 225 * @param object $assigner
226 + * @param int $previousAgentId
188 227 */
189 - do_action('fluent_support/agent_assigned_to_ticket', $person, $ticket, $assigner);
228 + do_action('fluent_support/agent_assigned_to_ticket', $person, $ticket, $assigner, $previousAgentId);
190 229 $updateData['agent'] = $ticket->agent;
191 230 }
192 231
193 232 /*
194 - * Action on conversation
233 + * Fires after a response or note is added.
195 234 *
196 235 * @since v1.0.0
197 236 * @param string $convoType
198 237 * @param string $personType
@@ -204,9 +243,9 @@
204 243
205 244
206 245 if ($closed) {
207 246 /*
208 - * Action on ticket close
247 + * Fires when a ticket is closed.
209 248 *
210 249 * @since v1.0.0
211 250 * @param object $ticket
212 251 * @param object $person
@@ -213,9 +252,9 @@
213 252 */
214 253 do_action('fluent_support/ticket_closed', $ticket, $person);
215 254
216 255 /*
217 - * Action on ticket close
256 + * Fires when a ticket is closed, per closer type.
218 257 *
219 258 * @since v1.0.0
220 259 * @param string $personType
221 260 * @param object $ticket
@@ -223,11 +262,85 @@
223 262 */
224 263 do_action('fluent_support/ticket_closed_by_' . $person->person_type, $ticket, $person);
225 264 }
226 265
227 - return [
228 - 'response' => $createdResponse,
229 - 'ticket' => $ticket,
230 - 'update_data' => $updateData
231 - ];
266 + return $updateData;
267 + }
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 + */
317 + public function maybeAppendSignature($content, $person)
318 + {
319 + $signature = self::getAppendedSignature($person);
320 +
321 + if ($signature === '') {
322 + return $content;
323 + }
324 +
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', '');
341 + $hasText = !empty(trim(strip_tags($signature)));
342 + $hasImage = (bool) preg_match('/<img[\s>]/i', $signature);
343 +
344 + return ($hasText || $hasImage) ? $signature : '';
232 345 }
233 346 }