PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / trunk
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration vtrunk
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 1.41 1.45 All 41 releases
fluent-boards / app / Services / Helper.php

Helper.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration trunk, at app/Services/Helper.php

747 lines 24.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBoards\App\Services;
4
5 use FluentBoards\App\Models\Activity;
6 use FluentBoards\App\Models\Stage;
7 use FluentBoards\App\Models\Webhook;
8 use FluentBoards\Framework\Support\Arr;
9 use FluentBoards\Framework\Support\Str;
10 use FluentBoards\App\Models\Board;
11 use FluentBoards\App\Services\Parsedown;
12 use FluentBoards\App\Services\PermissionManager;
13
14 class Helper
15 {
16 public static function snake_case($string)
17 {
18 return strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $string));
19 }
20
21 public static function slugify($text, $id = '', $length = 20)
22 {
23 $text = substr($text, 0, $length); // limiting to max 20 chars
24 $text = Str::slug($text);
25 if ($id) {
26 $text = $id . '-' . $text;
27 }
28
29 return $text;
30 }
31
32 public static function loadView($template, $data)
33 {
34 extract($data, EXTR_OVERWRITE);
35
36 $template = sanitize_file_name($template);
37
38 $template = str_replace('.', DIRECTORY_SEPARATOR, $template);
39
40 ob_start();
41 include FLUENT_BOARDS_PLUGIN_PATH . 'app/Views/' . $template . '.php';
42
43 return ob_get_clean();
44 }
45
46 private static function sanitizeData($data, $fieldMaps)
47 {
48 foreach ($data as $key => $value) {
49 if ($value && isset($fieldMaps[$key]) && !is_array($value)) {
50 $data[$key] = call_user_func($fieldMaps[$key], $value);
51 }
52 }
53
54 return $data;
55 }
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
116 public static function sanitizeTask($data)
117 {
118 $fieldMaps = [
119 'title' => 'sanitize_text_field',
120 'board_id' => 'intval',
121 'parent_id' => 'intval',
122 'crm_contact_id' => 'intval',
123 'type' => 'sanitize_text_field',
124 'stage' => 'sanitize_text_field',
125 'reminder_type' => 'sanitize_text_field',
126 'priority' => 'sanitize_text_field',
127 'lead_value' => 'doubleval',
128 'remind_at' => 'sanitize_text_field',
129 'scope' => 'sanitize_text_field',
130 'source' => 'sanitize_text_field',
131 'source_id' => 'sanitize_text_field',
132 'description' => 'fluent_boards_sanitize_description',
133 'due_date' => 'sanitize_text_field',
134 'start_at' => 'sanitize_text_field',
135 'log_minutes' => 'sanitize_text_field',
136 'last_completed' => 'sanitize_text_field',
137 'assignees' => 'intval',
138 'is_archived' => 'intval',
139 'previous_stage' => 'sanitize_text_field',
140 'new_stage' => 'sanitize_text_field',
141 'new_index' => 'intval',
142 'old_index' => 'intval',
143 'new_board_id' => 'intval',
144 'position' => 'intval'
145
146 ];
147
148 return self::sanitizeData($data, $fieldMaps);
149 }
150
151 public static function sanitizeBoard($data)
152 {
153 $fieldMaps = [
154 'board_id' => 'intval',
155 'title' => 'sanitize_text_field',
156 'parent_id' => 'intval',
157 'type' => 'sanitize_text_field',
158 'description' => 'fluent_boards_sanitize_description',
159 'currency' => 'sanitize_text_field',
160 'image_url' => 'sanitize_url',
161 'is_auth_require' => 'intval',
162 'crm_contact_id' => 'intval',
163 'id' => 'sanitize_text_field',
164 'is_image' => 'rest_sanitize_boolean',
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',
168 ];
169
170 return self::sanitizeData($data, $fieldMaps);
171 }
172
173 public static function sanitizeStage($data)
174 {
175 $fieldMaps = [
176 'title' => 'sanitize_text_field',
177 'slug' => 'sanitize_text_field',
178 'type' => 'sanitize_text_field',
179 'status' => 'sanitize_text_field',
180 ];
181
182 return self::sanitizeData($data, $fieldMaps);
183 }
184
185 public static function sanitizeComment($data)
186 {
187 $fieldMaps = [
188 'description' => 'wp_kses_post',
189 'created_by' => 'intval',
190 'task_id' => 'intval',
191 'type' => 'sanitize_text_field',
192 ];
193
194 return self::sanitizeData($data, $fieldMaps);
195 }
196
197 public static function sanitizeLabel($data)
198 {
199 $fieldMaps = [
200 'bg_color' => 'sanitize_text_field',
201 'color' => 'sanitize_text_field',
202 'label' => 'sanitize_text_field',
203 'boardId' => 'intval',
204 'task_id' => 'intval',
205 'meta_value' => 'intval',
206 ];
207
208 return self::sanitizeData($data, $fieldMaps);
209 }
210
211 public static function sanitizeSubtask($data)
212 {
213 $fieldMaps = [
214 'title' => 'sanitize_text_field',
215 'stage' => 'sanitize_text_field',
216 'newPosition' => 'intval',
217 'priority' => 'sanitize_text_field',
218 'type' => 'sanitize_text_field',
219 'description' => 'fluent_boards_sanitize_description',
220 'group_id' => 'intval',
221 'board_id' => 'intval',
222 'created_by' => 'intval',
223 'due_date' => 'sanitize_text_field',
224 'due_at' => 'sanitize_text_field',
225 'started_at' => 'sanitize_text_field',
226 'reminder_type' => 'sanitize_text_field',
227 'remind_at' => 'sanitize_text_field',
228 'add_to_top' => 'rest_sanitize_boolean',
229 ];
230
231 $data = self::sanitizeData($data, $fieldMaps);
232
233 if (!empty($data['assignees']) && is_array($data['assignees'])) {
234 $data['assignees'] = array_slice(array_filter(array_map('intval', $data['assignees'])), 0, 1);
235 }
236
237 if (!empty($data['labels']) && is_array($data['labels'])) {
238 $data['labels'] = array_filter(array_map('intval', $data['labels']));
239 }
240
241 return $data;
242 }
243
244 public static function createActivity($data)
245 {
246 return Activity::create($data);
247 }
248
249 public static function sanitizeTaskMeta($data)
250 {
251 $fieldMaps = [
252 'title' => 'sanitize_text_field',
253 'url' => 'sanitize_url',
254
255 ];
256
257 return self::sanitizeData($data, $fieldMaps);
258 }
259
260 public static function getFormattedStagesByBoardId($boardId)
261 {
262 if (!$boardId) {
263 return [];
264 }
265
266 $board = Board::findOrFail($boardId);
267
268 $stages = $board->stages()->get();
269 // return static::formateStage($stages);
270 return $stages;
271 }
272
273 public static function getStagesByBoardId($boardId)
274 {
275 if (!$boardId) {
276 return [];
277 }
278
279 $board = Board::findOrFail($boardId);
280
281 $stages = $board->stages()->get();
282 return static::formateStage($stages);
283 // return $stages;
284 }
285
286 public static function formateStage($stages)
287 {
288 if (!$stages) return [];
289
290 $formattedStages = [];
291 foreach ($stages as $stage) {
292 $boardName = $stage->board->title;
293 $formattedStages[] = [
294 'id' => strval($stage->id),
295 'title' => $boardName . ' - ' . $stage->title
296 ];
297 }
298 return $formattedStages;
299 }
300
301 public static function getIdTitleArray($data)
302 {
303 $array = [];
304 foreach ($data as $item) {
305 $array[] = [
306 'id' => $item->id,
307 'title' => $item->title
308 ];
309 }
310 return $array;
311 }
312
313 // get Task Url by task id and board id
314 public static function getTaskUrl($taskId, $boardId): string
315 {
316 if (!$taskId || !$boardId) {
317 return '';
318 }
319 return fluent_boards_page_url() . 'boards/' . $boardId . '/tasks/' . $taskId;
320 }
321
322 public static function getTaskUrlByTask($task): string
323 {
324 if (!$task) {
325 return '';
326 }
327 return fluent_boards_page_url() . 'boards/' . $task->board_id . '/tasks/' . $task->id;
328 }
329
330 public static function getBoardUrl($boardId): string
331 {
332 if (!$boardId) {
333 return '';
334 }
335 return fluent_boards_page_url() . 'boards/' . $boardId;
336 }
337
338 public static function crm_contact($id)
339 {
340 if (!defined('FLUENTCRM')) {
341 return '';
342 }
343
344 $contact = \FluentCrm\App\Models\Subscriber::with(['tags', 'lists'])->find($id);
345
346 if (!$contact) {
347 return null;
348 }
349
350 return [
351 'id' => $contact->id,
352 'email' => $contact->email,
353 'first_name' => $contact->first_name,
354 'last_name' => $contact->last_name,
355 'full_name' => $contact->full_name,
356 'avatar' => $contact->avatar,
357 'photo' => $contact->photo,
358 'status' => $contact->status,
359 'contact_type' => $contact->contact_type,
360 'last_activity' => $contact->last_activity,
361 'life_time_value' => $contact->life_time_value,
362 'total_points' => $contact->total_points,
363 'user_id' => $contact->user_id,
364 'created_at' => $contact->created_at,
365 'tags' => Helper::getIdTitleArray($contact->tags),
366 'lists' => Helper::getIdTitleArray($contact->lists)
367
368 ];
369
370 }
371
372 public static function getStagesByBoardGroup()
373 {
374 $boards = self::getBoards();
375
376 $groups = [];
377 foreach ($boards as $board) {
378 $groups[] = [
379 'title' => $board->title,
380 'slug' => 'aaa' . '_' . $board->id,
381 'options' => self::getStagesByBoardId($board->id)
382 ];
383 }
384 return $groups;
385 }
386
387 public static function getBoards()
388 {
389 return Board::orderBy('created_at', 'ASC')->get();
390 }
391
392 public static function getStage($stageId)
393 {
394 return Stage::findOrFail($stageId);
395 }
396
397 public static function getBoardByStageId($stageId)
398 {
399 $stage = self::getStage($stageId);
400 return $stage->board;
401 }
402
403 public static function sanitizeUserCollections($users)
404 {
405 if (empty($users)) {
406 return $users;
407 }
408
409 foreach ($users as $key => $user) {
410 if (is_object($user) && isset($user->pivot)) {
411 $settings = maybe_unserialize($user->pivot->settings);
412 $user->role = Arr::get($settings, 'is_admin')
413 ? 'Admin'
414 : (Arr::has($settings, 'is_viewer_only') && Arr::get($settings, 'is_viewer_only')
415 ? 'Viewer'
416 : 'Member');
417 }
418 }
419
420 if (current_user_can('list_users')) {
421 return $users;
422 }
423
424 if (is_object($users) && method_exists($users, 'makeHidden')) {
425 $users->makeHidden(['user_email', 'user_nicename', 'user_registered', 'user_url', 'user_status']);
426 } elseif (is_array($users)) {
427 foreach ($users as &$user) {
428 if (is_array($user)) {
429 unset($user['user_email'], $user['user_nicename'], $user['user_registered'], $user['user_url'], $user['user_status']);
430 }
431 }
432 unset($user);
433 }
434
435 return $users;
436 }
437
438 // Callers formatting multiple lists may supply a resolved board-manager result.
439 public static function sanitizeUsersArray($users, $boardId = null, $isBoardManager = null)
440 {
441 if (current_user_can('list_users')) {
442 return $users;
443 }
444
445 $sanitizedUsers = [];
446
447 if (!($isBoardManager ?? PermissionManager::isBoardManager($boardId)))
448 {
449 $currentUser = wp_get_current_user();
450 if($currentUser && isset($currentUser->user_email)){
451 $currentUserEmail = $currentUser->user_email;
452 }
453 foreach ($users as $user) {
454
455 if($user['email'] === $currentUserEmail){
456 $sanitizedUsers[] = $user;
457 continue;
458 }
459
460 if (isset($user['email'])) {
461 $user['email'] = self::obfuscateEmail($user['email']);
462 }
463 if (isset($user['user_email'])) {
464 $user['user_email'] = self::obfuscateEmail($user['user_email']);
465 }
466
467 $sanitizedUsers[] = $user;
468 }
469 } else {
470 foreach ($users as $user) {
471 $sanitizedUsers[] = $user;
472 }
473 }
474
475 return $sanitizedUsers;
476 }
477
478 public static function obfuscateEmail($email)
479 {
480 if (!is_string($email) || filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
481 return $email; // Not a valid email, return as is
482 }
483
484 list($name, $domain) = explode('@', $email, 2);
485
486 // Local part: show first 3 chars, then fixed ****
487 $visibleLocal = substr($name, 0, 3);
488 $maskedLocal = $visibleLocal . '****';
489
490 // Domain: mask the main label to **** + last 2 chars, keep rest (TLDs) intact
491 $domainParts = explode('.', $domain);
492 $mainLabel = $domainParts[0] ?? '';
493 $tail = strlen($mainLabel) >= 2 ? substr($mainLabel, -2) : $mainLabel;
494 $domainParts[0] = '****' . $tail; // e.g., example.com -> ****le.com
495 $maskedDomain = implode('.', $domainParts);
496
497 return $maskedLocal . '@' . $maskedDomain;
498 }
499
500 public static function getPriorityOptions()
501 {
502 return [
503 [
504 'id' => '',
505 'title' => 'No priority'
506 ],
507 [
508 'id' => 'urgent',
509 'title' => 'Urgent'
510 ],
511 [
512 'id' => 'high',
513 'title' => 'High'
514 ],
515 [
516 'id' => 'medium',
517 'title' => 'Medium'
518 ],
519 [
520 'id' => 'low',
521 'title' => 'Low'
522 ],
523 ];
524 }
525
526 public static function dueDateConversion($due_time, $unit)
527 {
528 if($due_time <= 0) {
529 return null;
530 }
531 $currentTime = current_time('mysql');
532 $readyString = '+' . $due_time . ' ' . $unit;
533 return gmdate('Y-m-d H:i:s',strtotime($readyString,strtotime($currentTime)));
534 }
535
536 public static function searchWordPressUsers($searchQuery, $limit = 20)
537 {
538 $search = sanitize_text_field($searchQuery);
539
540 // Search by user login, email, and nicename
541 $args = array(
542 'role' => '',
543 'search' => '*' . $search . '*',
544 'number' => $limit,
545 );
546
547 // Get users by login, email, and nicename
548 $user_query = new \WP_User_Query($args);
549 $users_by_login = $user_query->get_results();
550
551 // Search by first name and last name
552 $meta_query_args = array(
553 'relation' => 'OR',
554 array(
555 'key' => 'first_name',
556 'value' => $search,
557 'compare' => 'LIKE',
558 ),
559 array(
560 'key' => 'last_name',
561 'value' => $search,
562 'compare' => 'LIKE',
563 ),
564 );
565
566 $meta_query = array(
567 'role' => '',
568 'meta_query' => $meta_query_args,
569 'number' => $limit,
570 );
571
572 // Get users by first name and last name
573 $users_by_meta = get_users($meta_query);
574
575 // Merge and remove duplicates
576 $users = array_merge($users_by_login, $users_by_meta);
577 $users = array_unique($users, SORT_REGULAR);
578
579 return $users;
580
581 }
582
583 public static function sanitizeTaskAttachment($data)
584 {
585 $fieldMaps = [
586 'title' => 'sanitize_text_field',
587 'url' => 'sanitize_url',
588 ];
589
590 return self::sanitizeData($data, $fieldMaps);
591 }
592
593 public static function sanitizeTaskRepeatData($data)
594 {
595 $fieldMaps = [
596 'create_new' => 'intval',
597 'repeat_in' => 'intval',
598 'repeat_type' => 'sanitize_text_field',
599 'repeat_when_complete' => 'intval',
600 'selected_month' => 'sanitize_text_field',
601 'selected_stage' => 'intval',
602 'board_id' => 'intval',
603 'time' => 'sanitize_text_field',
604 'time_zone' => 'sanitize_text_field',
605 'next_repeat_date' => 'sanitize_text_field',
606 'repeat_in_month_type' => 'sanitize_text_field',
607 ];
608
609 return self::sanitizeData($data, $fieldMaps);
610 }
611
612 /**
613 * Sanitize the author snapshot supplied by an external task integration.
614 *
615 * @param mixed $author
616 * @return array
617 */
618 private static function sanitizeExternalTaskAuthor($author)
619 {
620 if (!is_array($author)) {
621 return [];
622 }
623
624 return array_filter([
625 'name' => sanitize_text_field($author['name'] ?? ''),
626 'email' => sanitize_email($author['email'] ?? ''),
627 'photo' => esc_url_raw($author['photo'] ?? ''),
628 ]);
629 }
630
631 public static function sanitizeTaskForWebHook($data)
632 {
633 $fieldMaps = [
634 'title' => 'sanitize_text_field',
635 'board_id' => 'intval',
636 'parent_id' => 'intval',
637 'crm_contact_id' => 'intval',
638 'type' => 'sanitize_text_field',
639 'stage' => 'sanitize_text_field',
640 'reminder_type' => 'sanitize_text_field',
641 'priority' => 'sanitize_text_field',
642 'lead_value' => 'doubleval',
643 'remind_at' => 'sanitize_text_field',
644 'scope' => 'sanitize_text_field',
645 'source' => 'sanitize_text_field',
646 'source_id' => 'sanitize_text_field',
647 'description' => 'fluent_boards_sanitize_description',
648 'due_date' => 'sanitize_text_field',
649 'start_at' => 'sanitize_text_field',
650 'log_minutes' => 'sanitize_text_field',
651 'last_completed' => 'sanitize_text_field',
652 'is_archived' => 'intval',
653 'previous_stage' => 'sanitize_text_field',
654 'new_stage' => 'sanitize_text_field',
655 'new_index' => 'intval',
656 'old_index' => 'intval',
657 'new_board_id' => 'intval',
658 'position' => 'intval'
659
660 ];
661
662 $data = self::sanitizeData($data, $fieldMaps);
663
664 if (isset($data['settings']) && is_array($data['settings']) && isset($data['settings']['author'])) {
665 $data['settings'] = [
666 'author' => self::sanitizeExternalTaskAuthor($data['settings']['author']),
667 ];
668 } else {
669 unset($data['settings']);
670 }
671
672 return $data;
673 }
674
675
676 public static function taskReminderTypes()
677 {
678 $allowedTypes = [
679 '30_minutes_before' => __('30 minutes before', 'fluent-boards'),
680 '1_hour_before' => __('1 hour before', 'fluent-boards'),
681 '2_hours_before' => __('2 hours before', 'fluent-boards'),
682 '1_day_before' => __('1 day before', 'fluent-boards'),
683 '2_days_before' => __('2 days before', 'fluent-boards'),
684 '1_week_before' => __('1 week before', 'fluent-boards'),
685 ];
686
687 $allowedTypes = apply_filters('fluent_boards/task_reminder_types', $allowedTypes);
688
689 return $allowedTypes;
690 }
691
692 public static function translateActivities($activities)
693 {
694 $actionTranslations = [
695 'changed' => __('changed', 'fluent-boards'),
696 'updated' => __('updated', 'fluent-boards'),
697 'added' => __('added', 'fluent-boards'),
698 'removed' => __('removed', 'fluent-boards'),
699 'created' => __('created', 'fluent-boards'),
700 'closed' => __('closed', 'fluent-boards'),
701 'reopened' => __('reopened', 'fluent-boards'),
702 'joined' => __('joined', 'fluent-boards'),
703 'left' => __('left', 'fluent-boards'),
704 'cloned' => __('cloned', 'fluent-boards'),
705 'deleted' => __('deleted', 'fluent-boards'),
706 'archived' => __('archived', 'fluent-boards'),
707 'restored' => __('restored', 'fluent-boards'),
708 'set' => __('set', 'fluent-boards'),
709 'moved' => __('moved', 'fluent-boards'),
710 ];
711
712 $columnTranslations = [
713 'task' => __('task', 'fluent-boards'),
714 'description' => __('description', 'fluent-boards'),
715 'board' => __('board', 'fluent-boards'),
716 'assignee' => __('assignee', 'fluent-boards'),
717 'label' => __('label', 'fluent-boards'),
718 'Due Date' => __('Due Date', 'fluent-boards'),
719 'Start Date' => __('Start Date', 'fluent-boards'),
720 'priority' => __('priority', 'fluent-boards'),
721 'comment' => __('comment', 'fluent-boards'),
722 'a reply' => __('a reply', 'fluent-boards'),
723 'subtask' => __('subtask', 'fluent-boards'),
724 'subtask group' => __('subtask group', 'fluent-boards'),
725 'subtask group title' => __('subtask group title', 'fluent-boards'),
726 'stage' => __('stage', 'fluent-boards'),
727 'the associate email' => __('the associate email', 'fluent-boards'),
728 'attachment' => __('attachment', 'fluent-boards'),
729 'repeat task' => __('repeat task', 'fluent-boards'),
730 'Repeat Task' => __('Repeat Task', 'fluent-boards'),
731 'tasks' => __('tasks', 'fluent-boards'),
732 ];
733
734 foreach ($activities as $activity) {
735 $activity->action_key = $activity->action;
736 $activity->column_key = $activity->column;
737
738 if (isset($actionTranslations[$activity->action])) {
739 $activity->action = $actionTranslations[$activity->action];
740 }
741 if (isset($columnTranslations[$activity->column])) {
742 $activity->column = $columnTranslations[$activity->column];
743 }
744 }
745 }
746 }
747