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 +319 -16 1.31 → 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,20 +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 - if ($users) {
443 + if (is_object($users) && method_exists($users, 'makeHidden')) {
331 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);
332 452 }
333 453
334 454 return $users;
335 455 }
336 456
337 - 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)
338 459 {
339 460 if (current_user_can('list_users')) {
340 461 return $users;
341 462 }
@@ -341,13 +462,28 @@
341 462 }
342 463
343 464 $sanitizedUsers = [];
344 465
345 - if(!PermissionManager::isBoardManager($boardId)) //Todo: may create permission security issue, will be modified later
466 + if (!($isBoardManager ?? PermissionManager::isBoardManager($boardId)))
346 467 {
468 + $currentUser = wp_get_current_user();
469 + if($currentUser && isset($currentUser->user_email)){
470 + $currentUserEmail = $currentUser->user_email;
471 + }
347 472 foreach ($users as $user) {
348 - unset($user->user_email);
349 - 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 +
350 486 $sanitizedUsers[] = $user;
351 487 }
352 488 } else {
353 489 foreach ($users as $user) {
@@ -357,22 +493,52 @@
357 493
358 494 return $sanitizedUsers;
359 495 }
360 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 +
361 519 public static function getPriorityOptions()
362 520 {
363 521 return [
364 522 [
365 - 'id' => 'low',
366 - 'title' => 'Low'
523 + 'id' => '',
524 + 'title' => 'No priority'
367 525 ],
368 526 [
527 + 'id' => 'urgent',
528 + 'title' => 'Urgent'
529 + ],
530 + [
531 + 'id' => 'high',
532 + 'title' => 'High'
533 + ],
534 + [
369 535 'id' => 'medium',
370 536 'title' => 'Medium'
371 537 ],
372 538 [
373 - 'id' => 'high',
374 - 'title' => 'High'
539 + 'id' => 'low',
540 + 'title' => 'Low'
375 541 ],
376 542 ];
377 543 }
378 544
@@ -450,8 +616,9 @@
450 616 'repeat_in' => 'intval',
451 617 'repeat_type' => 'sanitize_text_field',
452 618 'repeat_when_complete' => 'intval',
453 619 'selected_month' => 'sanitize_text_field',
620 + 'selected_stage' => 'intval',
454 621 'board_id' => 'intval',
455 622 'time' => 'sanitize_text_field',
456 623 'time_zone' => 'sanitize_text_field',
457 624 'next_repeat_date' => 'sanitize_text_field',
@@ -458,6 +625,142 @@
458 625 'repeat_in_month_type' => 'sanitize_text_field',
459 626 ];
460 627
461 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 + }
462 765 }
463 766 }