$value) { if ($value && isset($fieldMaps[$key]) && !is_array($value)) { $data[$key] = call_user_func($fieldMaps[$key], $value); } } return $data; } /** * Normalize a nullable datetime value so task flows can safely persist NULL. * * @param mixed $value * @return mixed|null */ public static function normalizeDateValue($value) { if ($value === null || is_bool($value)) { return null; } if (is_string($value)) { $value = trim($value); if ($value === '') { return null; } $normalizedValue = strtolower($value); if (in_array($normalizedValue, ['none', 'null'], true)) { return null; } if (in_array($value, ['0000-00-00', '0000-00-00 00:00:00'], true)) { return null; } if (preg_match('/^(\d{4})-/', $value, $matches) && (int) $matches[1] < 1900) { return null; } if (strtotime($value) === false) { return null; } } return $value; } /** * Normalize a list of nullable datetime keys inside an attribute array. * * @param array $data * @param array $dateKeys * @return array */ public static function normalizeDates($data, $dateKeys = []) { foreach ($dateKeys as $dateKey) { if (array_key_exists($dateKey, $data)) { $data[$dateKey] = self::normalizeDateValue($data[$dateKey]); } } return $data; } public static function sanitizeTask($data) { $fieldMaps = [ 'title' => 'sanitize_text_field', 'board_id' => 'intval', 'parent_id' => 'intval', 'crm_contact_id' => 'intval', 'type' => 'sanitize_text_field', 'stage' => 'sanitize_text_field', 'reminder_type' => 'sanitize_text_field', 'priority' => 'sanitize_text_field', 'lead_value' => 'doubleval', 'remind_at' => 'sanitize_text_field', 'scope' => 'sanitize_text_field', 'source' => 'sanitize_text_field', 'source_id' => 'sanitize_text_field', 'description' => 'fluent_boards_sanitize_description', 'due_date' => 'sanitize_text_field', 'start_at' => 'sanitize_text_field', 'log_minutes' => 'sanitize_text_field', 'last_completed' => 'sanitize_text_field', 'assignees' => 'intval', 'is_archived' => 'intval', 'previous_stage' => 'sanitize_text_field', 'new_stage' => 'sanitize_text_field', 'new_index' => 'intval', 'old_index' => 'intval', 'new_board_id' => 'intval', 'position' => 'intval' ]; return self::sanitizeData($data, $fieldMaps); } public static function sanitizeBoard($data) { $fieldMaps = [ 'board_id' => 'intval', 'title' => 'sanitize_text_field', 'parent_id' => 'intval', 'type' => 'sanitize_text_field', 'description' => 'fluent_boards_sanitize_description', 'currency' => 'sanitize_text_field', 'image_url' => 'sanitize_url', 'is_auth_require' => 'intval', 'crm_contact_id' => 'intval', 'id' => 'sanitize_text_field', 'is_image' => 'rest_sanitize_boolean', 'color' => 'sanitize_text_field', // sanitize_hex_color doesn't work when color code is greater than 6 characters 'reset' => 'rest_sanitize_boolean', 'created_by' => 'intval', ]; return self::sanitizeData($data, $fieldMaps); } public static function sanitizeStage($data) { $fieldMaps = [ 'title' => 'sanitize_text_field', 'slug' => 'sanitize_text_field', 'type' => 'sanitize_text_field', 'status' => 'sanitize_text_field', ]; return self::sanitizeData($data, $fieldMaps); } public static function sanitizeComment($data) { $fieldMaps = [ 'description' => 'wp_kses_post', 'created_by' => 'intval', 'task_id' => 'intval', 'type' => 'sanitize_text_field', ]; return self::sanitizeData($data, $fieldMaps); } public static function sanitizeLabel($data) { $fieldMaps = [ 'bg_color' => 'sanitize_text_field', 'color' => 'sanitize_text_field', 'label' => 'sanitize_text_field', 'boardId' => 'intval', 'task_id' => 'intval', 'meta_value' => 'intval', ]; return self::sanitizeData($data, $fieldMaps); } public static function sanitizeSubtask($data) { $fieldMaps = [ 'title' => 'sanitize_text_field', 'stage' => 'sanitize_text_field', 'newPosition' => 'intval', 'priority' => 'sanitize_text_field', 'type' => 'sanitize_text_field', 'description' => 'fluent_boards_sanitize_description', 'group_id' => 'intval', 'board_id' => 'intval', 'created_by' => 'intval', 'due_date' => 'sanitize_text_field', 'due_at' => 'sanitize_text_field', 'started_at' => 'sanitize_text_field', 'reminder_type' => 'sanitize_text_field', 'remind_at' => 'sanitize_text_field', 'add_to_top' => 'rest_sanitize_boolean', ]; $data = self::sanitizeData($data, $fieldMaps); if (!empty($data['assignees']) && is_array($data['assignees'])) { $data['assignees'] = array_slice(array_filter(array_map('intval', $data['assignees'])), 0, 1); } if (!empty($data['labels']) && is_array($data['labels'])) { $data['labels'] = array_filter(array_map('intval', $data['labels'])); } return $data; } public static function createActivity($data) { return Activity::create($data); } public static function sanitizeTaskMeta($data) { $fieldMaps = [ 'title' => 'sanitize_text_field', 'url' => 'sanitize_url', ]; return self::sanitizeData($data, $fieldMaps); } public static function getFormattedStagesByBoardId($boardId) { if (!$boardId) { return []; } $board = Board::findOrFail($boardId); $stages = $board->stages()->get(); // return static::formateStage($stages); return $stages; } public static function getStagesByBoardId($boardId) { if (!$boardId) { return []; } $board = Board::findOrFail($boardId); $stages = $board->stages()->get(); return static::formateStage($stages); // return $stages; } public static function formateStage($stages) { if (!$stages) return []; $formattedStages = []; foreach ($stages as $stage) { $boardName = $stage->board->title; $formattedStages[] = [ 'id' => strval($stage->id), 'title' => $boardName . ' - ' . $stage->title ]; } return $formattedStages; } public static function getIdTitleArray($data) { $array = []; foreach ($data as $item) { $array[] = [ 'id' => $item->id, 'title' => $item->title ]; } return $array; } // get Task Url by task id and board id public static function getTaskUrl($taskId, $boardId): string { if (!$taskId || !$boardId) { return ''; } return fluent_boards_page_url() . 'boards/' . $boardId . '/tasks/' . $taskId; } public static function getTaskUrlByTask($task): string { if (!$task) { return ''; } return fluent_boards_page_url() . 'boards/' . $task->board_id . '/tasks/' . $task->id; } public static function getBoardUrl($boardId): string { if (!$boardId) { return ''; } return fluent_boards_page_url() . 'boards/' . $boardId; } public static function crm_contact($id) { if (!defined('FLUENTCRM')) { return ''; } $contact = \FluentCrm\App\Models\Subscriber::with(['tags', 'lists'])->find($id); if (!$contact) { return null; } return [ 'id' => $contact->id, 'email' => $contact->email, 'first_name' => $contact->first_name, 'last_name' => $contact->last_name, 'full_name' => $contact->full_name, 'avatar' => $contact->avatar, 'photo' => $contact->photo, 'status' => $contact->status, 'contact_type' => $contact->contact_type, 'last_activity' => $contact->last_activity, 'life_time_value' => $contact->life_time_value, 'total_points' => $contact->total_points, 'user_id' => $contact->user_id, 'created_at' => $contact->created_at, 'tags' => Helper::getIdTitleArray($contact->tags), 'lists' => Helper::getIdTitleArray($contact->lists) ]; } public static function getStagesByBoardGroup() { $boards = self::getBoards(); $groups = []; foreach ($boards as $board) { $groups[] = [ 'title' => $board->title, 'slug' => 'aaa' . '_' . $board->id, 'options' => self::getStagesByBoardId($board->id) ]; } return $groups; } public static function getBoards() { return Board::orderBy('created_at', 'ASC')->get(); } public static function getStage($stageId) { return Stage::findOrFail($stageId); } public static function getBoardByStageId($stageId) { $stage = self::getStage($stageId); return $stage->board; } public static function sanitizeUserCollections($users) { if (empty($users)) { return $users; } foreach ($users as $key => $user) { if (is_object($user) && isset($user->pivot)) { $settings = maybe_unserialize($user->pivot->settings); $user->role = Arr::get($settings, 'is_admin') ? 'Admin' : (Arr::has($settings, 'is_viewer_only') && Arr::get($settings, 'is_viewer_only') ? 'Viewer' : 'Member'); } } if (current_user_can('list_users')) { return $users; } if (is_object($users) && method_exists($users, 'makeHidden')) { $users->makeHidden(['user_email', 'user_nicename', 'user_registered', 'user_url', 'user_status']); } elseif (is_array($users)) { foreach ($users as &$user) { if (is_array($user)) { unset($user['user_email'], $user['user_nicename'], $user['user_registered'], $user['user_url'], $user['user_status']); } } unset($user); } return $users; } // Callers formatting multiple lists may supply a resolved board-manager result. public static function sanitizeUsersArray($users, $boardId = null, $isBoardManager = null) { if (current_user_can('list_users')) { return $users; } $sanitizedUsers = []; if (!($isBoardManager ?? PermissionManager::isBoardManager($boardId))) { $currentUser = wp_get_current_user(); if($currentUser && isset($currentUser->user_email)){ $currentUserEmail = $currentUser->user_email; } foreach ($users as $user) { if($user['email'] === $currentUserEmail){ $sanitizedUsers[] = $user; continue; } if (isset($user['email'])) { $user['email'] = self::obfuscateEmail($user['email']); } if (isset($user['user_email'])) { $user['user_email'] = self::obfuscateEmail($user['user_email']); } $sanitizedUsers[] = $user; } } else { foreach ($users as $user) { $sanitizedUsers[] = $user; } } return $sanitizedUsers; } public static function obfuscateEmail($email) { if (!is_string($email) || filter_var($email, FILTER_VALIDATE_EMAIL) === false) { return $email; // Not a valid email, return as is } list($name, $domain) = explode('@', $email, 2); // Local part: show first 3 chars, then fixed **** $visibleLocal = substr($name, 0, 3); $maskedLocal = $visibleLocal . '****'; // Domain: mask the main label to **** + last 2 chars, keep rest (TLDs) intact $domainParts = explode('.', $domain); $mainLabel = $domainParts[0] ?? ''; $tail = strlen($mainLabel) >= 2 ? substr($mainLabel, -2) : $mainLabel; $domainParts[0] = '****' . $tail; // e.g., example.com -> ****le.com $maskedDomain = implode('.', $domainParts); return $maskedLocal . '@' . $maskedDomain; } public static function getPriorityOptions() { return [ [ 'id' => '', 'title' => 'No priority' ], [ 'id' => 'urgent', 'title' => 'Urgent' ], [ 'id' => 'high', 'title' => 'High' ], [ 'id' => 'medium', 'title' => 'Medium' ], [ 'id' => 'low', 'title' => 'Low' ], ]; } public static function dueDateConversion($due_time, $unit) { if($due_time <= 0) { return null; } $currentTime = current_time('mysql'); $readyString = '+' . $due_time . ' ' . $unit; return gmdate('Y-m-d H:i:s',strtotime($readyString,strtotime($currentTime))); } public static function searchWordPressUsers($searchQuery, $limit = 20) { $search = sanitize_text_field($searchQuery); // Search by user login, email, and nicename $args = array( 'role' => '', 'search' => '*' . $search . '*', 'number' => $limit, ); // Get users by login, email, and nicename $user_query = new \WP_User_Query($args); $users_by_login = $user_query->get_results(); // Search by first name and last name $meta_query_args = array( 'relation' => 'OR', array( 'key' => 'first_name', 'value' => $search, 'compare' => 'LIKE', ), array( 'key' => 'last_name', 'value' => $search, 'compare' => 'LIKE', ), ); $meta_query = array( 'role' => '', 'meta_query' => $meta_query_args, 'number' => $limit, ); // Get users by first name and last name $users_by_meta = get_users($meta_query); // Merge and remove duplicates $users = array_merge($users_by_login, $users_by_meta); $users = array_unique($users, SORT_REGULAR); return $users; } public static function sanitizeTaskAttachment($data) { $fieldMaps = [ 'title' => 'sanitize_text_field', 'url' => 'sanitize_url', ]; return self::sanitizeData($data, $fieldMaps); } public static function sanitizeTaskRepeatData($data) { $fieldMaps = [ 'create_new' => 'intval', 'repeat_in' => 'intval', 'repeat_type' => 'sanitize_text_field', 'repeat_when_complete' => 'intval', 'selected_month' => 'sanitize_text_field', 'selected_stage' => 'intval', 'board_id' => 'intval', 'time' => 'sanitize_text_field', 'time_zone' => 'sanitize_text_field', 'next_repeat_date' => 'sanitize_text_field', 'repeat_in_month_type' => 'sanitize_text_field', ]; return self::sanitizeData($data, $fieldMaps); } /** * Sanitize the author snapshot supplied by an external task integration. * * @param mixed $author * @return array */ private static function sanitizeExternalTaskAuthor($author) { if (!is_array($author)) { return []; } return array_filter([ 'name' => sanitize_text_field($author['name'] ?? ''), 'email' => sanitize_email($author['email'] ?? ''), 'photo' => esc_url_raw($author['photo'] ?? ''), ]); } public static function sanitizeTaskForWebHook($data) { $fieldMaps = [ 'title' => 'sanitize_text_field', 'board_id' => 'intval', 'parent_id' => 'intval', 'crm_contact_id' => 'intval', 'type' => 'sanitize_text_field', 'stage' => 'sanitize_text_field', 'reminder_type' => 'sanitize_text_field', 'priority' => 'sanitize_text_field', 'lead_value' => 'doubleval', 'remind_at' => 'sanitize_text_field', 'scope' => 'sanitize_text_field', 'source' => 'sanitize_text_field', 'source_id' => 'sanitize_text_field', 'description' => 'fluent_boards_sanitize_description', 'due_date' => 'sanitize_text_field', 'start_at' => 'sanitize_text_field', 'log_minutes' => 'sanitize_text_field', 'last_completed' => 'sanitize_text_field', 'is_archived' => 'intval', 'previous_stage' => 'sanitize_text_field', 'new_stage' => 'sanitize_text_field', 'new_index' => 'intval', 'old_index' => 'intval', 'new_board_id' => 'intval', 'position' => 'intval' ]; $data = self::sanitizeData($data, $fieldMaps); if (isset($data['settings']) && is_array($data['settings']) && isset($data['settings']['author'])) { $data['settings'] = [ 'author' => self::sanitizeExternalTaskAuthor($data['settings']['author']), ]; } else { unset($data['settings']); } return $data; } public static function taskReminderTypes() { $allowedTypes = [ '30_minutes_before' => __('30 minutes before', 'fluent-boards'), '1_hour_before' => __('1 hour before', 'fluent-boards'), '2_hours_before' => __('2 hours before', 'fluent-boards'), '1_day_before' => __('1 day before', 'fluent-boards'), '2_days_before' => __('2 days before', 'fluent-boards'), '1_week_before' => __('1 week before', 'fluent-boards'), ]; $allowedTypes = apply_filters('fluent_boards/task_reminder_types', $allowedTypes); return $allowedTypes; } public static function translateActivities($activities) { $actionTranslations = [ 'changed' => __('changed', 'fluent-boards'), 'updated' => __('updated', 'fluent-boards'), 'added' => __('added', 'fluent-boards'), 'removed' => __('removed', 'fluent-boards'), 'created' => __('created', 'fluent-boards'), 'closed' => __('closed', 'fluent-boards'), 'reopened' => __('reopened', 'fluent-boards'), 'joined' => __('joined', 'fluent-boards'), 'left' => __('left', 'fluent-boards'), 'cloned' => __('cloned', 'fluent-boards'), 'deleted' => __('deleted', 'fluent-boards'), 'archived' => __('archived', 'fluent-boards'), 'restored' => __('restored', 'fluent-boards'), 'set' => __('set', 'fluent-boards'), 'moved' => __('moved', 'fluent-boards'), ]; $columnTranslations = [ 'task' => __('task', 'fluent-boards'), 'description' => __('description', 'fluent-boards'), 'board' => __('board', 'fluent-boards'), 'assignee' => __('assignee', 'fluent-boards'), 'label' => __('label', 'fluent-boards'), 'Due Date' => __('Due Date', 'fluent-boards'), 'Start Date' => __('Start Date', 'fluent-boards'), 'priority' => __('priority', 'fluent-boards'), 'comment' => __('comment', 'fluent-boards'), 'a reply' => __('a reply', 'fluent-boards'), 'subtask' => __('subtask', 'fluent-boards'), 'subtask group' => __('subtask group', 'fluent-boards'), 'subtask group title' => __('subtask group title', 'fluent-boards'), 'stage' => __('stage', 'fluent-boards'), 'the associate email' => __('the associate email', 'fluent-boards'), 'attachment' => __('attachment', 'fluent-boards'), 'repeat task' => __('repeat task', 'fluent-boards'), 'Repeat Task' => __('Repeat Task', 'fluent-boards'), 'tasks' => __('tasks', 'fluent-boards'), ]; foreach ($activities as $activity) { $activity->action_key = $activity->action; $activity->column_key = $activity->column; if (isset($actionTranslations[$activity->action])) { $activity->action = $actionTranslations[$activity->action]; } if (isset($columnTranslations[$activity->column])) { $activity->column = $columnTranslations[$activity->column]; } } } }