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 +320 -16 1.222.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,10 @@
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',
167 + 'created_by' => 'intval',
106 168 ];
107 169
108 170 return self::sanitizeData($data, $fieldMaps);
109 171 }
@@ -134,10 +196,11 @@
134 196
135 197 public static function sanitizeLabel($data)
136 198 {
137 199 $fieldMaps = [
138 - 'bg_color' => 'sanitize_hex_color',
139 - 'color' => 'sanitize_hex_color',
200 + 'bg_color' => 'sanitize_text_field',
201 + 'color' => 'sanitize_text_field',
202 + 'color_preset' => 'sanitize_key',
140 203 'label' => 'sanitize_text_field',
141 204 'boardId' => 'intval',
142 205 'task_id' => 'intval',
143 206 'meta_value' => 'intval',
@@ -152,15 +215,32 @@
152 215 'title' => 'sanitize_text_field',
153 216 'stage' => 'sanitize_text_field',
154 217 'newPosition' => 'intval',
155 218 'priority' => 'sanitize_text_field',
156 - 'task_type' => 'sanitize_text_field',
219 + 'type' => 'sanitize_text_field',
220 + 'description' => 'fluent_boards_sanitize_description',
221 + 'group_id' => 'intval',
157 222 'board_id' => 'intval',
158 223 'created_by' => 'intval',
159 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',
160 230 ];
161 231
162 - 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;
163 243 }
164 244
165 245 public static function createActivity($data)
166 246 {
@@ -322,18 +402,43 @@
322 402 }
323 403
324 404 public static function sanitizeUserCollections($users)
325 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 +
326 421 if (current_user_can('list_users')) {
327 422 return $users;
328 423 }
329 424
330 - $users->makeHidden(['user_email', 'user_nicename', 'user_registered', 'user_url', 'user_status']);
425 + if (is_object($users) && method_exists($users, 'makeHidden')) {
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);
434 + }
331 435
332 436 return $users;
333 437 }
334 438
335 - 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)
336 441 {
337 442 if (current_user_can('list_users')) {
338 443 return $users;
339 444 }
@@ -339,13 +444,28 @@
339 444 }
340 445
341 446 $sanitizedUsers = [];
342 447
343 - if(!PermissionManager::isBoardManager($boardId)) //Todo: may create permission security issue, will be modified later
448 + if (!($isBoardManager ?? PermissionManager::isBoardManager($boardId)))
344 449 {
450 + $currentUser = wp_get_current_user();
451 + if($currentUser && isset($currentUser->user_email)){
452 + $currentUserEmail = $currentUser->user_email;
453 + }
345 454 foreach ($users as $user) {
346 - unset($user->user_email);
347 - 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 +
348 468 $sanitizedUsers[] = $user;
349 469 }
350 470 } else {
351 471 foreach ($users as $user) {
@@ -355,22 +475,52 @@
355 475
356 476 return $sanitizedUsers;
357 477 }
358 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 +
359 501 public static function getPriorityOptions()
360 502 {
361 503 return [
362 504 [
363 - 'id' => 'low',
364 - 'title' => 'Low'
505 + 'id' => '',
506 + 'title' => 'No priority'
365 507 ],
366 508 [
509 + 'id' => 'urgent',
510 + 'title' => 'Urgent'
511 + ],
512 + [
513 + 'id' => 'high',
514 + 'title' => 'High'
515 + ],
516 + [
367 517 'id' => 'medium',
368 518 'title' => 'Medium'
369 519 ],
370 520 [
371 - 'id' => 'high',
372 - 'title' => 'High'
521 + 'id' => 'low',
522 + 'title' => 'Low'
373 523 ],
374 524 ];
375 525 }
376 526
@@ -438,6 +588,160 @@
438 588 'url' => 'sanitize_url',
439 589 ];
440 590
441 591 return self::sanitizeData($data, $fieldMaps);
592 + }
593 +
594 + public static function sanitizeTaskRepeatData($data)
595 + {
596 + $fieldMaps = [
597 + 'create_new' => 'intval',
598 + 'repeat_in' => 'intval',
599 + 'repeat_type' => 'sanitize_text_field',
600 + 'repeat_when_complete' => 'intval',
601 + 'selected_month' => 'sanitize_text_field',
602 + 'selected_stage' => 'intval',
603 + 'board_id' => 'intval',
604 + 'time' => 'sanitize_text_field',
605 + 'time_zone' => 'sanitize_text_field',
606 + 'next_repeat_date' => 'sanitize_text_field',
607 + 'repeat_in_month_type' => 'sanitize_text_field',
608 + ];
609 +
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 + }
442 746 }
443 747 }