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/Http/Controllers/CustomerController.php +360 -100 1.5.3 → 2.4.5 View file →
@@ -1,163 +1,423 @@
1 1 <?php
2 2
3 3 namespace FluentSupport\App\Http\Controllers;
4 4
5 -use FluentSupport\App\Models\Attachment;
6 -use FluentSupport\App\Models\Conversation;
5 +use FluentCrm\App\Models\Subscriber;
7 6 use FluentSupport\App\Models\Customer;
8 -use FluentSupport\App\Models\Ticket;
7 +use FluentSupport\Framework\Http\Request\Request;
8 +use FluentSupport\App\Services\AvatarUploder;
9 9 use FluentSupport\App\Services\Helper;
10 -use FluentSupport\App\Services\ProfileInfoService;
11 -use FluentSupport\Framework\Request\Request;
12 10 use FluentSupport\Framework\Support\Arr;
13 11
12 +/**
13 + * Customer endpoints.
14 + */
14 15 class CustomerController extends Controller
15 16 {
16 - public function index(Request $request)
17 + /**
18 + * Each id cascades into tickets, conversations and attachments, so the batch is capped.
19 + */
20 + const BULK_DELETE_LIMIT = 100;
21 +
22 + /**
23 + * @param Request $request
24 + * @param Customer $customer
25 + * @return array
26 + */
27 + public function index(Request $request, Customer $customer)
17 28 {
18 - $customersQuery = Customer::orderBy('id', 'DESC')
19 - ->orderBy($request->get('order_by', 'id'), $request->get('order_type', 'ASC'));
29 + return [
30 + 'customers' => $customer->getCustomers($request->getSafe('search', 'sanitize_text_field'), $request->getSafe('status', 'sanitize_text_field')),
31 + ];
32 + }
20 33
21 - if ($request->get('search')) {
22 - $customersQuery->searchBy($request->get('search'));
23 - }
34 + public function customerField (Request $request,Customer $customer, $customer_id) {
24 35
36 + $userID = $request->getSafe('user_id', 'intval');
37 + return[
38 + 'customerField' => $customer->getCustomerField($customer_id,$userID)
39 + ];
40 + }
25 41
26 - $status = $request->get('status');
27 - if ($status && $status != 'all') {
28 - $customersQuery->filterByStatues([$status]);
42 +
43 + /**
44 + * `with` can add widgets, tickets and FluentCRM data.
45 + *
46 + * @param Request $request
47 + * @param Customer $customer
48 + * @param int $customer_id
49 + * @return array
50 + */
51 + public function getCustomer(Request $request, Customer $customer, $customer_id)
52 + {
53 + $with = $request->get('with', null);
54 + $with = is_array($with) ? array_map('sanitize_key', $with) : [];
55 +
56 + return $customer->getCustomer($customer_id, $with);
57 + }
58 +
59 + /**
60 + * @param Request $request
61 + * @param Customer $customer
62 + * @return array
63 + * @throws \FluentSupport\Framework\Validator\ValidationException
64 + */
65 + public function create(Request $request, Customer $customer)
66 + {
67 + // The WordPress user is linked through the email only, so a client user_id could
68 + // otherwise make the new record another account's portal identity.
69 + if ($request->get('user_id')) {
70 + return $this->sendError([
71 + 'message' => __('user_id is not accepted when creating a customer. The WordPress user is linked by the customer email.', 'fluent-support')
72 + ], 422);
29 73 }
30 74
31 - $customers = $customersQuery->paginate();
75 + $fields = [
76 + 'id' => 'intval',
77 + 'customer_id' => 'intval',
78 + 'avatar' => 'esc_url_raw',
79 + 'person_type' => 'sanitize_text_field',
80 + 'hash' => 'sanitize_text_field',
81 + 'description' => 'sanitize_text_field',
82 + 'photo' => 'esc_url_raw',
83 + 'email' => 'sanitize_email',
84 + 'first_name' => 'sanitize_text_field',
85 + 'last_name' => 'sanitize_text_field',
86 + 'title' => 'sanitize_text_field',
87 + 'remote_uid' => 'sanitize_text_field',
88 + 'status' => 'sanitize_text_field',
89 + 'address_line_1' => 'sanitize_textarea_field',
90 + 'address_line_2' => 'sanitize_textarea_field',
91 + 'city' => 'sanitize_text_field',
92 + 'state' => 'sanitize_text_field',
93 + 'zip' => 'sanitize_text_field',
94 + 'country' => 'sanitize_text_field',
95 + 'note' => 'sanitize_textarea_field',
96 + 'ip_address' => 'sanitize_text_field',
97 + 'last_ip_address' => 'sanitize_text_field',
98 + ];
32 99
33 - foreach ($customers as $customer) {
34 - $customer->total_tickets = $customer->getTicketCounts();
35 - $customer->total_responses = $customer->getResponseCounts();
36 - if ($customer->user_id) {
37 - $customer->user_profile = admin_url('user-edit.php?user_id=' . $customer->user_id);
38 - }
39 - }
100 + $data = $this->sanitizeRequestData($request, $fields);
40 101
102 + $data = $this->validate($data, [
103 + 'email' => 'required|email|unique:fs_persons',
104 + 'first_name' => 'required',
105 + 'last_name' => 'nullable|string',
106 + 'title' => 'nullable|string',
107 + 'remote_uid' => 'nullable|string',
108 + 'status' => 'nullable|string',
109 + 'address_line_1' => 'nullable|string',
110 + 'address_line_2' => 'nullable|string',
111 + 'city' => 'nullable|string',
112 + 'state' => 'nullable|string',
113 + 'zip' => 'nullable|string',
114 + 'country' => 'nullable|string',
115 + 'note' => 'nullable|string',
116 + 'ip_address' => 'nullable|string',
117 + 'last_ip_address' => 'nullable|string',
118 + ]);
119 +
41 120 return [
42 - 'customers' => $customers,
121 + 'message' => __('Customer has been added', 'fluent-support'),
122 + 'customer' => $customer->createCustomer($data)
43 123 ];
44 124 }
45 125
46 - public function getCustomer(Request $request, $customerId)
126 + /**
127 + * @param Request $request
128 + * @param Customer $customer
129 + * @param int $customer_id
130 + * @return array
131 + * @throws \FluentSupport\Framework\Validator\ValidationException
132 + */
133 + public function update(Request $request, Customer $customer, $customer_id)
47 134 {
48 - $customer = Customer::findOrFail($customerId);
135 + $fields = [
136 + 'id' => 'intval',
137 + 'customer_id' => 'intval',
138 + 'avatar' => 'esc_url_raw',
139 + 'person_type' => 'sanitize_text_field',
140 + 'hash' => 'sanitize_text_field',
141 + 'description' => 'sanitize_text_field',
142 + 'photo' => 'esc_url_raw',
143 + 'email' => 'sanitize_email',
144 + 'first_name' => 'sanitize_text_field',
145 + 'last_name' => 'sanitize_text_field',
146 + 'title' => 'sanitize_text_field',
147 + 'user_id' => 'intval',
148 + 'remote_uid' => 'sanitize_text_field',
149 + 'status' => 'sanitize_text_field',
150 + 'address_line_1' => 'sanitize_textarea_field',
151 + 'address_line_2' => 'sanitize_textarea_field',
152 + 'city' => 'sanitize_text_field',
153 + 'state' => 'sanitize_text_field',
154 + 'zip' => 'sanitize_text_field',
155 + 'country' => 'sanitize_text_field',
156 + 'note' => 'sanitize_textarea_field',
157 + 'ip_address' => 'sanitize_text_field',
158 + 'last_ip_address' => 'sanitize_text_field',
159 + ];
49 160
50 - $data = [
51 - 'customer' => $customer
52 - ];
161 + $data = $this->sanitizeRequestData($request, $fields);
53 162
54 - $with = $request->get('with', []);
163 + $data = $this->validate($data, [
164 + 'email' => 'required|email',
165 + 'first_name' => 'required',
166 + 'last_name' => 'nullable|string',
167 + 'title' => 'nullable|string',
168 + 'user_id' => 'nullable|integer',
169 + 'remote_uid' => 'nullable|string',
170 + 'status' => 'nullable|string',
171 + 'address_line_1' => 'nullable|string',
172 + 'address_line_2' => 'nullable|string',
173 + 'city' => 'nullable|string',
174 + 'state' => 'nullable|string',
175 + 'zip' => 'nullable|string',
176 + 'country' => 'nullable|string',
177 + 'note' => 'nullable|string',
178 + 'ip_address' => 'nullable|string',
179 + 'last_ip_address' => 'nullable|string',
180 + ]);
55 181
56 - if (in_array('widgets', $with)) {
57 - $data['widgets'] = ProfileInfoService::getProfileExtraWidgets($customer);
182 + try {
183 + return [
184 + 'message' => __('Customer has been updated', 'fluent-support'),
185 + 'customer' => $customer->updateCustomer($customer_id, $data)
186 + ];
187 + } catch (\Exception $e) {
188 + return $this->sendError([
189 + 'message' => Helper::getSafeErrorMessage($e),
190 + 'errors' => [
191 + 'email' => [
192 + 'unique' => __('Email address has been assigned to other customer', 'fluent-support'),
193 + ]
194 + ]
195 + ], 423);
58 196 }
197 + }
59 198
60 - if (in_array('tickets', $with)) {
61 - $data['tickets'] = Ticket::select(['id', 'title', 'status', 'customer_id', 'created_at'])
62 - ->where('customer_id', $customer->id)
63 - ->orderBy('id', 'DESC')
64 - ->limit(20)
65 - ->get();
66 - }
199 + /**
200 + * Also deletes the customer's tickets.
201 + *
202 + * @param Request $request
203 + * @param Customer $customer
204 + * @param int $customer_id
205 + * @return array
206 + */
207 + public function delete(Request $request, Customer $customer, $customer_id)
208 + {
209 + return $customer->deleteCustomer($customer_id);
210 + }
67 211
68 - if(in_array('fluentcrm_profile', $with)) {
69 - $data['fluentcrm_profile'] = Helper::getFluentCrmContactData($customer);
212 + /**
213 + * Also deletes the customers' tickets.
214 + *
215 + * @param Request $request
216 + * @param Customer $customer
217 + * @return array
218 + */
219 + public function bulkDelete(Request $request, Customer $customer)
220 + {
221 + $customerIds = $request->get('customer_ids', []);
222 + $customerIds = is_array($customerIds) ? array_map('intval', $customerIds) : [];
223 +
224 + $customerIds = array_filter($customerIds, function ($id) {
225 + return $id > 0;
226 + });
227 +
228 + $customerIds = array_values(array_unique($customerIds));
229 +
230 + $this->validate(['customer_ids' => $customerIds], [
231 + 'customer_ids' => 'required|array|min:1|max:' . self::BULK_DELETE_LIMIT,
232 + 'customer_ids.*' => 'required|integer|exists:fs_persons,id'
233 + ]);
234 +
235 + return $customer->bulkDeleteCustomers($customerIds);
236 + }
237 +
238 + /**
239 + * @param Request $request
240 + * @param AvatarUploder $avatarUploder
241 + * @return array
242 + */
243 + public function addOrUpdateProfileImage(Request $request, AvatarUploder $avatarUploder)
244 + {
245 + try {
246 + return $avatarUploder->addOrUpdateProfileImage($request->files(), $request->getSafe('customer_id', 'intval'), 'customer');
247 + } catch (\Exception $e) {
248 + return $this->sendError([
249 + 'message' => Helper::getSafeErrorMessage($e),
250 + ],
251 + $e->getCode()
252 + );
70 253 }
254 + }
71 255
72 - return $data;
256 + /**
257 + * No Customer type-hint: route-model binding runs before the policy, which would
258 + * let unauthenticated callers probe customer IDs. Resolve after auth.
259 + *
260 + * @param int|string $customer
261 + * @return array
262 + */
263 + public function resetAvatar($customer)
264 + {
265 + try {
266 + $customer = Customer::findOrFail((int) $customer);
267 + $customer->restoreAvatar();
73 268
269 + return [
270 + 'message' => __('Customer avatar reset to gravatar default', 'fluent-support'),
271 + ];
272 + } catch (\Exception $e) {
273 + return [
274 + 'message' => Helper::getSafeErrorMessage($e)
275 + ];
276 + }
74 277 }
75 278
76 - public function create(Request $request)
279 + public function searchContact(Request $request)
77 280 {
78 - $data = $request->all();
79 - $this->validate($data, [
80 - 'email' => 'required|email|unique:fs_persons'
81 - ]);
281 + $search = trim($request->getSafe('search', 'sanitize_text_field'));
82 282
83 - $email = $data['email'];
283 + // '*' is a WP_User_Query wildcard that survives sanitize_text_field.
284 + // A lone '*' would list every user.
285 + $search = trim(str_replace('*', '', $search));
84 286
85 - $data = Arr::only($data, (new Customer)->getFillable());
287 + if (!$search) {
288 + return $this->sendError([
289 + 'message' => __('Please provide search string', 'fluent-support')
290 + ]);
291 + }
86 292
87 - $user = get_user_by('email', $email);
293 + $isEmail = is_email($search);
88 294
89 - if ($user) {
90 - $data['user_id'] = $user->ID;
91 - if (empty($data['first_name'])) {
92 - $data['first_name'] = $user->first_name;
93 - }
94 - if (empty($data['last_name'])) {
95 - $data['last_name'] = $user->last_name;
96 - }
295 + // Stops walking the list one letter at a time. Emails match exactly, so no minimum.
296 + if (!$isEmail && mb_strlen($search) < 3) {
297 + return $this->sendError([
298 + 'message' => __('Please provide at least 3 characters to search', 'fluent-support')
299 + ]);
97 300 }
98 301
99 - $customer = Customer::create($data);
302 + if (Helper::hitRateLimit('fs_contact_search_' . get_current_user_id(), 60, 5 * MINUTE_IN_SECONDS)) {
303 + return $this->sendError([
304 + 'message' => __('Too many contact searches. Please try again in a few minutes.', 'fluent-support')
305 + ], 429);
306 + }
100 307
101 - return [
102 - 'message' => __('Customer has been added', 'fluent-support'),
103 - 'customer' => $customer
104 - ];
105 - }
308 + // Escape LIKE wildcards rather than strip them: '_' is valid in emails.
309 + global $wpdb;
310 + $likeSearch = $wpdb->esc_like($search);
106 311
107 - public function update(Request $request, $customerId)
108 - {
109 - $customer = Customer::findOrFail($customerId);
110 - $data = $request->all();
111 - $this->validate($data, [
112 - 'email' => 'required|email',
113 - 'first_name' => 'required'
114 - ]);
312 + // Customers first, then FluentCRM contacts, then WP users.
313 + if ($isEmail) {
314 + $customers = Customer::select(['first_name', 'last_name', 'email', 'id', 'user_id'])
315 + ->where('email', $search)
316 + ->get();
317 + } else {
318 + $customers = Customer::select(['first_name', 'last_name', 'email', 'id', 'user_id'])
319 + ->searchBy($likeSearch)
320 + ->limit(10)
321 + ->get();
322 + }
115 323
116 - if ($otherCustomer = Customer::where('id', '!=', $customerId)->where('email', $data['email'])->first()) {
117 - return $this->sendError([
118 - 'message' => __('Another Customer has same email address', 'fluent-support'),
119 - 'errors' => [
120 - 'email' => [
121 - 'unique' => __('Email address has been assigned to other customer', 'fluent-support')
122 - ]
123 - ]
124 - ], 423);
324 + if (!$customers->isEmpty()) {
325 + return [
326 + 'type' => 'search_result',
327 + 'provider' => 'fluent_support',
328 + 'data' => $customers,
329 + 'is_email' => $isEmail,
330 + 'search' => $search
331 + ];
125 332 }
126 333
127 - $validKeys = (new Customer)->getFillable();
128 - unset($validKeys['hash']);
129 - unset($validKeys['user_id']);
334 + if (defined('FLUENTCRM')) {
130 335
131 - $updateData = Arr::only($data, $validKeys);
336 + if ($isEmail) {
337 + $contacts = \FluentCrm\App\Models\Subscriber::where('email', $search)
338 + ->select(['first_name', 'last_name', 'email', 'id', 'user_id'])
339 + ->get();
340 + } else {
132 341
133 - $user = get_user_by('email', $data['email']);
342 + $contacts = \FluentCrm\App\Models\Subscriber::searchBy($likeSearch)
343 + ->select(['first_name', 'last_name', 'email', 'id', 'user_id'])
344 + ->limit(10)
345 + ->get();
346 + }
134 347
135 - if ($user) {
136 - $updateData['user_id'] = $user->ID;
348 + if (!$contacts->isEmpty()) {
349 + return [
350 + 'type' => 'search_result',
351 + 'provider' => 'fluent_crm',
352 + 'data' => $contacts,
353 + 'is_email' => $isEmail
354 + ];
355 + }
137 356 }
138 357
139 - Customer::where('id', $customer->id)
140 - ->update($updateData);
358 + $user_query = new \WP_User_Query(array('search' => $search, 'number' => 10));
141 359
360 + $users = $user_query->get_results();
361 +
362 + if ($users) {
363 + $formattedUsers = [];
364 +
365 + foreach ($users as $user) {
366 + $formattedUsers[] = [
367 + 'id' => $user->ID,
368 + 'first_name' => $user->first_name,
369 + 'last_name' => $user->last_name,
370 + 'user_id' => $user->ID,
371 + 'email' => $user->user_email
372 + ];
373 + }
374 +
375 + return [
376 + 'type' => 'search_result',
377 + 'provider' => 'wp_users',
378 + 'data' => $formattedUsers,
379 + 'is_email' => $isEmail
380 + ];
381 + }
382 +
142 383 return [
143 - 'message' => __('Customer has been updated', 'fluent-support'),
144 - 'customer' => Customer::findOrFail($customerId)
384 + 'type' => 'none',
385 + 'provider' => 'none',
386 + 'data' => [],
387 + 'is_email' => $isEmail
145 388 ];
389 +
146 390 }
147 391
148 - public function delete(Request $request, $customerId)
392 + /**
393 + * Known fields use their mapped sanitizer. Any other keys (e.g. added by hooks)
394 + * get sanitize_text_field.
395 + *
396 + * @param Request $request
397 + * @param array $fieldsMap associative array field => sanitizer callable name
398 + * @return array
399 + */
400 + private function sanitizeRequestData(Request $request, array $fieldsMap)
149 401 {
150 - $customer = Customer::findOrFail($customerId);
402 + $sanitized = [];
151 403
152 - $tickets = Ticket::where('customer_id', $customer->id)->get();
404 + foreach ($fieldsMap as $field => $sanitizer) {
405 + $sanitized[$field] = $request->getSafe($field, $sanitizer);
406 + }
153 407
154 - foreach ($tickets as $ticket) {
155 - $ticket->deleteTicket();
408 + $raw = $request->get();
409 + foreach ($raw as $key => $value) {
410 + if (array_key_exists($key, $sanitized)) {
411 + continue;
412 + }
413 +
414 + if (is_array($value)) {
415 + $sanitized[$key] = array_map('sanitize_text_field', $value);
416 + } else {
417 + $sanitized[$key] = is_string($value) ? sanitize_text_field($value) : $value;
418 + }
156 419 }
157 420
158 - $customer->delete();
159 - return [
160 - 'message' => __('Customer Deleted Successfully', 'fluent-support')
161 - ];
421 + return $sanitized;
162 422 }
163 423 }