PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.0.1
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.0.1
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 2.0.1, at app/Services/Helper.php

743 lines 23.7 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' => 'wp_kses_post',
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' => 'wp_kses_post',
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' => 'wp_kses_post',
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 public static function sanitizeUsersArray($users, $boardId = null)
439 {
440 if (current_user_can('list_users')) {
441 return $users;
442 }
443
444 $sanitizedUsers = [];
445
446 if(!PermissionManager::isBoardManager($boardId)) //Todo: may create permission security issue, will be modified later
447 {
448 $currentUser = wp_get_current_user();
449 if($currentUser && isset($currentUser->user_email)){
450 $currentUserEmail = $currentUser->user_email;
451 }
452 foreach ($users as $user) {
453
454 if($user['email'] === $currentUserEmail){
455 $sanitizedUsers[] = $user;
456 continue;
457 }
458
459 if (isset($user['email'])) {
460 $user['email'] = self::obfuscateEmail($user['email']);
461 }
462 if (isset($user['user_email'])) {
463 $user['user_email'] = self::obfuscateEmail($user['user_email']);
464 }
465
466 $sanitizedUsers[] = $user;
467 }
468 } else {
469 foreach ($users as $user) {
470 $sanitizedUsers[] = $user;
471 }
472 }
473
474 return $sanitizedUsers;
475 }
476
477 public static function obfuscateEmail($email)
478 {
479 if (!is_string($email) || filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
480 return $email; // Not a valid email, return as is
481 }
482
483 list($name, $domain) = explode('@', $email, 2);
484
485 // Local part: show first 3 chars, then fixed ****
486 $visibleLocal = substr($name, 0, 3);
487 $maskedLocal = $visibleLocal . '****';
488
489 // Domain: mask the main label to **** + last 2 chars, keep rest (TLDs) intact
490 $domainParts = explode('.', $domain);
491 $mainLabel = $domainParts[0] ?? '';
492 $tail = strlen($mainLabel) >= 2 ? substr($mainLabel, -2) : $mainLabel;
493 $domainParts[0] = '****' . $tail; // e.g., example.com -> ****le.com
494 $maskedDomain = implode('.', $domainParts);
495
496 return $maskedLocal . '@' . $maskedDomain;
497 }
498
499 public static function getPriorityOptions()
500 {
501 return [
502 [
503 'id' => '',
504 'title' => 'No priority'
505 ],
506 [
507 'id' => 'urgent',
508 'title' => 'Urgent'
509 ],
510 [
511 'id' => 'high',
512 'title' => 'High'
513 ],
514 [
515 'id' => 'medium',
516 'title' => 'Medium'
517 ],
518 [
519 'id' => 'low',
520 'title' => 'Low'
521 ],
522 ];
523 }
524
525 public static function dueDateConversion($due_time, $unit)
526 {
527 if($due_time <= 0) {
528 return null;
529 }
530 $currentTime = current_time('mysql');
531 $readyString = '+' . $due_time . ' ' . $unit;
532 return gmdate('Y-m-d H:i:s',strtotime($readyString,strtotime($currentTime)));
533 }
534
535 public static function searchWordPressUsers($searchQuery, $limit = 20)
536 {
537 $search = sanitize_text_field($searchQuery);
538
539 // Search by user login, email, and nicename
540 $args = array(
541 'role' => '',
542 'search' => '*' . $search . '*',
543 'number' => $limit,
544 );
545
546 // Get users by login, email, and nicename
547 $user_query = new \WP_User_Query($args);
548 $users_by_login = $user_query->get_results();
549
550 // Search by first name and last name
551 $meta_query_args = array(
552 'relation' => 'OR',
553 array(
554 'key' => 'first_name',
555 'value' => $search,
556 'compare' => 'LIKE',
557 ),
558 array(
559 'key' => 'last_name',
560 'value' => $search,
561 'compare' => 'LIKE',
562 ),
563 );
564
565 $meta_query = array(
566 'role' => '',
567 'meta_query' => $meta_query_args,
568 'number' => $limit,
569 );
570
571 // Get users by first name and last name
572 $users_by_meta = get_users($meta_query);
573
574 // Merge and remove duplicates
575 $users = array_merge($users_by_login, $users_by_meta);
576 $users = array_unique($users, SORT_REGULAR);
577
578 return $users;
579
580 }
581
582 public static function sanitizeTaskAttachment($data)
583 {
584 $fieldMaps = [
585 'title' => 'sanitize_text_field',
586 'url' => 'sanitize_url',
587 ];
588
589 return self::sanitizeData($data, $fieldMaps);
590 }
591
592 public static function sanitizeTaskRepeatData($data)
593 {
594 $fieldMaps = [
595 'create_new' => 'intval',
596 'repeat_in' => 'intval',
597 'repeat_type' => 'sanitize_text_field',
598 'repeat_when_complete' => 'intval',
599 'selected_month' => 'sanitize_text_field',
600 'board_id' => 'intval',
601 'time' => 'sanitize_text_field',
602 'time_zone' => 'sanitize_text_field',
603 'next_repeat_date' => 'sanitize_text_field',
604 'repeat_in_month_type' => 'sanitize_text_field',
605 ];
606
607 return self::sanitizeData($data, $fieldMaps);
608 }
609
610 /**
611 * Sanitize the author snapshot supplied by an external task integration.
612 *
613 * @param mixed $author
614 * @return array
615 */
616 private static function sanitizeExternalTaskAuthor($author)
617 {
618 if (!is_array($author)) {
619 return [];
620 }
621
622 return array_filter([
623 'name' => sanitize_text_field($author['name'] ?? ''),
624 'email' => sanitize_email($author['email'] ?? ''),
625 'photo' => esc_url_raw($author['photo'] ?? ''),
626 ]);
627 }
628
629 public static function sanitizeTaskForWebHook($data)
630 {
631 $fieldMaps = [
632 'title' => 'sanitize_text_field',
633 'board_id' => 'intval',
634 'parent_id' => 'intval',
635 'crm_contact_id' => 'intval',
636 'type' => 'sanitize_text_field',
637 'stage' => 'sanitize_text_field',
638 'reminder_type' => 'sanitize_text_field',
639 'priority' => 'sanitize_text_field',
640 'lead_value' => 'doubleval',
641 'remind_at' => 'sanitize_text_field',
642 'scope' => 'sanitize_text_field',
643 'source' => 'sanitize_text_field',
644 'source_id' => 'sanitize_text_field',
645 'description' => 'wp_kses_post',
646 'due_date' => 'sanitize_text_field',
647 'start_at' => 'sanitize_text_field',
648 'log_minutes' => 'sanitize_text_field',
649 'last_completed' => 'sanitize_text_field',
650 'is_archived' => 'intval',
651 'previous_stage' => 'sanitize_text_field',
652 'new_stage' => 'sanitize_text_field',
653 'new_index' => 'intval',
654 'old_index' => 'intval',
655 'new_board_id' => 'intval',
656 'position' => 'intval'
657
658 ];
659
660 $data = self::sanitizeData($data, $fieldMaps);
661
662 if (isset($data['settings']) && is_array($data['settings']) && isset($data['settings']['author'])) {
663 $data['settings'] = [
664 'author' => self::sanitizeExternalTaskAuthor($data['settings']['author']),
665 ];
666 } else {
667 unset($data['settings']);
668 }
669
670 return $data;
671 }
672
673
674 public static function taskReminderTypes()
675 {
676 $allowedTypes = [
677 '30_minutes_before' => __('30 minutes before', 'fluent-boards'),
678 '1_hour_before' => __('1 hour before', 'fluent-boards'),
679 '2_hours_before' => __('2 hours before', 'fluent-boards'),
680 '1_day_before' => __('1 day before', 'fluent-boards'),
681 '2_days_before' => __('2 days before', 'fluent-boards'),
682 '1_week_before' => __('1 week before', 'fluent-boards'),
683 ];
684
685 $allowedTypes = apply_filters('fluent_boards/task_reminder_types', $allowedTypes);
686
687 return $allowedTypes;
688 }
689
690 public static function translateActivities($activities)
691 {
692 $actionTranslations = [
693 'changed' => __('changed', 'fluent-boards'),
694 'updated' => __('updated', 'fluent-boards'),
695 'added' => __('added', 'fluent-boards'),
696 'removed' => __('removed', 'fluent-boards'),
697 'created' => __('created', 'fluent-boards'),
698 'closed' => __('closed', 'fluent-boards'),
699 'reopened' => __('reopened', 'fluent-boards'),
700 'joined' => __('joined', 'fluent-boards'),
701 'left' => __('left', 'fluent-boards'),
702 'cloned' => __('cloned', 'fluent-boards'),
703 'deleted' => __('deleted', 'fluent-boards'),
704 'archived' => __('archived', 'fluent-boards'),
705 'restored' => __('restored', 'fluent-boards'),
706 'set' => __('set', 'fluent-boards'),
707 ];
708
709 $columnTranslations = [
710 'task' => __('task', 'fluent-boards'),
711 'description' => __('description', 'fluent-boards'),
712 'board' => __('board', 'fluent-boards'),
713 'assignee' => __('assignee', 'fluent-boards'),
714 'label' => __('label', 'fluent-boards'),
715 'Due Date' => __('Due Date', 'fluent-boards'),
716 'Start Date' => __('Start Date', 'fluent-boards'),
717 'priority' => __('priority', 'fluent-boards'),
718 'comment' => __('comment', 'fluent-boards'),
719 'a reply' => __('a reply', 'fluent-boards'),
720 'subtask' => __('subtask', 'fluent-boards'),
721 'subtask group' => __('subtask group', 'fluent-boards'),
722 'subtask group title' => __('subtask group title', 'fluent-boards'),
723 'stage' => __('stage', 'fluent-boards'),
724 'the associate email' => __('the associate email', 'fluent-boards'),
725 'attachment' => __('attachment', 'fluent-boards'),
726 'repeat task' => __('repeat task', 'fluent-boards'),
727 'Repeat Task' => __('Repeat Task', 'fluent-boards'),
728 ];
729
730 foreach ($activities as $activity) {
731 $activity->action_key = $activity->action;
732 $activity->column_key = $activity->column;
733
734 if (isset($actionTranslations[$activity->action])) {
735 $activity->action = $actionTranslations[$activity->action];
736 }
737 if (isset($columnTranslations[$activity->column])) {
738 $activity->column = $columnTranslations[$activity->column];
739 }
740 }
741 }
742 }
743