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 +365 -96 1.4.0 → 2.4.5 View file →
@@ -1,154 +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;
9 -use FluentSupport\App\Services\ProfileInfoService;
10 -use FluentSupport\Framework\Request\Request;
7 +use FluentSupport\Framework\Http\Request\Request;
8 +use FluentSupport\App\Services\AvatarUploder;
9 +use FluentSupport\App\Services\Helper;
11 10 use FluentSupport\Framework\Support\Arr;
12 11
12 +/**
13 + * Customer endpoints.
14 + */
13 15 class CustomerController extends Controller
14 16 {
15 - 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)
16 28 {
17 - $customersQuery = Customer::orderBy('id', 'DESC')
18 - ->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 + }
19 33
20 - if ($request->get('search')) {
21 - $customersQuery->searchBy($request->get('search'));
22 - }
34 + public function customerField (Request $request,Customer $customer, $customer_id) {
23 35
36 + $userID = $request->getSafe('user_id', 'intval');
37 + return[
38 + 'customerField' => $customer->getCustomerField($customer_id,$userID)
39 + ];
40 + }
24 41
25 - $status = $request->get('status');
26 - if ($status && $status != 'all') {
27 - $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);
28 73 }
29 74
30 - $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 + ];
31 99
32 - foreach ($customers as $customer) {
33 - $customer->total_tickets = $customer->getTicketCounts();
34 - $customer->total_responses = $customer->getResponseCounts();
35 - if ($customer->user_id) {
36 - $customer->user_profile = admin_url('user-edit.php?user_id=' . $customer->user_id);
37 - }
38 - }
100 + $data = $this->sanitizeRequestData($request, $fields);
39 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 +
40 120 return [
41 - 'customers' => $customers,
121 + 'message' => __('Customer has been added', 'fluent-support'),
122 + 'customer' => $customer->createCustomer($data)
42 123 ];
43 124 }
44 125
45 - 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)
46 134 {
47 - $customer = Customer::findOrFail($customerId);
48 -
49 - $data = [
50 - 'customer' => $customer
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',
51 159 ];
52 160
53 - $with = $request->get('with', []);
161 + $data = $this->sanitizeRequestData($request, $fields);
54 162
55 - if (in_array('widgets', $with)) {
56 - $data['widgets'] = ProfileInfoService::getProfileExtraWidgets($customer);
57 - }
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 + ]);
58 181
59 - if (in_array('tickets', $with)) {
60 - $data['tickets'] = Ticket::select(['id', 'title', 'status', 'customer_id', 'created_at'])
61 - ->where('customer_id', $customer->id)
62 - ->orderBy('id', 'DESC')
63 - ->limit(20)
64 - ->get();
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);
65 196 }
197 + }
66 198
67 - return $data;
68 -
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);
69 210 }
70 211
71 - public function create(Request $request)
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)
72 220 {
73 - $data = $request->all();
74 - $this->validate($data, [
75 - 'email' => 'required|email|unique:fs_persons'
76 - ]);
221 + $customerIds = $request->get('customer_ids', []);
222 + $customerIds = is_array($customerIds) ? array_map('intval', $customerIds) : [];
77 223
78 - $email = $data['email'];
224 + $customerIds = array_filter($customerIds, function ($id) {
225 + return $id > 0;
226 + });
79 227
80 - $data = Arr::only($data, (new Customer)->getFillable());
228 + $customerIds = array_values(array_unique($customerIds));
81 229
82 - $user = get_user_by('email', $email);
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 + ]);
83 234
84 - if ($user) {
85 - $data['user_id'] = $user->ID;
86 - if (empty($data['first_name'])) {
87 - $data['first_name'] = $user->first_name;
88 - }
89 - if (empty($data['last_name'])) {
90 - $data['last_name'] = $user->last_name;
91 - }
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 + );
92 253 }
254 + }
93 255
94 - $customer = Customer::create($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();
95 268
96 - return [
97 - 'message' => __('Customer has been added', 'fluent-support'),
98 - 'customer' => $customer
99 - ];
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 + }
100 277 }
101 278
102 - public function update(Request $request, $customerId)
279 + public function searchContact(Request $request)
103 280 {
104 - $customer = Customer::findOrFail($customerId);
105 - $data = $request->all();
106 - $this->validate($data, [
107 - 'email' => 'required|email',
108 - 'first_name' => 'required'
109 - ]);
281 + $search = trim($request->getSafe('search', 'sanitize_text_field'));
110 282
111 - if ($otherCustomer = Customer::where('id', '!=', $customerId)->where('email', $data['email'])->first()) {
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));
286 +
287 + if (!$search) {
112 288 return $this->sendError([
113 - 'message' => __('Another Customer has same email address', 'fluent-support'),
114 - 'errors' => [
115 - 'email' => [
116 - 'unique' => __('Email address has been assigned to other customer', 'fluent-support')
117 - ]
118 - ]
119 - ], 423);
289 + 'message' => __('Please provide search string', 'fluent-support')
290 + ]);
120 291 }
121 292
122 - $validKeys = (new Customer)->getFillable();
123 - unset($validKeys['hash']);
124 - unset($validKeys['user_id']);
293 + $isEmail = is_email($search);
125 294
126 - $updateData = Arr::only($data, $validKeys);
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 + ]);
300 + }
127 301
128 - $user = get_user_by('email', $data['email']);
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 + }
129 307
130 - if ($user) {
131 - $updateData['user_id'] = $user->ID;
308 + // Escape LIKE wildcards rather than strip them: '_' is valid in emails.
309 + global $wpdb;
310 + $likeSearch = $wpdb->esc_like($search);
311 +
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();
132 322 }
133 323
134 - Customer::where('id', $customer->id)
135 - ->update($updateData);
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 + ];
332 + }
136 333
334 + if (defined('FLUENTCRM')) {
335 +
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 {
341 +
342 + $contacts = \FluentCrm\App\Models\Subscriber::searchBy($likeSearch)
343 + ->select(['first_name', 'last_name', 'email', 'id', 'user_id'])
344 + ->limit(10)
345 + ->get();
346 + }
347 +
348 + if (!$contacts->isEmpty()) {
349 + return [
350 + 'type' => 'search_result',
351 + 'provider' => 'fluent_crm',
352 + 'data' => $contacts,
353 + 'is_email' => $isEmail
354 + ];
355 + }
356 + }
357 +
358 + $user_query = new \WP_User_Query(array('search' => $search, 'number' => 10));
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 +
137 383 return [
138 - 'message' => __('Customer has been updated', 'fluent-support'),
139 - 'customer' => Customer::findOrFail($customerId)
384 + 'type' => 'none',
385 + 'provider' => 'none',
386 + 'data' => [],
387 + 'is_email' => $isEmail
140 388 ];
389 +
141 390 }
142 391
143 - 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)
144 401 {
145 - $customer = Customer::findOrFail($customerId);
146 - Ticket::where('customer_id', $customerId)->delete();
147 - Attachment::where('person_id', $customerId)->delete();
148 - Conversation::where('person_id', $customerId)->delete();
149 - $customer->delete();
150 - return [
151 - 'message' => __('Customer Deleted Successfully', 'fluent-support')
152 - ];
402 + $sanitized = [];
403 +
404 + foreach ($fieldsMap as $field => $sanitizer) {
405 + $sanitized[$field] = $request->getSafe($field, $sanitizer);
406 + }
407 +
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 + }
419 + }
420 +
421 + return $sanitized;
153 422 }
154 423 }