PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.2.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.2.0
2.2.0 2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 All 43 releases
← All changes | app/Services/Helper.php +316 -14 1.40 → 2.2.0 View file →
@@ -53,8 +53,67 @@
53 53
54 54 return $data;
55 55 }
56 56
57 + /**
58 + * Normalize a nullable datetime value so task flows can safely persist NULL.
59 + *
60 + * @param mixed $value
61 + * @return mixed|null
62 + */
63 + public static function normalizeDateValue($value)
64 + {
65 + if ($value === null || is_bool($value)) {
66 + return null;
67 + }
68 +
69 + if (is_string($value)) {
70 + $value = trim($value);
71 +
72 + if ($value === '') {
73 + return null;
74 + }
75 +
76 + $normalizedValue = strtolower($value);
77 +
78 + if (in_array($normalizedValue, ['none', 'null'], true)) {
79 + return null;
80 + }
81 +
82 + if (in_array($value, ['0000-00-00', '0000-00-00 00:00:00'], true)) {
83 + return null;
84 + }
85 +
86 + if (preg_match('/^(\d{4})-/', $value, $matches) && (int) $matches[1] < 1900) {
87 + return null;
88 + }
89 +
90 + if (strtotime($value) === false) {
91 + return null;
92 + }
93 + }
94 +
95 + return $value;
96 + }
97 +
98 + /**
99 + * Normalize a list of nullable datetime keys inside an attribute array.
100 + *
101 + * @param array $data
102 + * @param array $dateKeys
103 + * @return array
104 + */
105 + public static function normalizeDates($data, $dateKeys = [])
106 + {
107 + foreach ($dateKeys as $dateKey) {
108 + if (array_key_exists($dateKey, $data)) {
109 + $data[$dateKey] = self::normalizeDateValue($data[$dateKey]);
110 + }
111 + }
112 +
113 + return $data;
114 + }
115 +
57 116 public static function sanitizeTask($data)
58 117 {
59 118 $fieldMaps = [
60 119 'title' => 'sanitize_text_field',
@@ -68,9 +127,10 @@
68 127 'lead_value' => 'doubleval',
69 128 'remind_at' => 'sanitize_text_field',
70 129 'scope' => 'sanitize_text_field',
71 130 'source' => 'sanitize_text_field',
72 - 'description' => 'wp_kses_post',
131 + 'source_id' => 'sanitize_text_field',
132 + 'description' => 'fluent_boards_sanitize_description',
73 133 'due_date' => 'sanitize_text_field',
74 134 'start_at' => 'sanitize_text_field',
75 135 'log_minutes' => 'sanitize_text_field',
76 136 'last_completed' => 'sanitize_text_field',
@@ -94,9 +154,9 @@
94 154 'board_id' => 'intval',
95 155 'title' => 'sanitize_text_field',
96 156 'parent_id' => 'intval',
97 157 'type' => 'sanitize_text_field',
98 - 'description' => 'wp_kses_post',
158 + 'description' => 'fluent_boards_sanitize_description',
99 159 'currency' => 'sanitize_text_field',
100 160 'image_url' => 'sanitize_url',
101 161 'is_auth_require' => 'intval',
102 162 'crm_contact_id' => 'intval',
@@ -102,8 +162,9 @@
102 162 'crm_contact_id' => 'intval',
103 163 'id' => 'sanitize_text_field',
104 164 'is_image' => 'rest_sanitize_boolean',
105 165 'color' => 'sanitize_text_field', // sanitize_hex_color doesn't work when color code is greater than 6 characters
166 + 'reset' => 'rest_sanitize_boolean',
106 167 'created_by' => 'intval',
107 168 ];
108 169
109 170 return self::sanitizeData($data, $fieldMaps);
@@ -135,10 +196,11 @@
135 196
136 197 public static function sanitizeLabel($data)
137 198 {
138 199 $fieldMaps = [
139 - 'bg_color' => 'sanitize_hex_color',
140 - 'color' => 'sanitize_hex_color',
200 + 'bg_color' => 'sanitize_text_field',
201 + 'color' => 'sanitize_text_field',
202 + 'color_preset' => 'sanitize_key',
141 203 'label' => 'sanitize_text_field',
142 204 'boardId' => 'intval',
143 205 'task_id' => 'intval',
144 206 'meta_value' => 'intval',
@@ -154,18 +216,48 @@
154 216 'stage' => 'sanitize_text_field',
155 217 'newPosition' => 'intval',
156 218 'priority' => 'sanitize_text_field',
157 219 'type' => 'sanitize_text_field',
220 + 'description' => 'fluent_boards_sanitize_description',
221 + 'group_id' => 'intval',
158 222 'board_id' => 'intval',
159 223 'created_by' => 'intval',
160 224 'due_date' => 'sanitize_text_field',
225 + 'due_at' => 'sanitize_text_field',
226 + 'started_at' => 'sanitize_text_field',
227 + 'reminder_type' => 'sanitize_text_field',
228 + 'remind_at' => 'sanitize_text_field',
229 + 'add_to_top' => 'rest_sanitize_boolean',
161 230 ];
162 231
163 - return self::sanitizeData($data, $fieldMaps);
232 + $data = self::sanitizeData($data, $fieldMaps);
233 +
234 + if (!empty($data['assignees']) && is_array($data['assignees'])) {
235 + $data['assignees'] = array_slice(array_filter(array_map('intval', $data['assignees'])), 0, 1);
236 + }
237 +
238 + if (!empty($data['labels']) && is_array($data['labels'])) {
239 + $data['labels'] = array_filter(array_map('intval', $data['labels']));
240 + }
241 +
242 + return $data;
164 243 }
165 244
245 + /** Store full custom-field messages in LONGTEXT while keeping legacy VARCHAR columns bounded. */
166 246 public static function createActivity($data)
167 247 {
248 + if (!empty($data['settings']['custom_field_id'])) {
249 + $payload = [];
250 + foreach (['column', 'old_value', 'new_value', 'description'] as $key) {
251 + $payload[$key] = $data[$key] ?? null;
252 + }
253 + $data['settings']['custom_field_activity_payload'] = true;
254 + $data['description'] = wp_json_encode($payload);
255 + $data['column'] = Constant::ACTIVITY_CUSTOM_FIELD;
256 + $data['old_value'] = null;
257 + $data['new_value'] = null;
258 + }
259 +
168 260 return Activity::create($data);
169 261 }
170 262
171 263 public static function sanitizeTaskMeta($data)
@@ -256,8 +348,9 @@
256 348 }
257 349 return fluent_boards_page_url() . 'boards/' . $boardId;
258 350 }
259 351
352 + /** Format a CRM contact, avoiding a database lookup for unassociated tasks. */
260 353 public static function crm_contact($id)
261 354 {
262 355 if (!defined('FLUENTCRM')) {
263 356 return '';
@@ -262,8 +355,12 @@
262 355 if (!defined('FLUENTCRM')) {
263 356 return '';
264 357 }
265 358
359 + if (!$id) {
360 + return null;
361 + }
362 +
266 363 $contact = \FluentCrm\App\Models\Subscriber::with(['tags', 'lists'])->find($id);
267 364
268 365 if (!$contact) {
269 366 return null;
@@ -323,20 +420,43 @@
323 420 }
324 421
325 422 public static function sanitizeUserCollections($users)
326 423 {
424 + if (empty($users)) {
425 + return $users;
426 + }
427 +
428 + foreach ($users as $key => $user) {
429 + if (is_object($user) && isset($user->pivot)) {
430 + $settings = maybe_unserialize($user->pivot->settings);
431 + $user->role = Arr::get($settings, 'is_admin')
432 + ? 'Admin'
433 + : (Arr::has($settings, 'is_viewer_only') && Arr::get($settings, 'is_viewer_only')
434 + ? 'Viewer'
435 + : 'Member');
436 + }
437 + }
438 +
327 439 if (current_user_can('list_users')) {
328 440 return $users;
329 441 }
330 442
331 - if ($users) {
443 + if (is_object($users) && method_exists($users, 'makeHidden')) {
332 444 $users->makeHidden(['user_email', 'user_nicename', 'user_registered', 'user_url', 'user_status']);
445 + } elseif (is_array($users)) {
446 + foreach ($users as &$user) {
447 + if (is_array($user)) {
448 + unset($user['user_email'], $user['user_nicename'], $user['user_registered'], $user['user_url'], $user['user_status']);
449 + }
450 + }
451 + unset($user);
333 452 }
334 453
335 454 return $users;
336 455 }
337 456
338 - public static function sanitizeUsersArray($users, $boardId = null)
457 + // Callers formatting multiple lists may supply a resolved board-manager result.
458 + public static function sanitizeUsersArray($users, $boardId = null, $isBoardManager = null)
339 459 {
340 460 if (current_user_can('list_users')) {
341 461 return $users;
342 462 }
@@ -342,13 +462,28 @@
342 462 }
343 463
344 464 $sanitizedUsers = [];
345 465
346 - if(!PermissionManager::isBoardManager($boardId)) //Todo: may create permission security issue, will be modified later
466 + if (!($isBoardManager ?? PermissionManager::isBoardManager($boardId)))
347 467 {
468 + $currentUser = wp_get_current_user();
469 + if($currentUser && isset($currentUser->user_email)){
470 + $currentUserEmail = $currentUser->user_email;
471 + }
348 472 foreach ($users as $user) {
349 - unset($user->user_email);
350 - unset($user->email);
473 +
474 + if($user['email'] === $currentUserEmail){
475 + $sanitizedUsers[] = $user;
476 + continue;
477 + }
478 +
479 + if (isset($user['email'])) {
480 + $user['email'] = self::obfuscateEmail($user['email']);
481 + }
482 + if (isset($user['user_email'])) {
483 + $user['user_email'] = self::obfuscateEmail($user['user_email']);
484 + }
485 +
351 486 $sanitizedUsers[] = $user;
352 487 }
353 488 } else {
354 489 foreach ($users as $user) {
@@ -358,22 +493,52 @@
358 493
359 494 return $sanitizedUsers;
360 495 }
361 496
497 + public static function obfuscateEmail($email)
498 + {
499 + if (!is_string($email) || filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
500 + return $email; // Not a valid email, return as is
501 + }
502 +
503 + list($name, $domain) = explode('@', $email, 2);
504 +
505 + // Local part: show first 3 chars, then fixed ****
506 + $visibleLocal = substr($name, 0, 3);
507 + $maskedLocal = $visibleLocal . '****';
508 +
509 + // Domain: mask the main label to **** + last 2 chars, keep rest (TLDs) intact
510 + $domainParts = explode('.', $domain);
511 + $mainLabel = $domainParts[0] ?? '';
512 + $tail = strlen($mainLabel) >= 2 ? substr($mainLabel, -2) : $mainLabel;
513 + $domainParts[0] = '****' . $tail; // e.g., example.com -> ****le.com
514 + $maskedDomain = implode('.', $domainParts);
515 +
516 + return $maskedLocal . '@' . $maskedDomain;
517 + }
518 +
362 519 public static function getPriorityOptions()
363 520 {
364 521 return [
365 522 [
366 - 'id' => 'low',
367 - 'title' => 'Low'
523 + 'id' => '',
524 + 'title' => 'No priority'
368 525 ],
369 526 [
527 + 'id' => 'urgent',
528 + 'title' => 'Urgent'
529 + ],
530 + [
531 + 'id' => 'high',
532 + 'title' => 'High'
533 + ],
534 + [
370 535 'id' => 'medium',
371 536 'title' => 'Medium'
372 537 ],
373 538 [
374 - 'id' => 'high',
375 - 'title' => 'High'
539 + 'id' => 'low',
540 + 'title' => 'Low'
376 541 ],
377 542 ];
378 543 }
379 544
@@ -451,8 +616,9 @@
451 616 'repeat_in' => 'intval',
452 617 'repeat_type' => 'sanitize_text_field',
453 618 'repeat_when_complete' => 'intval',
454 619 'selected_month' => 'sanitize_text_field',
620 + 'selected_stage' => 'intval',
455 621 'board_id' => 'intval',
456 622 'time' => 'sanitize_text_field',
457 623 'time_zone' => 'sanitize_text_field',
458 624 'next_repeat_date' => 'sanitize_text_field',
@@ -459,6 +625,142 @@
459 625 'repeat_in_month_type' => 'sanitize_text_field',
460 626 ];
461 627
462 628 return self::sanitizeData($data, $fieldMaps);
629 + }
630 +
631 + /**
632 + * Sanitize the author snapshot supplied by an external task integration.
633 + *
634 + * @param mixed $author
635 + * @return array
636 + */
637 + private static function sanitizeExternalTaskAuthor($author)
638 + {
639 + if (!is_array($author)) {
640 + return [];
641 + }
642 +
643 + return array_filter([
644 + 'name' => sanitize_text_field($author['name'] ?? ''),
645 + 'email' => sanitize_email($author['email'] ?? ''),
646 + 'photo' => esc_url_raw($author['photo'] ?? ''),
647 + ]);
648 + }
649 +
650 + public static function sanitizeTaskForWebHook($data)
651 + {
652 + $fieldMaps = [
653 + 'title' => 'sanitize_text_field',
654 + 'board_id' => 'intval',
655 + 'parent_id' => 'intval',
656 + 'crm_contact_id' => 'intval',
657 + 'type' => 'sanitize_text_field',
658 + 'stage' => 'sanitize_text_field',
659 + 'reminder_type' => 'sanitize_text_field',
660 + 'priority' => 'sanitize_text_field',
661 + 'lead_value' => 'doubleval',
662 + 'remind_at' => 'sanitize_text_field',
663 + 'scope' => 'sanitize_text_field',
664 + 'source' => 'sanitize_text_field',
665 + 'source_id' => 'sanitize_text_field',
666 + 'description' => 'fluent_boards_sanitize_description',
667 + 'due_date' => 'sanitize_text_field',
668 + 'start_at' => 'sanitize_text_field',
669 + 'log_minutes' => 'sanitize_text_field',
670 + 'last_completed' => 'sanitize_text_field',
671 + 'is_archived' => 'intval',
672 + 'previous_stage' => 'sanitize_text_field',
673 + 'new_stage' => 'sanitize_text_field',
674 + 'new_index' => 'intval',
675 + 'old_index' => 'intval',
676 + 'new_board_id' => 'intval',
677 + 'position' => 'intval'
678 +
679 + ];
680 +
681 + $data = self::sanitizeData($data, $fieldMaps);
682 +
683 + if (isset($data['settings']) && is_array($data['settings']) && isset($data['settings']['author'])) {
684 + $data['settings'] = [
685 + 'author' => self::sanitizeExternalTaskAuthor($data['settings']['author']),
686 + ];
687 + } else {
688 + unset($data['settings']);
689 + }
690 +
691 + return $data;
692 + }
693 +
694 +
695 + public static function taskReminderTypes()
696 + {
697 + $allowedTypes = [
698 + '30_minutes_before' => __('30 minutes before', 'fluent-boards'),
699 + '1_hour_before' => __('1 hour before', 'fluent-boards'),
700 + '2_hours_before' => __('2 hours before', 'fluent-boards'),
701 + '1_day_before' => __('1 day before', 'fluent-boards'),
702 + '2_days_before' => __('2 days before', 'fluent-boards'),
703 + '1_week_before' => __('1 week before', 'fluent-boards'),
704 + ];
705 +
706 + $allowedTypes = apply_filters('fluent_boards/task_reminder_types', $allowedTypes);
707 +
708 + return $allowedTypes;
709 + }
710 +
711 + public static function translateActivities($activities)
712 + {
713 + $actionTranslations = [
714 + 'changed' => __('changed', 'fluent-boards'),
715 + 'updated' => __('updated', 'fluent-boards'),
716 + 'added' => __('added', 'fluent-boards'),
717 + 'removed' => __('removed', 'fluent-boards'),
718 + 'created' => __('created', 'fluent-boards'),
719 + 'closed' => __('closed', 'fluent-boards'),
720 + 'reopened' => __('reopened', 'fluent-boards'),
721 + 'joined' => __('joined', 'fluent-boards'),
722 + 'left' => __('left', 'fluent-boards'),
723 + 'cloned' => __('cloned', 'fluent-boards'),
724 + 'deleted' => __('deleted', 'fluent-boards'),
725 + 'archived' => __('archived', 'fluent-boards'),
726 + 'restored' => __('restored', 'fluent-boards'),
727 + 'set' => __('set', 'fluent-boards'),
728 + 'moved' => __('moved', 'fluent-boards'),
729 + ];
730 +
731 + $columnTranslations = [
732 + Constant::ACTIVITY_CUSTOM_FIELD => __('custom field', 'fluent-boards'),
733 + 'task' => __('task', 'fluent-boards'),
734 + 'description' => __('description', 'fluent-boards'),
735 + 'board' => __('board', 'fluent-boards'),
736 + 'assignee' => __('assignee', 'fluent-boards'),
737 + 'label' => __('label', 'fluent-boards'),
738 + 'Due Date' => __('Due Date', 'fluent-boards'),
739 + 'Start Date' => __('Start Date', 'fluent-boards'),
740 + 'priority' => __('priority', 'fluent-boards'),
741 + 'comment' => __('comment', 'fluent-boards'),
742 + 'a reply' => __('a reply', 'fluent-boards'),
743 + 'subtask' => __('subtask', 'fluent-boards'),
744 + 'subtask group' => __('subtask group', 'fluent-boards'),
745 + 'subtask group title' => __('subtask group title', 'fluent-boards'),
746 + 'stage' => __('stage', 'fluent-boards'),
747 + 'the associate email' => __('the associate email', 'fluent-boards'),
748 + 'attachment' => __('attachment', 'fluent-boards'),
749 + 'repeat task' => __('repeat task', 'fluent-boards'),
750 + 'Repeat Task' => __('Repeat Task', 'fluent-boards'),
751 + 'tasks' => __('tasks', 'fluent-boards'),
752 + ];
753 +
754 + foreach ($activities as $activity) {
755 + $activity->action_key = $activity->action;
756 + $activity->column_key = $activity->column;
757 +
758 + if (isset($actionTranslations[$activity->action])) {
759 + $activity->action = $actionTranslations[$activity->action];
760 + }
761 + if (isset($columnTranslations[$activity->column])) {
762 + $activity->column = $columnTranslations[$activity->column];
763 + }
764 + }
463 765 }
464 766 }