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 +354 -119 1.5.4 → 2.4.5 View file →
@@ -1,188 +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\Includes\FileSystem;
11 -use FluentSupport\App\Services\ProfileInfoService;
12 -use FluentSupport\Framework\Request\Request;
13 10 use FluentSupport\Framework\Support\Arr;
14 11
12 +/**
13 + * Customer endpoints.
14 + */
15 15 class CustomerController extends Controller
16 16 {
17 - 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)
18 28 {
19 - $customersQuery = Customer::orderBy('id', 'DESC')
20 - ->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 + }
21 33
22 - if ($request->get('search')) {
23 - $customersQuery->searchBy($request->get('search'));
24 - }
34 + public function customerField (Request $request,Customer $customer, $customer_id) {
25 35
36 + $userID = $request->getSafe('user_id', 'intval');
37 + return[
38 + 'customerField' => $customer->getCustomerField($customer_id,$userID)
39 + ];
40 + }
26 41
27 - $status = $request->get('status');
28 - if ($status && $status != 'all') {
29 - $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);
30 73 }
31 74
32 - $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 + ];
33 99
34 - foreach ($customers as $customer) {
35 - $customer->total_tickets = $customer->getTicketCounts();
36 - $customer->total_responses = $customer->getResponseCounts();
37 - if ($customer->user_id) {
38 - $customer->user_profile = admin_url('user-edit.php?user_id=' . $customer->user_id);
39 - }
40 - }
100 + $data = $this->sanitizeRequestData($request, $fields);
41 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 +
42 120 return [
43 - 'customers' => $customers,
121 + 'message' => __('Customer has been added', 'fluent-support'),
122 + 'customer' => $customer->createCustomer($data)
44 123 ];
45 124 }
46 125
47 - 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)
48 134 {
49 - $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 + ];
50 160
51 - $data = [
52 - 'customer' => $customer
53 - ];
161 + $data = $this->sanitizeRequestData($request, $fields);
54 162
55 - $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 + ]);
56 181
57 - if (in_array('widgets', $with)) {
58 - $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);
59 196 }
197 + }
60 198
61 - if (in_array('tickets', $with)) {
62 - $data['tickets'] = Ticket::select(['id', 'title', 'status', 'customer_id', 'created_at'])
63 - ->where('customer_id', $customer->id)
64 - ->orderBy('id', 'DESC')
65 - ->limit(20)
66 - ->get();
67 - }
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 + }
68 211
69 - if(in_array('fluentcrm_profile', $with)) {
70 - $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 + );
71 253 }
254 + }
72 255
73 - 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();
74 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 + }
75 277 }
76 278
77 - public function create(Request $request)
279 + public function searchContact(Request $request)
78 280 {
79 - $data = $request->all();
80 - $this->validate($data, [
81 - 'email' => 'required|email|unique:fs_persons'
82 - ]);
281 + $search = trim($request->getSafe('search', 'sanitize_text_field'));
83 282
84 - $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));
85 286
86 - $data = Arr::only($data, (new Customer)->getFillable());
287 + if (!$search) {
288 + return $this->sendError([
289 + 'message' => __('Please provide search string', 'fluent-support')
290 + ]);
291 + }
87 292
88 - $user = get_user_by('email', $email);
293 + $isEmail = is_email($search);
89 294
90 - if ($user) {
91 - $data['user_id'] = $user->ID;
92 - if (empty($data['first_name'])) {
93 - $data['first_name'] = $user->first_name;
94 - }
95 - if (empty($data['last_name'])) {
96 - $data['last_name'] = $user->last_name;
97 - }
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 + ]);
98 300 }
99 301
100 - $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 + }
101 307
102 - return [
103 - 'message' => __('Customer has been added', 'fluent-support'),
104 - 'customer' => $customer
105 - ];
106 - }
308 + // Escape LIKE wildcards rather than strip them: '_' is valid in emails.
309 + global $wpdb;
310 + $likeSearch = $wpdb->esc_like($search);
107 311
108 - public function update(Request $request, $customerId)
109 - {
110 - $customer = Customer::findOrFail($customerId);
111 - $data = $request->all();
112 - $this->validate($data, [
113 - 'email' => 'required|email',
114 - 'first_name' => 'required'
115 - ]);
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 + }
116 323
117 - if ($otherCustomer = Customer::where('id', '!=', $customerId)->where('email', $data['email'])->first()) {
118 - return $this->sendError([
119 - 'message' => __('Another Customer has same email address', 'fluent-support'),
120 - 'errors' => [
121 - 'email' => [
122 - 'unique' => __('Email address has been assigned to other customer', 'fluent-support')
123 - ]
124 - ]
125 - ], 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 + ];
126 332 }
127 333
128 - $validKeys = (new Customer)->getFillable();
129 - unset($validKeys['hash']);
130 - unset($validKeys['user_id']);
334 + if (defined('FLUENTCRM')) {
131 335
132 - $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 {
133 341
134 - $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 + }
135 347
136 - if ($user) {
137 - $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 + }
138 356 }
139 357
140 - Customer::where('id', $customer->id)
141 - ->update($updateData);
358 + $user_query = new \WP_User_Query(array('search' => $search, 'number' => 10));
142 359
143 - return [
144 - 'message' => __('Customer has been updated', 'fluent-support'),
145 - 'customer' => Customer::findOrFail($customerId)
146 - ];
147 - }
360 + $users = $user_query->get_results();
148 361
149 - public function delete(Request $request, $customerId)
150 - {
151 - $customer = Customer::findOrFail($customerId);
362 + if ($users) {
363 + $formattedUsers = [];
152 364
153 - $tickets = Ticket::where('customer_id', $customer->id)->get();
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 + }
154 374
155 - foreach ($tickets as $ticket) {
156 - $ticket->deleteTicket();
375 + return [
376 + 'type' => 'search_result',
377 + 'provider' => 'wp_users',
378 + 'data' => $formattedUsers,
379 + 'is_email' => $isEmail
380 + ];
157 381 }
158 382
159 - $customer->delete();
160 383 return [
161 - 'message' => __('Customer Deleted Successfully', 'fluent-support')
384 + 'type' => 'none',
385 + 'provider' => 'none',
386 + 'data' => [],
387 + 'is_email' => $isEmail
162 388 ];
389 +
163 390 }
164 391
165 - public function addOrUpdateProfileImage(Request $request)
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)
166 401 {
167 - $customer = Customer::findOrFail($request->get('customer_id'));
402 + $sanitized = [];
168 403
169 - $uploadedImage = FileSystem::setSubDir('customer_avatars')->put($request->files());
404 + foreach ($fieldsMap as $field => $sanitizer) {
405 + $sanitized[$field] = $request->getSafe($field, $sanitizer);
406 + }
170 407
171 - if($avatar = $uploadedImage[0]['url']){
172 - $customer->avatar = $avatar;
173 - $customer->save();
408 + $raw = $request->get();
409 + foreach ($raw as $key => $value) {
410 + if (array_key_exists($key, $sanitized)) {
411 + continue;
412 + }
174 413
175 - return[
176 - 'message' => __('Profile picture has been updated successfully', 'fluent-support'),
177 - 'image' => $customer->avatar,
178 - 'customer' => $customer
179 - ];
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 + }
180 419 }
181 420
182 - else{
183 - return $this->sendError([
184 - 'message' => __('Something went wrong while updating the profile picture', 'fluent-support')
185 - ]);
186 - }
421 + return $sanitized;
187 422 }
188 423 }