PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / trunk
Fluent Support – Helpdesk & Customer Support Ticket System vtrunk
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 1.5.5 All 68 releases
fluent-support / app / Services / Tickets / TicketService.php

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

319 lines 12.3 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 $uploader = Helper::getCurrentAgent() ?: $customer;
112
113 $attachments = Attachment::whereIn('file_hash', $attachmentsHashes)
114 ->where('status', 'in-active')
115 ->whereNull('ticket_id')
116 ->where('person_id', $uploader->id)
117 ->get();
118
119 if ($attachments->isEmpty()) {
120 return $ticket;
121 }
122
123 $storageDriver = Helper::getUploadDriverKey();
124
125 if ($storageDriver != 'local') {
126 foreach ($attachments as $attachment) {
127 do_action_ref_array('fluent_support/finalize_file_upload_' . $storageDriver, [&$attachment, $ticket->id]);
128 }
129 }
130
131 foreach ($attachments as $attachment) {
132 if ($attachment->driver == 'local') {
133 $newFileInfo = UploadService::copyFileTicketFolder($attachment->file_path, $ticket->id);
134 if ($newFileInfo) {
135 $attachment->file_path = $newFileInfo['file_path'];
136 $attachment->full_url = $newFileInfo['url'];
137 }
138 }
139
140 $attachment->ticket_id = $ticket->id;
141 $attachment->person_id = $customer->id;
142 $attachment->status = 'active';
143 $attachment->save();
144 }
145
146 $ticket->load('attachments');
147 }
148
149 return $ticket;
150 }
151
152 /**
153 * Sanitize ticket data, create the ticket, handle attachments & custom fields, fire hooks.
154 * Shared by TicketController::createTicket and ProTicketService::handleSplitTicket.
155 *
156 * @param array $ticketData
157 * @param \FluentSupport\App\Models\Customer $customer
158 * @return \FluentSupport\App\Models\Ticket
159 */
160 public function storeTicket($ticketData, $customer)
161 {
162 if (empty($ticketData['mailbox_id'])) {
163 $mailbox = Helper::getDefaultMailBox();
164 if ($mailbox) {
165 $ticketData['mailbox_id'] = $mailbox->id;
166 }
167 } else {
168 $mailbox = MailBox::findOrFail($ticketData['mailbox_id']);
169 }
170
171 // The target box is caller-supplied. Returns [] when no agent is logged in,
172 // so cron/email piping/API callers are unaffected.
173 $restrictedBoxes = (new AgentTicketAccess())->getRestrictedMailboxIds();
174
175 // Reject an explicitly forbidden target up front, before any filter runs.
176 $this->assertMailboxAllowed($ticketData, $restrictedBoxes);
177
178 if (!empty($ticketData['product_id'])) {
179 $ticketData['product_source'] = 'local';
180 }
181
182 // Input is already unslashed at the request boundary; sanitize only, so
183 // literal backslashes in the title and body survive.
184 $ticketData['title'] = sanitize_text_field($ticketData['title']);
185 $ticketData['content'] = wp_specialchars_decode(wp_kses_post($ticketData['content']));
186
187 if (!empty($ticketData['priority'])) {
188 $ticketData['priority'] = sanitize_text_field($ticketData['priority']);
189 }
190
191 if (!empty($ticketData['client_priority'])) {
192 $ticketData['client_priority'] = sanitize_text_field($ticketData['client_priority']);
193 }
194
195 $ticketData = apply_filters('fluent_support/create_ticket_data', $ticketData, $customer);
196
197 // Re-check the final target: a routing filter may have moved the ticket into a
198 // box this agent cannot reach. This is the value that actually gets persisted.
199 $this->assertMailboxAllowed($ticketData, $restrictedBoxes);
200
201 do_action('fluent_support/before_ticket_create', $ticketData, $customer);
202
203 $createdTicket = Ticket::create($ticketData);
204
205 $disabledFields = apply_filters('fluent_support/disabled_ticket_fields', []);
206 $disabledFields = array_diff($disabledFields, ['file_upload']);
207 self::addTicketAttachments($ticketData, $disabledFields, $createdTicket, $customer);
208
209 if (defined('FLUENTSUPPORTPRO') && !empty($ticketData['custom_fields'])) {
210 $createdTicket->syncCustomFields($ticketData['custom_fields']);
211 }
212
213 $agent = Helper::getAgentByUserId();
214 if ($agent) {
215 $isAgentInitiated = Arr::get($ticketData, 'agent_initiated') === 'yes';
216 $createdTicket->created_by = $agent->id;
217
218 if ($isAgentInitiated) {
219 // Skip all ticket creation emails for agent-initiated tickets
220 add_filter('fluent_support/should_send_notification', function ($shouldSend, $channel, $type) {
221 if ($channel === 'email' && in_array($type, ['ticket_created_email_to_customer', 'ticket_created_email_to_admin', 'ticket_created_by_agent_email_to_customer'])) {
222 return false;
223 }
224 return $shouldSend;
225 }, 10, 3);
226
227 $initializedMessage = $agent->full_name . __(' initialized this ticket', 'fluent-support');
228
229 $responseContent = apply_filters('fluent_support/parse_smartcode_data', $createdTicket->content, [
230 'customer' => $customer,
231 'agent' => $agent,
232 'ticket' => $createdTicket,
233 ]);
234
235 $responseContent = (new ResponseService())->maybeAppendSignature($responseContent, $agent);
236 $responseContent = wp_kses_post($responseContent);
237
238 // Agent response: actual ticket content — sends reply email to customer
239 $agentResponse = Conversation::create([
240 'ticket_id' => $createdTicket->id,
241 'person_id' => $agent->id,
242 'conversation_type' => 'response',
243 'content' => $responseContent
244 ]);
245
246 if ($attachmentHashes = Arr::get($ticketData, 'attachments', [])) {
247 Attachment::where('ticket_id', $createdTicket->id)
248 ->whereIn('file_hash', $attachmentHashes)
249 ->where('status', 'active')
250 ->update([
251 'person_id' => $agentResponse->person_id,
252 'conversation_id' => $agentResponse->id
253 ]);
254
255 $agentResponse->load('attachments');
256 }
257
258 // The outreach email may render {{ticket.content}}, so expose the parsed
259 // body while the hook runs, then restore the log message that gets persisted.
260 $createdTicket->content = $responseContent;
261
262 do_action('fluent_support/agent_initiated_ticket_response', $agentResponse, $createdTicket, $agent);
263
264 $createdTicket->content = $initializedMessage;
265
266 }
267
268 $createdTicket->save();
269 }
270
271 do_action('fluent_support/ticket_created', $createdTicket, $customer);
272 do_action('fluent_support/ticket_created_behalf_of_customer', $createdTicket, $customer, $agent);
273
274 return $createdTicket;
275 }
276
277 /**
278 * Block ticket creation in a business box the acting agent is restricted from.
279 *
280 * $restrictedBoxes is resolved once per create and passed in, so both the
281 * pre-filter and post-filter checks read the same agent meta.
282 *
283 * @param array $ticketData
284 * @param array $restrictedBoxes Already int-normalized by AgentTicketAccess
285 * @return void
286 * @throws \Exception
287 */
288 protected function assertMailboxAllowed($ticketData, $restrictedBoxes)
289 {
290 if (empty($restrictedBoxes) || empty($ticketData['mailbox_id'])) {
291 return;
292 }
293
294 if (in_array((int) $ticketData['mailbox_id'], $restrictedBoxes, true)) {
295 throw new \Exception(
296 esc_html__('Sorry, you do not have permission to create a ticket in this business box', 'fluent-support')
297 );
298 }
299 }
300
301 // Callers own authorization — this also runs in system contexts (developer API, GDPR erasure, cron) with no current user.
302 public function deleteTicket($ticket, $agent = null)
303 {
304 if (!$agent) {
305 $agent = Helper::getAgentByUserId();
306 }
307
308 $ticketData = [
309 'id' => $ticket->id,
310 'title' => $ticket->title
311 ];
312
313 do_action('fluent_support/deleting_ticket', $ticket);
314 $ticket->delete();
315 do_action('fluent_support/ticket_deleted', $agent, $ticketData);
316 }
317
318 }
319