PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.3.2
Fluent Support – Helpdesk & Customer Support Ticket System v2.3.2
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
fluent-support / app / Services / Tickets / TicketService.php

TicketService.php in Fluent Support – Helpdesk & Customer Support Ticket System 2.3.2, at app/Services/Tickets/TicketService.php

309 lines 11.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentSupport\App\Services\Tickets;
4
5 use FluentSupport\App\Models\Attachment;
6 use FluentSupport\App\Models\Conversation;
7 use FluentSupport\App\Models\MailBox;
8 use FluentSupport\App\Models\Ticket;
9 use FluentSupport\App\Services\Helper;
10 use FluentSupport\App\Services\Includes\UploadService;
11 use FluentSupport\Framework\Support\Arr;
12
13 class TicketService
14 {
15 /**
16 * this function will set the ticket status as closed
17 * @param $ticket
18 * @param $person
19 * @param string $internalNote
20 * @param bool $silently
21 * @return mixed
22 */
23
24 public function close($ticket, $person, $internalNote = '', $silently = 'no')
25 {
26 if ($ticket->status != 'closed') {
27 $ticket->status = 'closed';
28 $ticket->resolved_at = current_time('mysql');
29 $ticket->closed_by = $person->id;
30 $ticket->total_close_time = current_time('timestamp') - strtotime($ticket->created_at);
31 $ticket->save();
32
33 if ('no' == $silently) {
34 do_action('fluent_support/ticket_closed', $ticket, $person);
35 do_action('fluent_support/ticket_closed_by_' . $person->person_type, $ticket, $person);
36 }
37
38 if (!$internalNote) {
39 $internalNote = __('Ticket has been closed', 'fluent-support');
40 }
41
42 //Keep track in conversation
43 Conversation::create([
44 'ticket_id' => $ticket->id,
45 'person_id' => $person->id,
46 'conversation_type' => 'internal_info',
47 'content' => $internalNote
48 ]);
49 }
50
51 return $ticket;
52 }
53
54 public function reopen($ticket, $person)
55 {
56 if ($ticket->status == 'closed') {
57 $ticket->status = 'active';
58 $ticket->waiting_since = current_time('mysql');
59 $ticket->resolved_at = null;
60 $ticket->save();
61
62 /*
63 * Action on ticket reopen
64 *
65 * @since v1.0.0
66 * @param object $ticket
67 * @param object $person
68 */
69 do_action('fluent_support/ticket_reopen', $ticket, $person);
70 do_action('fluent_support/ticket_reopen_by_' . $person->person_type, $ticket, $person);
71 Conversation::create([
72 'ticket_id' => $ticket->id,
73 'person_id' => $person->id,
74 'conversation_type' => 'internal_info',
75 'content' => __('Ticket has been reopened', 'fluent-support')
76 ]);
77 }
78
79 return $ticket;
80 }
81
82 public function onAgentChange($ticket, $person)
83 {
84 do_action('fluent_support/ticket_agent_change', $ticket, $person);
85 Conversation::create([
86 'ticket_id' => $ticket->id,
87 'person_id' => $person->id,
88 'conversation_type' => 'internal_info',
89 'content' => $ticket->agent->user_id !== $person->user_id ?
90 $person->full_name . __(' assigned ', 'fluent-support') . $ticket->agent->full_name . __(' in this ticket', 'fluent-support') :
91 $person->full_name .__( ' assign this ticket to self', 'fluent-support')
92 ]);
93
94 return $person;
95 }
96
97 /**
98 * This `addTicketAttachments` method is responsible for adding attachments to ticket
99 * @param array $data
100 * @param array $disabledFields
101 * @param \FluentSupport\App\Models\Ticket $ticket
102 * @param object $customer
103 * @return \FluentSupport\App\Models\Ticket $ticket
104 * @since 1.5.7
105 */
106 public static function addTicketAttachments($data, $disabledFields, $ticket, $customer)
107 {
108 Helper::tempImageMoveUploadDir($ticket->id, 'ticket-create', null, $customer->id);
109
110 if (($attachmentsHashes = Arr::get($data, 'attachments')) && !in_array('file_upload', $disabledFields)) {
111 $attachments = Attachment::whereIn('file_hash', $attachmentsHashes)
112 ->where('status', 'in-active')
113 ->get();
114
115 if ($attachments->isEmpty()) {
116 return $ticket;
117 }
118
119 $storageDriver = Helper::getUploadDriverKey();
120
121 if ($storageDriver != 'local') {
122 foreach ($attachments as $attachment) {
123 do_action_ref_array('fluent_support/finalize_file_upload_' . $storageDriver, [&$attachment, $ticket->id]);
124 }
125 }
126
127 foreach ($attachments as $attachment) {
128 if ($attachment->driver == 'local') {
129 $newFileInfo = UploadService::copyFileTicketFolder($attachment->file_path, $ticket->id);
130 if ($newFileInfo) {
131 $attachment->file_path = $newFileInfo['file_path'];
132 $attachment->full_url = $newFileInfo['url'];
133 }
134 }
135
136 $attachment->ticket_id = $ticket->id;
137 $attachment->person_id = $customer->id;
138 $attachment->status = 'active';
139 $attachment->save();
140 }
141
142 $ticket->load('attachments');
143 }
144
145 return $ticket;
146 }
147
148 /**
149 * Sanitize ticket data, create the ticket, handle attachments & custom fields, fire hooks.
150 * Shared by TicketController::createTicket and ProTicketService::handleSplitTicket.
151 *
152 * @param array $ticketData
153 * @param \FluentSupport\App\Models\Customer $customer
154 * @return \FluentSupport\App\Models\Ticket
155 */
156 public function storeTicket($ticketData, $customer)
157 {
158 if (empty($ticketData['mailbox_id'])) {
159 $mailbox = Helper::getDefaultMailBox();
160 if ($mailbox) {
161 $ticketData['mailbox_id'] = $mailbox->id;
162 }
163 } else {
164 $mailbox = MailBox::findOrFail($ticketData['mailbox_id']);
165 }
166
167 // The target box is caller-supplied. Returns [] when no agent is logged in,
168 // so cron/email piping/API callers are unaffected.
169 $restrictedBoxes = (new AgentTicketAccess())->getRestrictedMailboxIds();
170
171 // Reject an explicitly forbidden target up front, before any filter runs.
172 $this->assertMailboxAllowed($ticketData, $restrictedBoxes);
173
174 if (!empty($ticketData['product_id'])) {
175 $ticketData['product_source'] = 'local';
176 }
177
178 $ticketData['title'] = sanitize_text_field(wp_unslash($ticketData['title']));
179 $ticketData['content'] = wp_specialchars_decode(wp_unslash(wp_kses_post($ticketData['content'])));
180
181 if (!empty($ticketData['priority'])) {
182 $ticketData['priority'] = sanitize_text_field($ticketData['priority']);
183 }
184
185 if (!empty($ticketData['client_priority'])) {
186 $ticketData['client_priority'] = sanitize_text_field($ticketData['client_priority']);
187 }
188
189 $ticketData = apply_filters('fluent_support/create_ticket_data', $ticketData, $customer);
190
191 // Re-check the final target: a routing filter may have moved the ticket into a
192 // box this agent cannot reach. This is the value that actually gets persisted.
193 $this->assertMailboxAllowed($ticketData, $restrictedBoxes);
194
195 do_action('fluent_support/before_ticket_create', $ticketData, $customer);
196
197 $createdTicket = Ticket::create($ticketData);
198
199 $disabledFields = apply_filters('fluent_support/disabled_ticket_fields', []);
200 $disabledFields = array_diff($disabledFields, ['file_upload']);
201 self::addTicketAttachments($ticketData, $disabledFields, $createdTicket, $customer);
202
203 if (defined('FLUENTSUPPORTPRO') && !empty($ticketData['custom_fields'])) {
204 $createdTicket->syncCustomFields($ticketData['custom_fields']);
205 }
206
207 $agent = Helper::getAgentByUserId();
208 if ($agent) {
209 $isAgentInitiated = Arr::get($ticketData, 'agent_initiated') === 'yes';
210 $createdTicket->created_by = $agent->id;
211
212 if ($isAgentInitiated) {
213 // Skip all ticket creation emails for agent-initiated tickets
214 add_filter('fluent_support/should_send_notification', function ($shouldSend, $channel, $type) {
215 if ($channel === 'email' && in_array($type, ['ticket_created_email_to_customer', 'ticket_created_email_to_admin', 'ticket_created_by_agent_email_to_customer'])) {
216 return false;
217 }
218 return $shouldSend;
219 }, 10, 3);
220
221 $initializedMessage = $agent->full_name . __(' initialized this ticket', 'fluent-support');
222
223 $responseContent = apply_filters('fluent_support/parse_smartcode_data', $createdTicket->content, [
224 'customer' => $customer,
225 'agent' => $agent,
226 'ticket' => $createdTicket,
227 ]);
228
229 $responseContent = (new ResponseService())->maybeAppendSignature($responseContent, $agent);
230 $responseContent = wp_kses_post($responseContent);
231
232 // Agent response: actual ticket content — sends reply email to customer
233 $agentResponse = Conversation::create([
234 'ticket_id' => $createdTicket->id,
235 'person_id' => $agent->id,
236 'conversation_type' => 'response',
237 'content' => $responseContent
238 ]);
239
240 if ($attachmentHashes = Arr::get($ticketData, 'attachments', [])) {
241 Attachment::where('ticket_id', $createdTicket->id)
242 ->whereIn('file_hash', $attachmentHashes)
243 ->where('status', 'active')
244 ->update([
245 'person_id' => $agentResponse->person_id,
246 'conversation_id' => $agentResponse->id
247 ]);
248
249 $agentResponse->load('attachments');
250 }
251
252 do_action('fluent_support/agent_initiated_ticket_response', $agentResponse, $createdTicket, $agent);
253
254 $createdTicket->content = $initializedMessage;
255
256 }
257
258 $createdTicket->save();
259 }
260
261 do_action('fluent_support/ticket_created', $createdTicket, $customer);
262 do_action('fluent_support/ticket_created_behalf_of_customer', $createdTicket, $customer, $agent);
263
264 return $createdTicket;
265 }
266
267 /**
268 * Block ticket creation in a business box the acting agent is restricted from.
269 *
270 * $restrictedBoxes is resolved once per create and passed in, so both the
271 * pre-filter and post-filter checks read the same agent meta.
272 *
273 * @param array $ticketData
274 * @param array $restrictedBoxes Already int-normalized by AgentTicketAccess
275 * @return void
276 * @throws \Exception
277 */
278 protected function assertMailboxAllowed($ticketData, $restrictedBoxes)
279 {
280 if (empty($restrictedBoxes) || empty($ticketData['mailbox_id'])) {
281 return;
282 }
283
284 if (in_array((int) $ticketData['mailbox_id'], $restrictedBoxes, true)) {
285 throw new \Exception(
286 esc_html__('Sorry, you do not have permission to create a ticket in this business box', 'fluent-support')
287 );
288 }
289 }
290
291 // Callers own authorization — this also runs in system contexts (developer API, GDPR erasure, cron) with no current user.
292 public function deleteTicket($ticket, $agent = null)
293 {
294 if (!$agent) {
295 $agent = Helper::getAgentByUserId();
296 }
297
298 $ticketData = [
299 'id' => $ticket->id,
300 'title' => $ticket->title
301 ];
302
303 do_action('fluent_support/deleting_ticket', $ticket);
304 $ticket->delete();
305 do_action('fluent_support/ticket_deleted', $agent, $ticketData);
306 }
307
308 }
309