PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.1.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.1.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 1.41 All 42 releases
← All changes | app/Services/Helper.php +299 -16 1.352.1.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',
@@ -60,9 +119,9 @@
60 119 'title' => 'sanitize_text_field',
61 120 'board_id' => 'intval',
62 121 'parent_id' => 'intval',
63 122 'crm_contact_id' => 'intval',
64 - 'task_type' => 'sanitize_text_field',
123 + 'type' => 'sanitize_text_field',
65 124 'stage' => 'sanitize_text_field',
66 125 'reminder_type' => 'sanitize_text_field',
67 126 'priority' => 'sanitize_text_field',
68 127 'lead_value' => 'doubleval',
@@ -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',
@@ -153,15 +215,32 @@
153 215 'title' => 'sanitize_text_field',
154 216 'stage' => 'sanitize_text_field',
155 217 'newPosition' => 'intval',
156 218 'priority' => 'sanitize_text_field',
157 - 'task_type' => 'sanitize_text_field',
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
166 245 public static function createActivity($data)
167 246 {
@@ -323,20 +402,43 @@
323 402 }
324 403
325 404 public static function sanitizeUserCollections($users)
326 405 {
406 + if (empty($users)) {
407 + return $users;
408 + }
409 +
410 + foreach ($users as $key => $user) {
411 + if (is_object($user) && isset($user->pivot)) {
412 + $settings = maybe_unserialize($user->pivot->settings);
413 + $user->role = Arr::get($settings, 'is_admin')
414 + ? 'Admin'
415 + : (Arr::has($settings, 'is_viewer_only') && Arr::get($settings, 'is_viewer_only')
416 + ? 'Viewer'
417 + : 'Member');
418 + }
419 + }
420 +
327 421 if (current_user_can('list_users')) {
328 422 return $users;
329 423 }
330 424
331 - if ($users) {
425 + if (is_object($users) && method_exists($users, 'makeHidden')) {
332 426 $users->makeHidden(['user_email', 'user_nicename', 'user_registered', 'user_url', 'user_status']);
427 + } elseif (is_array($users)) {
428 + foreach ($users as &$user) {
429 + if (is_array($user)) {
430 + unset($user['user_email'], $user['user_nicename'], $user['user_registered'], $user['user_url'], $user['user_status']);
431 + }
432 + }
433 + unset($user);
333 434 }
334 435
335 436 return $users;
336 437 }
337 438
338 - public static function sanitizeUsersArray($users, $boardId = null)
439 + // Callers formatting multiple lists may supply a resolved board-manager result.
440 + public static function sanitizeUsersArray($users, $boardId = null, $isBoardManager = null)
339 441 {
340 442 if (current_user_can('list_users')) {
341 443 return $users;
342 444 }
@@ -342,13 +444,28 @@
342 444 }
343 445
344 446 $sanitizedUsers = [];
345 447
346 - if(!PermissionManager::isBoardManager($boardId)) //Todo: may create permission security issue, will be modified later
448 + if (!($isBoardManager ?? PermissionManager::isBoardManager($boardId)))
347 449 {
450 + $currentUser = wp_get_current_user();
451 + if($currentUser && isset($currentUser->user_email)){
452 + $currentUserEmail = $currentUser->user_email;
453 + }
348 454 foreach ($users as $user) {
349 - unset($user->user_email);
350 - unset($user->email);
455 +
456 + if($user['email'] === $currentUserEmail){
457 + $sanitizedUsers[] = $user;
458 + continue;
459 + }
460 +
461 + if (isset($user['email'])) {
462 + $user['email'] = self::obfuscateEmail($user['email']);
463 + }
464 + if (isset($user['user_email'])) {
465 + $user['user_email'] = self::obfuscateEmail($user['user_email']);
466 + }
467 +
351 468 $sanitizedUsers[] = $user;
352 469 }
353 470 } else {
354 471 foreach ($users as $user) {
@@ -358,22 +475,52 @@
358 475
359 476 return $sanitizedUsers;
360 477 }
361 478
479 + public static function obfuscateEmail($email)
480 + {
481 + if (!is_string($email) || filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
482 + return $email; // Not a valid email, return as is
483 + }
484 +
485 + list($name, $domain) = explode('@', $email, 2);
486 +
487 + // Local part: show first 3 chars, then fixed ****
488 + $visibleLocal = substr($name, 0, 3);
489 + $maskedLocal = $visibleLocal . '****';
490 +
491 + // Domain: mask the main label to **** + last 2 chars, keep rest (TLDs) intact
492 + $domainParts = explode('.', $domain);
493 + $mainLabel = $domainParts[0] ?? '';
494 + $tail = strlen($mainLabel) >= 2 ? substr($mainLabel, -2) : $mainLabel;
495 + $domainParts[0] = '****' . $tail; // e.g., example.com -> ****le.com
496 + $maskedDomain = implode('.', $domainParts);
497 +
498 + return $maskedLocal . '@' . $maskedDomain;
499 + }
500 +
362 501 public static function getPriorityOptions()
363 502 {
364 503 return [
365 504 [
366 - 'id' => 'low',
367 - 'title' => 'Low'
505 + 'id' => '',
506 + 'title' => 'No priority'
368 507 ],
369 508 [
509 + 'id' => 'urgent',
510 + 'title' => 'Urgent'
511 + ],
512 + [
513 + 'id' => 'high',
514 + 'title' => 'High'
515 + ],
516 + [
370 517 'id' => 'medium',
371 518 'title' => 'Medium'
372 519 ],
373 520 [
374 - 'id' => 'high',
375 - 'title' => 'High'
521 + 'id' => 'low',
522 + 'title' => 'Low'
376 523 ],
377 524 ];
378 525 }
379 526
@@ -451,8 +598,9 @@
451 598 'repeat_in' => 'intval',
452 599 'repeat_type' => 'sanitize_text_field',
453 600 'repeat_when_complete' => 'intval',
454 601 'selected_month' => 'sanitize_text_field',
602 + 'selected_stage' => 'intval',
455 603 'board_id' => 'intval',
456 604 'time' => 'sanitize_text_field',
457 605 'time_zone' => 'sanitize_text_field',
458 606 'next_repeat_date' => 'sanitize_text_field',
@@ -459,6 +607,141 @@
459 607 'repeat_in_month_type' => 'sanitize_text_field',
460 608 ];
461 609
462 610 return self::sanitizeData($data, $fieldMaps);
611 + }
612 +
613 + /**
614 + * Sanitize the author snapshot supplied by an external task integration.
615 + *
616 + * @param mixed $author
617 + * @return array
618 + */
619 + private static function sanitizeExternalTaskAuthor($author)
620 + {
621 + if (!is_array($author)) {
622 + return [];
623 + }
624 +
625 + return array_filter([
626 + 'name' => sanitize_text_field($author['name'] ?? ''),
627 + 'email' => sanitize_email($author['email'] ?? ''),
628 + 'photo' => esc_url_raw($author['photo'] ?? ''),
629 + ]);
630 + }
631 +
632 + public static function sanitizeTaskForWebHook($data)
633 + {
634 + $fieldMaps = [
635 + 'title' => 'sanitize_text_field',
636 + 'board_id' => 'intval',
637 + 'parent_id' => 'intval',
638 + 'crm_contact_id' => 'intval',
639 + 'type' => 'sanitize_text_field',
640 + 'stage' => 'sanitize_text_field',
641 + 'reminder_type' => 'sanitize_text_field',
642 + 'priority' => 'sanitize_text_field',
643 + 'lead_value' => 'doubleval',
644 + 'remind_at' => 'sanitize_text_field',
645 + 'scope' => 'sanitize_text_field',
646 + 'source' => 'sanitize_text_field',
647 + 'source_id' => 'sanitize_text_field',
648 + 'description' => 'fluent_boards_sanitize_description',
649 + 'due_date' => 'sanitize_text_field',
650 + 'start_at' => 'sanitize_text_field',
651 + 'log_minutes' => 'sanitize_text_field',
652 + 'last_completed' => 'sanitize_text_field',
653 + 'is_archived' => 'intval',
654 + 'previous_stage' => 'sanitize_text_field',
655 + 'new_stage' => 'sanitize_text_field',
656 + 'new_index' => 'intval',
657 + 'old_index' => 'intval',
658 + 'new_board_id' => 'intval',
659 + 'position' => 'intval'
660 +
661 + ];
662 +
663 + $data = self::sanitizeData($data, $fieldMaps);
664 +
665 + if (isset($data['settings']) && is_array($data['settings']) && isset($data['settings']['author'])) {
666 + $data['settings'] = [
667 + 'author' => self::sanitizeExternalTaskAuthor($data['settings']['author']),
668 + ];
669 + } else {
670 + unset($data['settings']);
671 + }
672 +
673 + return $data;
674 + }
675 +
676 +
677 + public static function taskReminderTypes()
678 + {
679 + $allowedTypes = [
680 + '30_minutes_before' => __('30 minutes before', 'fluent-boards'),
681 + '1_hour_before' => __('1 hour before', 'fluent-boards'),
682 + '2_hours_before' => __('2 hours before', 'fluent-boards'),
683 + '1_day_before' => __('1 day before', 'fluent-boards'),
684 + '2_days_before' => __('2 days before', 'fluent-boards'),
685 + '1_week_before' => __('1 week before', 'fluent-boards'),
686 + ];
687 +
688 + $allowedTypes = apply_filters('fluent_boards/task_reminder_types', $allowedTypes);
689 +
690 + return $allowedTypes;
691 + }
692 +
693 + public static function translateActivities($activities)
694 + {
695 + $actionTranslations = [
696 + 'changed' => __('changed', 'fluent-boards'),
697 + 'updated' => __('updated', 'fluent-boards'),
698 + 'added' => __('added', 'fluent-boards'),
699 + 'removed' => __('removed', 'fluent-boards'),
700 + 'created' => __('created', 'fluent-boards'),
701 + 'closed' => __('closed', 'fluent-boards'),
702 + 'reopened' => __('reopened', 'fluent-boards'),
703 + 'joined' => __('joined', 'fluent-boards'),
704 + 'left' => __('left', 'fluent-boards'),
705 + 'cloned' => __('cloned', 'fluent-boards'),
706 + 'deleted' => __('deleted', 'fluent-boards'),
707 + 'archived' => __('archived', 'fluent-boards'),
708 + 'restored' => __('restored', 'fluent-boards'),
709 + 'set' => __('set', 'fluent-boards'),
710 + 'moved' => __('moved', 'fluent-boards'),
711 + ];
712 +
713 + $columnTranslations = [
714 + 'task' => __('task', 'fluent-boards'),
715 + 'description' => __('description', 'fluent-boards'),
716 + 'board' => __('board', 'fluent-boards'),
717 + 'assignee' => __('assignee', 'fluent-boards'),
718 + 'label' => __('label', 'fluent-boards'),
719 + 'Due Date' => __('Due Date', 'fluent-boards'),
720 + 'Start Date' => __('Start Date', 'fluent-boards'),
721 + 'priority' => __('priority', 'fluent-boards'),
722 + 'comment' => __('comment', 'fluent-boards'),
723 + 'a reply' => __('a reply', 'fluent-boards'),
724 + 'subtask' => __('subtask', 'fluent-boards'),
725 + 'subtask group' => __('subtask group', 'fluent-boards'),
726 + 'subtask group title' => __('subtask group title', 'fluent-boards'),
727 + 'stage' => __('stage', 'fluent-boards'),
728 + 'the associate email' => __('the associate email', 'fluent-boards'),
729 + 'attachment' => __('attachment', 'fluent-boards'),
730 + 'repeat task' => __('repeat task', 'fluent-boards'),
731 + 'Repeat Task' => __('Repeat Task', 'fluent-boards'),
732 + 'tasks' => __('tasks', 'fluent-boards'),
733 + ];
734 +
735 + foreach ($activities as $activity) {
736 + $activity->action_key = $activity->action;
737 + $activity->column_key = $activity->column;
738 +
739 + if (isset($actionTranslations[$activity->action])) {
740 + $activity->action = $actionTranslations[$activity->action];
741 + }
742 + if (isset($columnTranslations[$activity->column])) {
743 + $activity->column = $columnTranslations[$activity->column];
744 + }
745 + }
463 746 }
464 747 }