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 +213 -40 1.10.4 → 2.4.5 View file →
@@ -3,22 +3,24 @@
3 3 namespace FluentSupport\App\Http\Controllers;
4 4
5 5 use FluentCrm\App\Models\Subscriber;
6 6 use FluentSupport\App\Models\Customer;
7 -use FluentSupport\Framework\Request\Request;
7 +use FluentSupport\Framework\Http\Request\Request;
8 8 use FluentSupport\App\Services\AvatarUploder;
9 +use FluentSupport\App\Services\Helper;
10 +use FluentSupport\Framework\Support\Arr;
9 11
10 12 /**
11 - * CustomerController class for REST API
12 - * This class is responsible for getting data for all request related to customer
13 - * @package FluentSupport\App\Http\Controllers
14 - *
15 - * @version 1.0.0
13 + * Customer endpoints.
16 14 */
17 15 class CustomerController extends Controller
18 16 {
19 17 /**
20 - * 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 + /**
21 23 * @param Request $request
22 24 * @param Customer $customer
23 25 * @return array
24 26 */
@@ -30,9 +32,9 @@
30 32 }
31 33
32 34 public function customerField (Request $request,Customer $customer, $customer_id) {
33 35
34 - $userID = intval($request->get('user_id'));
36 + $userID = $request->getSafe('user_id', 'intval');
35 37 return[
36 38 'customerField' => $customer->getCustomerField($customer_id,$userID)
37 39 ];
38 40 }
@@ -38,22 +40,24 @@
38 40 }
39 41
40 42
41 43 /**
42 - * getCustomer method will return individual customer information by customer id
43 - * This function will also get information about extra widgets, tickets and Fluent CRM
44 + * `with` can add widgets, tickets and FluentCRM data.
45 + *
44 46 * @param Request $request
45 47 * @param Customer $customer
46 - * @param $customer_id
48 + * @param int $customer_id
47 49 * @return array
48 50 */
49 51 public function getCustomer(Request $request, Customer $customer, $customer_id)
50 52 {
51 - return $customer->getCustomer($customer_id, $request->getSafe('with',null,[]));
53 + $with = $request->get('with', null);
54 + $with = is_array($with) ? array_map('sanitize_key', $with) : [];
55 +
56 + return $customer->getCustomer($customer_id, $with);
52 57 }
53 58
54 59 /**
55 - * Create method will create new customer
56 60 * @param Request $request
57 61 * @param Customer $customer
58 62 * @return array
59 63 * @throws \FluentSupport\Framework\Validator\ValidationException
@@ -59,31 +63,121 @@
59 63 * @throws \FluentSupport\Framework\Validator\ValidationException
60 64 */
61 65 public function create(Request $request, Customer $customer)
62 66 {
63 - $this->validate($request->get(), [
64 - 'email' => 'required|email|unique:fs_persons'
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 +
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 + ];
99 +
100 + $data = $this->sanitizeRequestData($request, $fields);
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',
65 118 ]);
66 119
67 120 return [
68 121 'message' => __('Customer has been added', 'fluent-support'),
69 - 'customer' => $customer->createCustomer($request->get())
122 + 'customer' => $customer->createCustomer($data)
70 123 ];
71 124 }
72 125
73 126 /**
74 - * update method will update existing customer by customer id
75 127 * @param Request $request
76 128 * @param Customer $customer
77 - * @param $customerId
129 + * @param int $customer_id
78 130 * @return array
79 131 * @throws \FluentSupport\Framework\Validator\ValidationException
80 132 */
81 133 public function update(Request $request, Customer $customer, $customer_id)
82 134 {
83 - $data = $this->validate($request->get(), [
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 + ];
160 +
161 + $data = $this->sanitizeRequestData($request, $fields);
162 +
163 + $data = $this->validate($data, [
84 164 'email' => 'required|email',
85 - 'first_name' => 'required'
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',
86 180 ]);
87 181
88 182 try {
89 183 return [
@@ -91,9 +185,9 @@
91 185 'customer' => $customer->updateCustomer($customer_id, $data)
92 186 ];
93 187 } catch (\Exception $e) {
94 188 return $this->sendError([
95 - 'message' => $e->getMessage(),
189 + 'message' => Helper::getSafeErrorMessage($e),
96 190 'errors' => [
97 191 'email' => [
98 192 'unique' => __('Email address has been assigned to other customer', 'fluent-support'),
99 193 ]
@@ -102,12 +196,13 @@
102 196 }
103 197 }
104 198
105 199 /**
106 - * delete method will delete a customer and all tickets by that customer
200 + * Also deletes the customer's tickets.
201 + *
107 202 * @param Request $request
108 203 * @param Customer $customer
109 - * @param int $customerId
204 + * @param int $customer_id
110 205 * @return array
111 206 */
112 207 public function delete(Request $request, Customer $customer, $customer_id)
113 208 {
@@ -114,13 +209,38 @@
114 209 return $customer->deleteCustomer($customer_id);
115 210 }
116 211
117 212 /**
118 - * addOrUpdateProfileImage method will update a customer avatar
119 - * For a successful upload it's required to send file object, customer id and the user type(customer)
213 + * Also deletes the customers' tickets.
214 + *
120 215 * @param Request $request
216 + * @param Customer $customer
121 217 * @return array
122 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 + */
123 243 public function addOrUpdateProfileImage(Request $request, AvatarUploder $avatarUploder)
124 244 {
125 245 try {
126 246 return $avatarUploder->addOrUpdateProfileImage($request->files(), $request->getSafe('customer_id', 'intval'), 'customer');
@@ -125,9 +245,9 @@
125 245 try {
126 246 return $avatarUploder->addOrUpdateProfileImage($request->files(), $request->getSafe('customer_id', 'intval'), 'customer');
127 247 } catch (\Exception $e) {
128 248 return $this->sendError([
129 - 'message' => $e->getMessage(),
249 + 'message' => Helper::getSafeErrorMessage($e),
130 250 ],
131 251 $e->getCode()
132 252 );
133 253 }
@@ -133,18 +253,19 @@
133 253 }
134 254 }
135 255
136 256 /**
137 - * resetAvatar method will restore a customer avatar
138 - * For a successful upload it's required to send file object, customer id and the user type(customer)
139 - * @param Request $request
140 - * @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
141 261 * @return array
142 262 */
143 - public function resetAvatar(Customer $customer, $customer_id)
263 + public function resetAvatar($customer)
144 264 {
145 265 try {
146 - $customer->restoreAvatar($customer, $customer_id);
266 + $customer = Customer::findOrFail((int) $customer);
267 + $customer->restoreAvatar();
147 268
148 269 return [
149 270 'message' => __('Customer avatar reset to gravatar default', 'fluent-support'),
150 271 ];
@@ -149,9 +270,9 @@
149 270 'message' => __('Customer avatar reset to gravatar default', 'fluent-support'),
150 271 ];
151 272 } catch (\Exception $e) {
152 273 return [
153 - 'message' => $e->getMessage()
274 + 'message' => Helper::getSafeErrorMessage($e)
154 275 ];
155 276 }
156 277 }
157 278
@@ -156,18 +277,40 @@
156 277 }
157 278
158 279 public function searchContact(Request $request)
159 280 {
160 - $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 +
161 287 if (!$search) {
162 288 return $this->sendError([
163 - 'message' => 'Please provide search string'
289 + 'message' => __('Please provide search string', 'fluent-support')
164 290 ]);
165 291 }
166 292
167 293 $isEmail = is_email($search);
168 294
169 - // 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.
170 313 if ($isEmail) {
171 314 $customers = Customer::select(['first_name', 'last_name', 'email', 'id', 'user_id'])
172 315 ->where('email', $search)
173 316 ->get();
@@ -172,9 +315,9 @@
172 315 ->where('email', $search)
173 316 ->get();
174 317 } else {
175 318 $customers = Customer::select(['first_name', 'last_name', 'email', 'id', 'user_id'])
176 - ->searchBy($search)
319 + ->searchBy($likeSearch)
177 320 ->limit(10)
178 321 ->get();
179 322 }
180 323
@@ -187,9 +330,8 @@
187 330 'search' => $search
188 331 ];
189 332 }
190 333
191 - // If FluentCRM exist then let's search for
192 334 if (defined('FLUENTCRM')) {
193 335
194 336 if ($isEmail) {
195 337 $contacts = \FluentCrm\App\Models\Subscriber::where('email', $search)
@@ -196,9 +338,9 @@
196 338 ->select(['first_name', 'last_name', 'email', 'id', 'user_id'])
197 339 ->get();
198 340 } else {
199 341
200 - $contacts = \FluentCrm\App\Models\Subscriber::searchBy($search)
342 + $contacts = \FluentCrm\App\Models\Subscriber::searchBy($likeSearch)
201 343 ->select(['first_name', 'last_name', 'email', 'id', 'user_id'])
202 344 ->limit(10)
203 345 ->get();
204 346 }
@@ -212,9 +354,8 @@
212 354 ];
213 355 }
214 356 }
215 357
216 - // let's search from user's database
217 358 $user_query = new \WP_User_Query(array('search' => $search, 'number' => 10));
218 359
219 360 $users = $user_query->get_results();
220 361
@@ -245,6 +386,38 @@
245 386 'data' => [],
246 387 'is_email' => $isEmail
247 388 ];
248 389
390 + }
391 +
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)
401 + {
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;
249 422 }
250 423 }