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 +61 -41 2.2.0 → 2.4.5 View file →
@@ -9,18 +9,18 @@
9 9 use FluentSupport\App\Services\Helper;
10 10 use FluentSupport\Framework\Support\Arr;
11 11
12 12 /**
13 - * CustomerController class for REST API
14 - * This class is responsible for getting data for all request related to customer
15 - * @package FluentSupport\App\Http\Controllers
16 - *
17 - * @version 1.0.0
13 + * Customer endpoints.
18 14 */
19 15 class CustomerController extends Controller
20 16 {
21 17 /**
22 - * index method will return the list of customers
18 + * Each id cascades into tickets, conversations and attachments, so the batch is capped.
19 + */
20 + const BULK_DELETE_LIMIT = 100;
21 +
22 + /**
23 23 * @param Request $request
24 24 * @param Customer $customer
25 25 * @return array
26 26 */
@@ -40,13 +40,13 @@
40 40 }
41 41
42 42
43 43 /**
44 - * getCustomer method will return individual customer information by customer id
45 - * This function will also get information about extra widgets, tickets and Fluent CRM
44 + * `with` can add widgets, tickets and FluentCRM data.
45 + *
46 46 * @param Request $request
47 47 * @param Customer $customer
48 - * @param $customer_id
48 + * @param int $customer_id
49 49 * @return array
50 50 */
51 51 public function getCustomer(Request $request, Customer $customer, $customer_id)
52 52 {
@@ -56,9 +56,8 @@
56 56 return $customer->getCustomer($customer_id, $with);
57 57 }
58 58
59 59 /**
60 - * Create method will create new customer
61 60 * @param Request $request
62 61 * @param Customer $customer
63 62 * @return array
64 63 * @throws \FluentSupport\Framework\Validator\ValidationException
@@ -64,9 +63,16 @@
64 63 * @throws \FluentSupport\Framework\Validator\ValidationException
65 64 */
66 65 public function create(Request $request, Customer $customer)
67 66 {
68 - // Define expected fields with their sanitizers
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);
73 + }
74 +
69 75 $fields = [
70 76 'id' => 'intval',
71 77 'customer_id' => 'intval',
72 78 'avatar' => 'esc_url_raw',
@@ -77,9 +83,8 @@
77 83 'email' => 'sanitize_email',
78 84 'first_name' => 'sanitize_text_field',
79 85 'last_name' => 'sanitize_text_field',
80 86 'title' => 'sanitize_text_field',
81 - 'user_id' => 'intval',
82 87 'remote_uid' => 'sanitize_text_field',
83 88 'status' => 'sanitize_text_field',
84 89 'address_line_1' => 'sanitize_textarea_field',
85 90 'address_line_2' => 'sanitize_textarea_field',
@@ -98,9 +103,8 @@
98 103 'email' => 'required|email|unique:fs_persons',
99 104 'first_name' => 'required',
100 105 'last_name' => 'nullable|string',
101 106 'title' => 'nullable|string',
102 - 'user_id' => 'nullable|integer',
103 107 'remote_uid' => 'nullable|string',
104 108 'status' => 'nullable|string',
105 109 'address_line_1' => 'nullable|string',
106 110 'address_line_2' => 'nullable|string',
@@ -119,18 +123,16 @@
119 123 ];
120 124 }
121 125
122 126 /**
123 - * update method will update existing customer by customer id
124 127 * @param Request $request
125 128 * @param Customer $customer
126 - * @param $customerId
129 + * @param int $customer_id
127 130 * @return array
128 131 * @throws \FluentSupport\Framework\Validator\ValidationException
129 132 */
130 133 public function update(Request $request, Customer $customer, $customer_id)
131 134 {
132 - // Sanitize only allowed fields and also sanitize any extra fields from hooks
133 135 $fields = [
134 136 'id' => 'intval',
135 137 'customer_id' => 'intval',
136 138 'avatar' => 'esc_url_raw',
@@ -194,12 +196,13 @@
194 196 }
195 197 }
196 198
197 199 /**
198 - * delete method will delete a customer and all tickets by that customer
200 + * Also deletes the customer's tickets.
201 + *
199 202 * @param Request $request
200 203 * @param Customer $customer
201 - * @param int $customerId
204 + * @param int $customer_id
202 205 * @return array
203 206 */
204 207 public function delete(Request $request, Customer $customer, $customer_id)
205 208 {
@@ -206,9 +209,10 @@
206 209 return $customer->deleteCustomer($customer_id);
207 210 }
208 211
209 212 /**
210 - * bulkDelete method will delete multiple customers and all their tickets
213 + * Also deletes the customers' tickets.
214 + *
211 215 * @param Request $request
212 216 * @param Customer $customer
213 217 * @return array
214 218 */
@@ -213,19 +217,19 @@
213 217 * @return array
214 218 */
215 219 public function bulkDelete(Request $request, Customer $customer)
216 220 {
217 - // Get and sanitize customer_ids before validation
218 221 $customerIds = $request->get('customer_ids', []);
219 222 $customerIds = is_array($customerIds) ? array_map('intval', $customerIds) : [];
220 223
221 - // Filter out any zero values (from invalid input)
222 224 $customerIds = array_filter($customerIds, function ($id) {
223 225 return $id > 0;
224 226 });
225 227
228 + $customerIds = array_values(array_unique($customerIds));
229 +
226 230 $this->validate(['customer_ids' => $customerIds], [
227 - 'customer_ids' => 'required|array|min:1',
231 + 'customer_ids' => 'required|array|min:1|max:' . self::BULK_DELETE_LIMIT,
228 232 'customer_ids.*' => 'required|integer|exists:fs_persons,id'
229 233 ]);
230 234
231 235 return $customer->bulkDeleteCustomers($customerIds);
@@ -231,11 +235,10 @@
231 235 return $customer->bulkDeleteCustomers($customerIds);
232 236 }
233 237
234 238 /**
235 - * addOrUpdateProfileImage method will update a customer avatar
236 - * For a successful upload it's required to send file object, customer id and the user type(customer)
237 239 * @param Request $request
240 + * @param AvatarUploder $avatarUploder
238 241 * @return array
239 242 */
240 243 public function addOrUpdateProfileImage(Request $request, AvatarUploder $avatarUploder)
241 244 {
@@ -250,17 +253,18 @@
250 253 }
251 254 }
252 255
253 256 /**
254 - * resetAvatar method will restore a customer avatar
255 - * For a successful upload it's required to send file object, customer id and the user type(customer)
256 - * @param Request $request
257 - * @param $id
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
258 261 * @return array
259 262 */
260 - public function resetAvatar(Customer $customer)
263 + public function resetAvatar($customer)
261 264 {
262 265 try {
266 + $customer = Customer::findOrFail((int) $customer);
263 267 $customer->restoreAvatar();
264 268
265 269 return [
266 270 'message' => __('Customer avatar reset to gravatar default', 'fluent-support'),
@@ -273,9 +277,14 @@
273 277 }
274 278
275 279 public function searchContact(Request $request)
276 280 {
277 - $search = $request->getSafe('search', 'sanitize_text_field');
281 + $search = trim($request->getSafe('search', 'sanitize_text_field'));
282 +
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 +
278 287 if (!$search) {
279 288 return $this->sendError([
280 289 'message' => __('Please provide search string', 'fluent-support')
281 290 ]);
@@ -282,9 +291,26 @@
282 291 }
283 292
284 293 $isEmail = is_email($search);
285 294
286 - // search the existing customers first
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 + }
301 +
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 + }
307 +
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.
287 313 if ($isEmail) {
288 314 $customers = Customer::select(['first_name', 'last_name', 'email', 'id', 'user_id'])
289 315 ->where('email', $search)
290 316 ->get();
@@ -289,9 +315,9 @@
289 315 ->where('email', $search)
290 316 ->get();
291 317 } else {
292 318 $customers = Customer::select(['first_name', 'last_name', 'email', 'id', 'user_id'])
293 - ->searchBy($search)
319 + ->searchBy($likeSearch)
294 320 ->limit(10)
295 321 ->get();
296 322 }
297 323
@@ -304,9 +330,8 @@
304 330 'search' => $search
305 331 ];
306 332 }
307 333
308 - // If FluentCRM exist then let's search for
309 334 if (defined('FLUENTCRM')) {
310 335
311 336 if ($isEmail) {
312 337 $contacts = \FluentCrm\App\Models\Subscriber::where('email', $search)
@@ -313,9 +338,9 @@
313 338 ->select(['first_name', 'last_name', 'email', 'id', 'user_id'])
314 339 ->get();
315 340 } else {
316 341
317 - $contacts = \FluentCrm\App\Models\Subscriber::searchBy($search)
342 + $contacts = \FluentCrm\App\Models\Subscriber::searchBy($likeSearch)
318 343 ->select(['first_name', 'last_name', 'email', 'id', 'user_id'])
319 344 ->limit(10)
320 345 ->get();
321 346 }
@@ -329,9 +354,8 @@
329 354 ];
330 355 }
331 356 }
332 357
333 - // let's search from user's database
334 358 $user_query = new \WP_User_Query(array('search' => $search, 'number' => 10));
335 359
336 360 $users = $user_query->get_results();
337 361
@@ -365,11 +389,10 @@
365 389
366 390 }
367 391
368 392 /**
369 - * Sanitize request data for given fields. Uses Request::getSafe for known fields
370 - * and falls back to sanitize_text_field for any other keys present in the raw request
371 - * (useful when hooks inject extra data).
393 + * Known fields use their mapped sanitizer. Any other keys (e.g. added by hooks)
394 + * get sanitize_text_field.
372 395 *
373 396 * @param Request $request
374 397 * @param array $fieldsMap associative array field => sanitizer callable name
375 398 * @return array
@@ -377,14 +400,12 @@
377 400 private function sanitizeRequestData(Request $request, array $fieldsMap)
378 401 {
379 402 $sanitized = [];
380 403
381 - // Use getSafe for known fields
382 404 foreach ($fieldsMap as $field => $sanitizer) {
383 405 $sanitized[$field] = $request->getSafe($field, $sanitizer);
384 406 }
385 407
386 - // Now sanitize any other incoming keys to avoid unsanitized data
387 408 $raw = $request->get();
388 409 foreach ($raw as $key => $value) {
389 410 if (array_key_exists($key, $sanitized)) {
390 411 continue;
@@ -392,9 +413,8 @@
392 413
393 414 if (is_array($value)) {
394 415 $sanitized[$key] = array_map('sanitize_text_field', $value);
395 416 } else {
396 - // Fallback sanitizer for unknown fields
397 417 $sanitized[$key] = is_string($value) ? sanitize_text_field($value) : $value;
398 418 }
399 419 }
400 420