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 +29 -47 2.4.0 → 2.4.5 View file →
@@ -9,23 +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 - * Maximum number of customers accepted by a single bulk-delete request.
18 + * Each id cascades into tickets, conversations and attachments, so the batch is capped.
23 19 */
24 20 const BULK_DELETE_LIMIT = 100;
25 21
26 22 /**
27 - * index method will return the list of customers
28 23 * @param Request $request
29 24 * @param Customer $customer
30 25 * @return array
31 26 */
@@ -45,13 +40,13 @@
45 40 }
46 41
47 42
48 43 /**
49 - * getCustomer method will return individual customer information by customer id
50 - * This function will also get information about extra widgets, tickets and Fluent CRM
44 + * `with` can add widgets, tickets and FluentCRM data.
45 + *
51 46 * @param Request $request
52 47 * @param Customer $customer
53 - * @param $customer_id
48 + * @param int $customer_id
54 49 * @return array
55 50 */
56 51 public function getCustomer(Request $request, Customer $customer, $customer_id)
57 52 {
@@ -61,9 +56,8 @@
61 56 return $customer->getCustomer($customer_id, $with);
62 57 }
63 58
64 59 /**
65 - * Create method will create new customer
66 60 * @param Request $request
67 61 * @param Customer $customer
68 62 * @return array
69 63 * @throws \FluentSupport\Framework\Validator\ValidationException
@@ -69,9 +63,16 @@
69 63 * @throws \FluentSupport\Framework\Validator\ValidationException
70 64 */
71 65 public function create(Request $request, Customer $customer)
72 66 {
73 - // 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 +
74 75 $fields = [
75 76 'id' => 'intval',
76 77 'customer_id' => 'intval',
77 78 'avatar' => 'esc_url_raw',
@@ -82,9 +83,8 @@
82 83 'email' => 'sanitize_email',
83 84 'first_name' => 'sanitize_text_field',
84 85 'last_name' => 'sanitize_text_field',
85 86 'title' => 'sanitize_text_field',
86 - 'user_id' => 'intval',
87 87 'remote_uid' => 'sanitize_text_field',
88 88 'status' => 'sanitize_text_field',
89 89 'address_line_1' => 'sanitize_textarea_field',
90 90 'address_line_2' => 'sanitize_textarea_field',
@@ -103,9 +103,8 @@
103 103 'email' => 'required|email|unique:fs_persons',
104 104 'first_name' => 'required',
105 105 'last_name' => 'nullable|string',
106 106 'title' => 'nullable|string',
107 - 'user_id' => 'nullable|integer',
108 107 'remote_uid' => 'nullable|string',
109 108 'status' => 'nullable|string',
110 109 'address_line_1' => 'nullable|string',
111 110 'address_line_2' => 'nullable|string',
@@ -124,18 +123,16 @@
124 123 ];
125 124 }
126 125
127 126 /**
128 - * update method will update existing customer by customer id
129 127 * @param Request $request
130 128 * @param Customer $customer
131 - * @param $customerId
129 + * @param int $customer_id
132 130 * @return array
133 131 * @throws \FluentSupport\Framework\Validator\ValidationException
134 132 */
135 133 public function update(Request $request, Customer $customer, $customer_id)
136 134 {
137 - // Sanitize only allowed fields and also sanitize any extra fields from hooks
138 135 $fields = [
139 136 'id' => 'intval',
140 137 'customer_id' => 'intval',
141 138 'avatar' => 'esc_url_raw',
@@ -199,12 +196,13 @@
199 196 }
200 197 }
201 198
202 199 /**
203 - * delete method will delete a customer and all tickets by that customer
200 + * Also deletes the customer's tickets.
201 + *
204 202 * @param Request $request
205 203 * @param Customer $customer
206 - * @param int $customerId
204 + * @param int $customer_id
207 205 * @return array
208 206 */
209 207 public function delete(Request $request, Customer $customer, $customer_id)
210 208 {
@@ -211,9 +209,10 @@
211 209 return $customer->deleteCustomer($customer_id);
212 210 }
213 211
214 212 /**
215 - * bulkDelete method will delete multiple customers and all their tickets
213 + * Also deletes the customers' tickets.
214 + *
216 215 * @param Request $request
217 216 * @param Customer $customer
218 217 * @return array
219 218 */
@@ -218,13 +217,11 @@
218 217 * @return array
219 218 */
220 219 public function bulkDelete(Request $request, Customer $customer)
221 220 {
222 - // Get and sanitize customer_ids before validation
223 221 $customerIds = $request->get('customer_ids', []);
224 222 $customerIds = is_array($customerIds) ? array_map('intval', $customerIds) : [];
225 223
226 - // Filter out any zero values (from invalid input)
227 224 $customerIds = array_filter($customerIds, function ($id) {
228 225 return $id > 0;
229 226 });
230 227
@@ -229,10 +226,8 @@
229 226 });
230 227
231 228 $customerIds = array_values(array_unique($customerIds));
232 229
233 - // Each id fans out into a full cascade delete (tickets, conversations,
234 - // attachments), so an unbounded batch means an unbounded request.
235 230 $this->validate(['customer_ids' => $customerIds], [
236 231 'customer_ids' => 'required|array|min:1|max:' . self::BULK_DELETE_LIMIT,
237 232 'customer_ids.*' => 'required|integer|exists:fs_persons,id'
238 233 ]);
@@ -240,11 +235,10 @@
240 235 return $customer->bulkDeleteCustomers($customerIds);
241 236 }
242 237
243 238 /**
244 - * addOrUpdateProfileImage method will update a customer avatar
245 - * For a successful upload it's required to send file object, customer id and the user type(customer)
246 239 * @param Request $request
240 + * @param AvatarUploder $avatarUploder
247 241 * @return array
248 242 */
249 243 public function addOrUpdateProfileImage(Request $request, AvatarUploder $avatarUploder)
250 244 {
@@ -259,14 +253,11 @@
259 253 }
260 254 }
261 255
262 256 /**
263 - * resetAvatar method will restore a customer avatar
264 - * For a successful upload it's required to send file object, customer id and the user type(customer)
257 + * No Customer type-hint: route-model binding runs before the policy, which would
258 + * let unauthenticated callers probe customer IDs. Resolve after auth.
265 259 *
266 - * No Customer type-hint here: route-model binding resolves inside the
267 - * permission callback, before any policy runs, which lets unauthenticated
268 - * callers probe customer ID existence (FS-PERM-001). Resolve after auth.
269 260 * @param int|string $customer
270 261 * @return array
271 262 */
272 263 public function resetAvatar($customer)
@@ -288,11 +279,10 @@
288 279 public function searchContact(Request $request)
289 280 {
290 281 $search = trim($request->getSafe('search', 'sanitize_text_field'));
291 282
292 - // '*' is a WP_User_Query wildcard and survives sanitize_text_field, so a
293 - // lone '*' would list every user on the site. Stripping it leaves
294 - // WP_User_Query doing an exact match.
283 + // '*' is a WP_User_Query wildcard that survives sanitize_text_field.
284 + // A lone '*' would list every user.
295 285 $search = trim(str_replace('*', '', $search));
296 286
297 287 if (!$search) {
298 288 return $this->sendError([
@@ -301,10 +291,9 @@
301 291 }
302 292
303 293 $isEmail = is_email($search);
304 294
305 - // Require a meaningful prefix so the endpoint can't be walked one letter
306 - // at a time. Emails are matched exactly, so they need no minimum.
295 + // Stops walking the list one letter at a time. Emails match exactly, so no minimum.
307 296 if (!$isEmail && mb_strlen($search) < 3) {
308 297 return $this->sendError([
309 298 'message' => __('Please provide at least 3 characters to search', 'fluent-support')
310 299 ]);
@@ -315,14 +304,13 @@
315 304 'message' => __('Too many contact searches. Please try again in a few minutes.', 'fluent-support')
316 305 ], 429);
317 306 }
318 307
319 - // '%' and '_' are LIKE wildcards for the customer and CRM scopes below.
320 - // Escape rather than strip: underscores are legitimate in emails.
308 + // Escape LIKE wildcards rather than strip them: '_' is valid in emails.
321 309 global $wpdb;
322 310 $likeSearch = $wpdb->esc_like($search);
323 311
324 - // search the existing customers first
312 + // Customers first, then FluentCRM contacts, then WP users.
325 313 if ($isEmail) {
326 314 $customers = Customer::select(['first_name', 'last_name', 'email', 'id', 'user_id'])
327 315 ->where('email', $search)
328 316 ->get();
@@ -342,9 +330,8 @@
342 330 'search' => $search
343 331 ];
344 332 }
345 333
346 - // If FluentCRM exist then let's search for
347 334 if (defined('FLUENTCRM')) {
348 335
349 336 if ($isEmail) {
350 337 $contacts = \FluentCrm\App\Models\Subscriber::where('email', $search)
@@ -367,9 +354,8 @@
367 354 ];
368 355 }
369 356 }
370 357
371 - // let's search from user's database
372 358 $user_query = new \WP_User_Query(array('search' => $search, 'number' => 10));
373 359
374 360 $users = $user_query->get_results();
375 361
@@ -403,11 +389,10 @@
403 389
404 390 }
405 391
406 392 /**
407 - * Sanitize request data for given fields. Uses Request::getSafe for known fields
408 - * and falls back to sanitize_text_field for any other keys present in the raw request
409 - * (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.
410 395 *
411 396 * @param Request $request
412 397 * @param array $fieldsMap associative array field => sanitizer callable name
413 398 * @return array
@@ -415,14 +400,12 @@
415 400 private function sanitizeRequestData(Request $request, array $fieldsMap)
416 401 {
417 402 $sanitized = [];
418 403
419 - // Use getSafe for known fields
420 404 foreach ($fieldsMap as $field => $sanitizer) {
421 405 $sanitized[$field] = $request->getSafe($field, $sanitizer);
422 406 }
423 407
424 - // Now sanitize any other incoming keys to avoid unsanitized data
425 408 $raw = $request->get();
426 409 foreach ($raw as $key => $value) {
427 410 if (array_key_exists($key, $sanitized)) {
428 411 continue;
@@ -430,9 +413,8 @@
430 413
431 414 if (is_array($value)) {
432 415 $sanitized[$key] = array_map('sanitize_text_field', $value);
433 416 } else {
434 - // Fallback sanitizer for unknown fields
435 417 $sanitized[$key] = is_string($value) ? sanitize_text_field($value) : $value;
436 418 }
437 419 }
438 420