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 +339 -16 1.22 → 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',
@@ -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,19 +215,49 @@
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
245 + /** Store full custom-field messages in LONGTEXT while keeping legacy VARCHAR columns bounded. */
165 246 public static function createActivity($data)
166 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 +
167 260 return Activity::create($data);
168 261 }
169 262
170 263 public static function sanitizeTaskMeta($data)
@@ -255,8 +348,9 @@
255 348 }
256 349 return fluent_boards_page_url() . 'boards/' . $boardId;
257 350 }
258 351
352 + /** Format a CRM contact, avoiding a database lookup for unassociated tasks. */
259 353 public static function crm_contact($id)
260 354 {
261 355 if (!defined('FLUENTCRM')) {
262 356 return '';
@@ -261,8 +355,12 @@
261 355 if (!defined('FLUENTCRM')) {
262 356 return '';
263 357 }
264 358
359 + if (!$id) {
360 + return null;
361 + }
362 +
265 363 $contact = \FluentCrm\App\Models\Subscriber::with(['tags', 'lists'])->find($id);
266 364
267 365 if (!$contact) {
268 366 return null;
@@ -322,18 +420,43 @@
322 420 }
323 421
324 422 public static function sanitizeUserCollections($users)
325 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 +
326 439 if (current_user_can('list_users')) {
327 440 return $users;
328 441 }
329 442
330 - $users->makeHidden(['user_email', 'user_nicename', 'user_registered', 'user_url', 'user_status']);
443 + if (is_object($users) && method_exists($users, 'makeHidden')) {
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);
452 + }
331 453
332 454 return $users;
333 455 }
334 456
335 - 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)
336 459 {
337 460 if (current_user_can('list_users')) {
338 461 return $users;
339 462 }
@@ -339,13 +462,28 @@
339 462 }
340 463
341 464 $sanitizedUsers = [];
342 465
343 - if(!PermissionManager::isBoardManager($boardId)) //Todo: may create permission security issue, will be modified later
466 + if (!($isBoardManager ?? PermissionManager::isBoardManager($boardId)))
344 467 {
468 + $currentUser = wp_get_current_user();
469 + if($currentUser && isset($currentUser->user_email)){
470 + $currentUserEmail = $currentUser->user_email;
471 + }
345 472 foreach ($users as $user) {
346 - unset($user->user_email);
347 - 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 +
348 486 $sanitizedUsers[] = $user;
349 487 }
350 488 } else {
351 489 foreach ($users as $user) {
@@ -355,22 +493,52 @@
355 493
356 494 return $sanitizedUsers;
357 495 }
358 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 +
359 519 public static function getPriorityOptions()
360 520 {
361 521 return [
362 522 [
363 - 'id' => 'low',
364 - 'title' => 'Low'
523 + 'id' => '',
524 + 'title' => 'No priority'
365 525 ],
366 526 [
527 + 'id' => 'urgent',
528 + 'title' => 'Urgent'
529 + ],
530 + [
531 + 'id' => 'high',
532 + 'title' => 'High'
533 + ],
534 + [
367 535 'id' => 'medium',
368 536 'title' => 'Medium'
369 537 ],
370 538 [
371 - 'id' => 'high',
372 - 'title' => 'High'
539 + 'id' => 'low',
540 + 'title' => 'Low'
373 541 ],
374 542 ];
375 543 }
376 544
@@ -438,6 +606,161 @@
438 606 'url' => 'sanitize_url',
439 607 ];
440 608
441 609 return self::sanitizeData($data, $fieldMaps);
610 + }
611 +
612 + public static function sanitizeTaskRepeatData($data)
613 + {
614 + $fieldMaps = [
615 + 'create_new' => 'intval',
616 + 'repeat_in' => 'intval',
617 + 'repeat_type' => 'sanitize_text_field',
618 + 'repeat_when_complete' => 'intval',
619 + 'selected_month' => 'sanitize_text_field',
620 + 'selected_stage' => 'intval',
621 + 'board_id' => 'intval',
622 + 'time' => 'sanitize_text_field',
623 + 'time_zone' => 'sanitize_text_field',
624 + 'next_repeat_date' => 'sanitize_text_field',
625 + 'repeat_in_month_type' => 'sanitize_text_field',
626 + ];
627 +
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 + }
442 765 }
443 766 }