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

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

748 lines 24.1 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 'color_preset' => 'sanitize_key',
203 'label' => 'sanitize_text_field',
204 'boardId' => 'intval',
205 'task_id' => 'intval',
206 'meta_value' => 'intval',
207 ];
208
209 return self::sanitizeData($data, $fieldMaps);
210 }
211
212 public static function sanitizeSubtask($data)
213 {
214 $fieldMaps = [
215 'title' => 'sanitize_text_field',
216 'stage' => 'sanitize_text_field',
217 'newPosition' => 'intval',
218 'priority' => 'sanitize_text_field',
219 'type' => 'sanitize_text_field',
220 'description' => 'fluent_boards_sanitize_description',
221 'group_id' => 'intval',
222 'board_id' => 'intval',
223 'created_by' => 'intval',
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',
230 ];
231
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;
243 }
244
245 public static function createActivity($data)
246 {
247 return Activity::create($data);
248 }
249
250 public static function sanitizeTaskMeta($data)
251 {
252 $fieldMaps = [
253 'title' => 'sanitize_text_field',
254 'url' => 'sanitize_url',
255
256 ];
257
258 return self::sanitizeData($data, $fieldMaps);
259 }
260
261 public static function getFormattedStagesByBoardId($boardId)
262 {
263 if (!$boardId) {
264 return [];
265 }
266
267 $board = Board::findOrFail($boardId);
268
269 $stages = $board->stages()->get();
270 // return static::formateStage($stages);
271 return $stages;
272 }
273
274 public static function getStagesByBoardId($boardId)
275 {
276 if (!$boardId) {
277 return [];
278 }
279
280 $board = Board::findOrFail($boardId);
281
282 $stages = $board->stages()->get();
283 return static::formateStage($stages);
284 // return $stages;
285 }
286
287 public static function formateStage($stages)
288 {
289 if (!$stages) return [];
290
291 $formattedStages = [];
292 foreach ($stages as $stage) {
293 $boardName = $stage->board->title;
294 $formattedStages[] = [
295 'id' => strval($stage->id),
296 'title' => $boardName . ' - ' . $stage->title
297 ];
298 }
299 return $formattedStages;
300 }
301
302 public static function getIdTitleArray($data)
303 {
304 $array = [];
305 foreach ($data as $item) {
306 $array[] = [
307 'id' => $item->id,
308 'title' => $item->title
309 ];
310 }
311 return $array;
312 }
313
314 // get Task Url by task id and board id
315 public static function getTaskUrl($taskId, $boardId): string
316 {
317 if (!$taskId || !$boardId) {
318 return '';
319 }
320 return fluent_boards_page_url() . 'boards/' . $boardId . '/tasks/' . $taskId;
321 }
322
323 public static function getTaskUrlByTask($task): string
324 {
325 if (!$task) {
326 return '';
327 }
328 return fluent_boards_page_url() . 'boards/' . $task->board_id . '/tasks/' . $task->id;
329 }
330
331 public static function getBoardUrl($boardId): string
332 {
333 if (!$boardId) {
334 return '';
335 }
336 return fluent_boards_page_url() . 'boards/' . $boardId;
337 }
338
339 public static function crm_contact($id)
340 {
341 if (!defined('FLUENTCRM')) {
342 return '';
343 }
344
345 $contact = \FluentCrm\App\Models\Subscriber::with(['tags', 'lists'])->find($id);
346
347 if (!$contact) {
348 return null;
349 }
350
351 return [
352 'id' => $contact->id,
353 'email' => $contact->email,
354 'first_name' => $contact->first_name,
355 'last_name' => $contact->last_name,
356 'full_name' => $contact->full_name,
357 'avatar' => $contact->avatar,
358 'photo' => $contact->photo,
359 'status' => $contact->status,
360 'contact_type' => $contact->contact_type,
361 'last_activity' => $contact->last_activity,
362 'life_time_value' => $contact->life_time_value,
363 'total_points' => $contact->total_points,
364 'user_id' => $contact->user_id,
365 'created_at' => $contact->created_at,
366 'tags' => Helper::getIdTitleArray($contact->tags),
367 'lists' => Helper::getIdTitleArray($contact->lists)
368
369 ];
370
371 }
372
373 public static function getStagesByBoardGroup()
374 {
375 $boards = self::getBoards();
376
377 $groups = [];
378 foreach ($boards as $board) {
379 $groups[] = [
380 'title' => $board->title,
381 'slug' => 'aaa' . '_' . $board->id,
382 'options' => self::getStagesByBoardId($board->id)
383 ];
384 }
385 return $groups;
386 }
387
388 public static function getBoards()
389 {
390 return Board::orderBy('created_at', 'ASC')->get();
391 }
392
393 public static function getStage($stageId)
394 {
395 return Stage::findOrFail($stageId);
396 }
397
398 public static function getBoardByStageId($stageId)
399 {
400 $stage = self::getStage($stageId);
401 return $stage->board;
402 }
403
404 public static function sanitizeUserCollections($users)
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
421 if (current_user_can('list_users')) {
422 return $users;
423 }
424
425 if (is_object($users) && method_exists($users, 'makeHidden')) {
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);
434 }
435
436 return $users;
437 }
438
439 // Callers formatting multiple lists may supply a resolved board-manager result.
440 public static function sanitizeUsersArray($users, $boardId = null, $isBoardManager = null)
441 {
442 if (current_user_can('list_users')) {
443 return $users;
444 }
445
446 $sanitizedUsers = [];
447
448 if (!($isBoardManager ?? PermissionManager::isBoardManager($boardId)))
449 {
450 $currentUser = wp_get_current_user();
451 if($currentUser && isset($currentUser->user_email)){
452 $currentUserEmail = $currentUser->user_email;
453 }
454 foreach ($users as $user) {
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
468 $sanitizedUsers[] = $user;
469 }
470 } else {
471 foreach ($users as $user) {
472 $sanitizedUsers[] = $user;
473 }
474 }
475
476 return $sanitizedUsers;
477 }
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
501 public static function getPriorityOptions()
502 {
503 return [
504 [
505 'id' => '',
506 'title' => 'No priority'
507 ],
508 [
509 'id' => 'urgent',
510 'title' => 'Urgent'
511 ],
512 [
513 'id' => 'high',
514 'title' => 'High'
515 ],
516 [
517 'id' => 'medium',
518 'title' => 'Medium'
519 ],
520 [
521 'id' => 'low',
522 'title' => 'Low'
523 ],
524 ];
525 }
526
527 public static function dueDateConversion($due_time, $unit)
528 {
529 if($due_time <= 0) {
530 return null;
531 }
532 $currentTime = current_time('mysql');
533 $readyString = '+' . $due_time . ' ' . $unit;
534 return gmdate('Y-m-d H:i:s',strtotime($readyString,strtotime($currentTime)));
535 }
536
537 public static function searchWordPressUsers($searchQuery, $limit = 20)
538 {
539 $search = sanitize_text_field($searchQuery);
540
541 // Search by user login, email, and nicename
542 $args = array(
543 'role' => '',
544 'search' => '*' . $search . '*',
545 'number' => $limit,
546 );
547
548 // Get users by login, email, and nicename
549 $user_query = new \WP_User_Query($args);
550 $users_by_login = $user_query->get_results();
551
552 // Search by first name and last name
553 $meta_query_args = array(
554 'relation' => 'OR',
555 array(
556 'key' => 'first_name',
557 'value' => $search,
558 'compare' => 'LIKE',
559 ),
560 array(
561 'key' => 'last_name',
562 'value' => $search,
563 'compare' => 'LIKE',
564 ),
565 );
566
567 $meta_query = array(
568 'role' => '',
569 'meta_query' => $meta_query_args,
570 'number' => $limit,
571 );
572
573 // Get users by first name and last name
574 $users_by_meta = get_users($meta_query);
575
576 // Merge and remove duplicates
577 $users = array_merge($users_by_login, $users_by_meta);
578 $users = array_unique($users, SORT_REGULAR);
579
580 return $users;
581
582 }
583
584 public static function sanitizeTaskAttachment($data)
585 {
586 $fieldMaps = [
587 'title' => 'sanitize_text_field',
588 'url' => 'sanitize_url',
589 ];
590
591 return self::sanitizeData($data, $fieldMaps);
592 }
593
594 public static function sanitizeTaskRepeatData($data)
595 {
596 $fieldMaps = [
597 'create_new' => 'intval',
598 'repeat_in' => 'intval',
599 'repeat_type' => 'sanitize_text_field',
600 'repeat_when_complete' => 'intval',
601 'selected_month' => 'sanitize_text_field',
602 'selected_stage' => 'intval',
603 'board_id' => 'intval',
604 'time' => 'sanitize_text_field',
605 'time_zone' => 'sanitize_text_field',
606 'next_repeat_date' => 'sanitize_text_field',
607 'repeat_in_month_type' => 'sanitize_text_field',
608 ];
609
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 }
746 }
747 }
748