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