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 +406 -17 1.11 → 2.2.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,19 +215,49 @@
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
245 + /** Store full custom-field messages in LONGTEXT while keeping legacy VARCHAR columns bounded. */
163 246 public static function createActivity($data)
164 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 +
165 260 return Activity::create($data);
166 261 }
167 262
168 263 public static function sanitizeTaskMeta($data)
@@ -253,8 +348,9 @@
253 348 }
254 349 return fluent_boards_page_url() . 'boards/' . $boardId;
255 350 }
256 351
352 + /** Format a CRM contact, avoiding a database lookup for unassociated tasks. */
257 353 public static function crm_contact($id)
258 354 {
259 355 if (!defined('FLUENTCRM')) {
260 356 return '';
@@ -259,8 +355,12 @@
259 355 if (!defined('FLUENTCRM')) {
260 356 return '';
261 357 }
262 358
359 + if (!$id) {
360 + return null;
361 + }
362 +
263 363 $contact = \FluentCrm\App\Models\Subscriber::with(['tags', 'lists'])->find($id);
264 364
265 365 if (!$contact) {
266 366 return null;
@@ -320,18 +420,43 @@
320 420 }
321 421
322 422 public static function sanitizeUserCollections($users)
323 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 +
324 439 if (current_user_can('list_users')) {
325 440 return $users;
326 441 }
327 442
328 - $users->makeHidden(['user_email', 'user_login', '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 + }
329 453
330 454 return $users;
331 455 }
332 456
333 - public static function sanitizeUsersArray($users)
457 + // Callers formatting multiple lists may supply a resolved board-manager result.
458 + public static function sanitizeUsersArray($users, $boardId = null, $isBoardManager = null)
334 459 {
335 460 if (current_user_can('list_users')) {
336 461 return $users;
337 462 }
@@ -337,31 +462,83 @@
337 462 }
338 463
339 464 $sanitizedUsers = [];
340 465
341 - foreach ($users as $user) {
342 - unset($user['user_email']);
343 - unset($user['email']);
344 - $sanitizedUsers[] = $user;
466 + if (!($isBoardManager ?? PermissionManager::isBoardManager($boardId)))
467 + {
468 + $currentUser = wp_get_current_user();
469 + if($currentUser && isset($currentUser->user_email)){
470 + $currentUserEmail = $currentUser->user_email;
471 + }
472 + foreach ($users as $user) {
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 +
486 + $sanitizedUsers[] = $user;
487 + }
488 + } else {
489 + foreach ($users as $user) {
490 + $sanitizedUsers[] = $user;
491 + }
345 492 }
346 493
347 494 return $sanitizedUsers;
348 495 }
349 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 +
350 519 public static function getPriorityOptions()
351 520 {
352 521 return [
353 522 [
354 - 'id' => 'low',
355 - 'title' => 'Low'
523 + 'id' => '',
524 + 'title' => 'No priority'
356 525 ],
357 526 [
527 + 'id' => 'urgent',
528 + 'title' => 'Urgent'
529 + ],
530 + [
531 + 'id' => 'high',
532 + 'title' => 'High'
533 + ],
534 + [
358 535 'id' => 'medium',
359 536 'title' => 'Medium'
360 537 ],
361 538 [
362 - 'id' => 'high',
363 - 'title' => 'High'
539 + 'id' => 'low',
540 + 'title' => 'Low'
364 541 ],
365 542 ];
366 543 }
367 544
@@ -372,6 +549,218 @@
372 549 }
373 550 $currentTime = current_time('mysql');
374 551 $readyString = '+' . $due_time . ' ' . $unit;
375 552 return gmdate('Y-m-d H:i:s',strtotime($readyString,strtotime($currentTime)));
553 + }
554 +
555 + public static function searchWordPressUsers($searchQuery, $limit = 20)
556 + {
557 + $search = sanitize_text_field($searchQuery);
558 +
559 + // Search by user login, email, and nicename
560 + $args = array(
561 + 'role' => '',
562 + 'search' => '*' . $search . '*',
563 + 'number' => $limit,
564 + );
565 +
566 + // Get users by login, email, and nicename
567 + $user_query = new \WP_User_Query($args);
568 + $users_by_login = $user_query->get_results();
569 +
570 + // Search by first name and last name
571 + $meta_query_args = array(
572 + 'relation' => 'OR',
573 + array(
574 + 'key' => 'first_name',
575 + 'value' => $search,
576 + 'compare' => 'LIKE',
577 + ),
578 + array(
579 + 'key' => 'last_name',
580 + 'value' => $search,
581 + 'compare' => 'LIKE',
582 + ),
583 + );
584 +
585 + $meta_query = array(
586 + 'role' => '',
587 + 'meta_query' => $meta_query_args,
588 + 'number' => $limit,
589 + );
590 +
591 + // Get users by first name and last name
592 + $users_by_meta = get_users($meta_query);
593 +
594 + // Merge and remove duplicates
595 + $users = array_merge($users_by_login, $users_by_meta);
596 + $users = array_unique($users, SORT_REGULAR);
597 +
598 + return $users;
599 +
600 + }
601 +
602 + public static function sanitizeTaskAttachment($data)
603 + {
604 + $fieldMaps = [
605 + 'title' => 'sanitize_text_field',
606 + 'url' => 'sanitize_url',
607 + ];
608 +
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 + }
376 765 }
377 766 }