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 +340 -17 1.202.1.0 View file →
@@ -7,8 +7,10 @@
7 7 use FluentBoards\App\Models\Webhook;
8 8 use FluentBoards\Framework\Support\Arr;
9 9 use FluentBoards\Framework\Support\Str;
10 10 use FluentBoards\App\Models\Board;
11 +use FluentBoards\App\Services\Parsedown;
12 +use FluentBoards\App\Services\PermissionManager;
11 13
12 14 class Helper
13 15 {
14 16 public static function snake_case($string)
@@ -51,8 +53,67 @@
51 53
52 54 return $data;
53 55 }
54 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 +
55 116 public static function sanitizeTask($data)
56 117 {
57 118 $fieldMaps = [
58 119 'title' => 'sanitize_text_field',
@@ -58,9 +119,9 @@
58 119 'title' => 'sanitize_text_field',
59 120 'board_id' => 'intval',
60 121 'parent_id' => 'intval',
61 122 'crm_contact_id' => 'intval',
62 - 'task_type' => 'sanitize_text_field',
123 + 'type' => 'sanitize_text_field',
63 124 'stage' => 'sanitize_text_field',
64 125 'reminder_type' => 'sanitize_text_field',
65 126 'priority' => 'sanitize_text_field',
66 127 'lead_value' => 'doubleval',
@@ -66,9 +127,10 @@
66 127 'lead_value' => 'doubleval',
67 128 'remind_at' => 'sanitize_text_field',
68 129 'scope' => 'sanitize_text_field',
69 130 'source' => 'sanitize_text_field',
70 - 'description' => 'wp_kses_post',
131 + 'source_id' => 'sanitize_text_field',
132 + 'description' => 'fluent_boards_sanitize_description',
71 133 'due_date' => 'sanitize_text_field',
72 134 'start_at' => 'sanitize_text_field',
73 135 'log_minutes' => 'sanitize_text_field',
74 136 'last_completed' => 'sanitize_text_field',
@@ -92,9 +154,9 @@
92 154 'board_id' => 'intval',
93 155 'title' => 'sanitize_text_field',
94 156 'parent_id' => 'intval',
95 157 'type' => 'sanitize_text_field',
96 - 'description' => 'wp_kses_post',
158 + 'description' => 'fluent_boards_sanitize_description',
97 159 'currency' => 'sanitize_text_field',
98 160 'image_url' => 'sanitize_url',
99 161 'is_auth_require' => 'intval',
100 162 'crm_contact_id' => 'intval',
@@ -100,8 +162,10 @@
100 162 'crm_contact_id' => 'intval',
101 163 'id' => 'sanitize_text_field',
102 164 'is_image' => 'rest_sanitize_boolean',
103 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',
104 168 ];
105 169
106 170 return self::sanitizeData($data, $fieldMaps);
107 171 }
@@ -132,10 +196,11 @@
132 196
133 197 public static function sanitizeLabel($data)
134 198 {
135 199 $fieldMaps = [
136 - 'bg_color' => 'sanitize_hex_color',
137 - 'color' => 'sanitize_hex_color',
200 + 'bg_color' => 'sanitize_text_field',
201 + 'color' => 'sanitize_text_field',
202 + 'color_preset' => 'sanitize_key',
138 203 'label' => 'sanitize_text_field',
139 204 'boardId' => 'intval',
140 205 'task_id' => 'intval',
141 206 'meta_value' => 'intval',
@@ -150,15 +215,32 @@
150 215 'title' => 'sanitize_text_field',
151 216 'stage' => 'sanitize_text_field',
152 217 'newPosition' => 'intval',
153 218 'priority' => 'sanitize_text_field',
154 - 'task_type' => 'sanitize_text_field',
219 + 'type' => 'sanitize_text_field',
220 + 'description' => 'fluent_boards_sanitize_description',
221 + 'group_id' => 'intval',
155 222 'board_id' => 'intval',
156 223 'created_by' => 'intval',
157 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',
158 230 ];
159 231
160 - 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;
161 243 }
162 244
163 245 public static function createActivity($data)
164 246 {
@@ -320,18 +402,43 @@
320 402 }
321 403
322 404 public static function sanitizeUserCollections($users)
323 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 +
324 421 if (current_user_can('list_users')) {
325 422 return $users;
326 423 }
327 424
328 - $users->makeHidden(['user_email', 'user_login', '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 + }
329 435
330 436 return $users;
331 437 }
332 438
333 - public static function sanitizeUsersArray($users)
439 + // Callers formatting multiple lists may supply a resolved board-manager result.
440 + public static function sanitizeUsersArray($users, $boardId = null, $isBoardManager = null)
334 441 {
335 442 if (current_user_can('list_users')) {
336 443 return $users;
337 444 }
@@ -337,31 +444,83 @@
337 444 }
338 445
339 446 $sanitizedUsers = [];
340 447
341 - foreach ($users as $user) {
342 - unset($user['user_email']);
343 - unset($user['email']);
344 - $sanitizedUsers[] = $user;
448 + if (!($isBoardManager ?? PermissionManager::isBoardManager($boardId)))
449 + {
450 + $currentUser = wp_get_current_user();
451 + if($currentUser && isset($currentUser->user_email)){
452 + $currentUserEmail = $currentUser->user_email;
453 + }
454 + foreach ($users as $user) {
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 +
468 + $sanitizedUsers[] = $user;
469 + }
470 + } else {
471 + foreach ($users as $user) {
472 + $sanitizedUsers[] = $user;
473 + }
345 474 }
346 475
347 476 return $sanitizedUsers;
348 477 }
349 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 +
350 501 public static function getPriorityOptions()
351 502 {
352 503 return [
353 504 [
354 - 'id' => 'low',
355 - 'title' => 'Low'
505 + 'id' => '',
506 + 'title' => 'No priority'
356 507 ],
357 508 [
509 + 'id' => 'urgent',
510 + 'title' => 'Urgent'
511 + ],
512 + [
513 + 'id' => 'high',
514 + 'title' => 'High'
515 + ],
516 + [
358 517 'id' => 'medium',
359 518 'title' => 'Medium'
360 519 ],
361 520 [
362 - 'id' => 'high',
363 - 'title' => 'High'
521 + 'id' => 'low',
522 + 'title' => 'Low'
364 523 ],
365 524 ];
366 525 }
367 526
@@ -419,6 +578,170 @@
419 578 $users = array_unique($users, SORT_REGULAR);
420 579
421 580 return $users;
422 581
582 + }
583 +
584 + public static function sanitizeTaskAttachment($data)
585 + {
586 + $fieldMaps = [
587 + 'title' => 'sanitize_text_field',
588 + 'url' => 'sanitize_url',
589 + ];
590 +
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 + }
423 746 }
424 747 }