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 +123 -31 2.3.1 → 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,15 +35,13 @@
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 - $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']);
44 44 $resetWaitingSince = apply_filters('fluent_support/reset_waiting_since', true, $content);
45 45 $content = apply_filters('fluent_support/response_content_before_use_anywhere', $content);
46 46 $response = [
47 47 'person_id' => $person->id,
@@ -60,12 +60,11 @@
60 60
61 61 $createdResponse = \FluentSupport\App\Models\Conversation::create($response);
62 62
63 63 if (!empty($cc_emails)) {
64 - //Store cc emails in meta settings for the response
65 64 $createdResponse->updateSettingsValue('cc_email', $cc_emails);
66 65
67 - //Store all the carbon copy customers under the ticket
66 + // Keep a running list of every CC'd address on the ticket.
68 67 $existingCcEmails = $ticket->getSettingsValue('all_cc_email', []);
69 68 $newData = array_merge($existingCcEmails, $cc_emails);
70 69 $ticket->updateSettingsValue('all_cc_email', array_unique($newData));
71 70 }
@@ -138,9 +137,9 @@
138 137 $ticket->save();
139 138
140 139 Helper::tempImageMoveUploadDir($ticket->id, 'conversation', $createdResponse->id, $person->id);
141 140
142 - //If file upload failed to local during create response
141 + // Finalize files uploaded ahead of the response (status in-active).
143 142 if ($attachmentHashes = Arr::get($data, 'attachments', [])) {
144 143 $attachments = Attachment::where('ticket_id', $ticket->id)
145 144 ->whereIn('file_hash', $attachmentHashes)
146 145 ->where('status', 'in-active')
@@ -171,26 +170,61 @@
171 170 }
172 171 }
173 172
174 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.
175 176 return [
176 - 'response' => $createdResponse,
177 - 'ticket' => $ticket,
178 - '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,
179 183 ];
180 184 }
181 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 +
182 215 if ($agentAdded) {
183 216 $assigner = Helper::getCurrentAgent();
184 217 $ticket->load('agent');
185 218
186 219 /*
187 - * Action on ticket assign to an agent
220 + * Fires when a ticket is assigned to an agent.
188 221 *
189 222 * @since v1.0.0
190 223 * @param object $person
191 224 * @param object $ticket
192 225 * @param object $assigner
226 + * @param int $previousAgentId
193 227 */
194 228 do_action('fluent_support/agent_assigned_to_ticket', $person, $ticket, $assigner, $previousAgentId);
195 229 $updateData['agent'] = $ticket->agent;
196 230 }
@@ -195,9 +229,9 @@
195 229 $updateData['agent'] = $ticket->agent;
196 230 }
197 231
198 232 /*
199 - * Action on conversation
233 + * Fires after a response or note is added.
200 234 *
201 235 * @since v1.0.0
202 236 * @param string $convoType
203 237 * @param string $personType
@@ -209,9 +243,9 @@
209 243
210 244
211 245 if ($closed) {
212 246 /*
213 - * Action on ticket close
247 + * Fires when a ticket is closed.
214 248 *
215 249 * @since v1.0.0
216 250 * @param object $ticket
217 251 * @param object $person
@@ -218,9 +252,9 @@
218 252 */
219 253 do_action('fluent_support/ticket_closed', $ticket, $person);
220 254
221 255 /*
222 - * Action on ticket close
256 + * Fires when a ticket is closed, per closer type.
223 257 *
224 258 * @since v1.0.0
225 259 * @param string $personType
226 260 * @param object $ticket
@@ -228,27 +262,85 @@
228 262 */
229 263 do_action('fluent_support/ticket_closed_by_' . $person->person_type, $ticket, $person);
230 264 }
231 265
232 - return [
233 - 'response' => $createdResponse,
234 - 'ticket' => $ticket,
235 - 'update_data' => $updateData
236 - ];
266 + return $updateData;
237 267 }
238 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 + */
239 317 public function maybeAppendSignature($content, $person)
240 318 {
241 - if ($person->getMeta('agent_signature_enabled', 'no') !== 'yes') {
319 + $signature = self::getAppendedSignature($person);
320 +
321 + if ($signature === '') {
242 322 return $content;
243 323 }
244 324
245 - $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', '');
246 341 $hasText = !empty(trim(strip_tags($signature)));
247 342 $hasImage = (bool) preg_match('/<img[\s>]/i', $signature);
248 - if (!$hasText && !$hasImage) {
249 - return $content;
250 - }
251 343
252 - return $content . '<div class="fs_agent_signature">' . $signature . '</div>';
344 + return ($hasText || $hasImage) ? $signature : '';
253 345 }
254 346 }