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

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

767 lines 24.9 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 /** Store full custom-field messages in LONGTEXT while keeping legacy VARCHAR columns bounded. */
246 public static function createActivity($data)
247 {
248 if (!empty($data['settings']['custom_field_id'])) {
249 $payload = [];
250 foreach (['column', 'old_value', 'new_value', 'description'] as $key) {
251 $payload[$key] = $data[$key] ?? null;
252 }
253 $data['settings']['custom_field_activity_payload'] = true;
254 $data['description'] = wp_json_encode($payload);
255 $data['column'] = Constant::ACTIVITY_CUSTOM_FIELD;
256 $data['old_value'] = null;
257 $data['new_value'] = null;
258 }
259
260 return Activity::create($data);
261 }
262
263 public static function sanitizeTaskMeta($data)
264 {
265 $fieldMaps = [
266 'title' => 'sanitize_text_field',
267 'url' => 'sanitize_url',
268
269 ];
270
271 return self::sanitizeData($data, $fieldMaps);
272 }
273
274 public static function getFormattedStagesByBoardId($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 getStagesByBoardId($boardId)
288 {
289 if (!$boardId) {
290 return [];
291 }
292
293 $board = Board::findOrFail($boardId);
294
295 $stages = $board->stages()->get();
296 return static::formateStage($stages);
297 // return $stages;
298 }
299
300 public static function formateStage($stages)
301 {
302 if (!$stages) return [];
303
304 $formattedStages = [];
305 foreach ($stages as $stage) {
306 $boardName = $stage->board->title;
307 $formattedStages[] = [
308 'id' => strval($stage->id),
309 'title' => $boardName . ' - ' . $stage->title
310 ];
311 }
312 return $formattedStages;
313 }
314
315 public static function getIdTitleArray($data)
316 {
317 $array = [];
318 foreach ($data as $item) {
319 $array[] = [
320 'id' => $item->id,
321 'title' => $item->title
322 ];
323 }
324 return $array;
325 }
326
327 // get Task Url by task id and board id
328 public static function getTaskUrl($taskId, $boardId): string
329 {
330 if (!$taskId || !$boardId) {
331 return '';
332 }
333 return fluent_boards_page_url() . 'boards/' . $boardId . '/tasks/' . $taskId;
334 }
335
336 public static function getTaskUrlByTask($task): string
337 {
338 if (!$task) {
339 return '';
340 }
341 return fluent_boards_page_url() . 'boards/' . $task->board_id . '/tasks/' . $task->id;
342 }
343
344 public static function getBoardUrl($boardId): string
345 {
346 if (!$boardId) {
347 return '';
348 }
349 return fluent_boards_page_url() . 'boards/' . $boardId;
350 }
351
352 /** Format a CRM contact, avoiding a database lookup for unassociated tasks. */
353 public static function crm_contact($id)
354 {
355 if (!defined('FLUENTCRM')) {
356 return '';
357 }
358
359 if (!$id) {
360 return null;
361 }
362
363 $contact = \FluentCrm\App\Models\Subscriber::with(['tags', 'lists'])->find($id);
364
365 if (!$contact) {
366 return null;
367 }
368
369 return [
370 'id' => $contact->id,
371 'email' => $contact->email,
372 'first_name' => $contact->first_name,
373 'last_name' => $contact->last_name,
374 'full_name' => $contact->full_name,
375 'avatar' => $contact->avatar,
376 'photo' => $contact->photo,
377 'status' => $contact->status,
378 'contact_type' => $contact->contact_type,
379 'last_activity' => $contact->last_activity,
380 'life_time_value' => $contact->life_time_value,
381 'total_points' => $contact->total_points,
382 'user_id' => $contact->user_id,
383 'created_at' => $contact->created_at,
384 'tags' => Helper::getIdTitleArray($contact->tags),
385 'lists' => Helper::getIdTitleArray($contact->lists)
386
387 ];
388
389 }
390
391 public static function getStagesByBoardGroup()
392 {
393 $boards = self::getBoards();
394
395 $groups = [];
396 foreach ($boards as $board) {
397 $groups[] = [
398 'title' => $board->title,
399 'slug' => 'aaa' . '_' . $board->id,
400 'options' => self::getStagesByBoardId($board->id)
401 ];
402 }
403 return $groups;
404 }
405
406 public static function getBoards()
407 {
408 return Board::orderBy('created_at', 'ASC')->get();
409 }
410
411 public static function getStage($stageId)
412 {
413 return Stage::findOrFail($stageId);
414 }
415
416 public static function getBoardByStageId($stageId)
417 {
418 $stage = self::getStage($stageId);
419 return $stage->board;
420 }
421
422 public static function sanitizeUserCollections($users)
423 {
424 if (empty($users)) {
425 return $users;
426 }
427
428 foreach ($users as $key => $user) {
429 if (is_object($user) && isset($user->pivot)) {
430 $settings = maybe_unserialize($user->pivot->settings);
431 $user->role = Arr::get($settings, 'is_admin')
432 ? 'Admin'
433 : (Arr::has($settings, 'is_viewer_only') && Arr::get($settings, 'is_viewer_only')
434 ? 'Viewer'
435 : 'Member');
436 }
437 }
438
439 if (current_user_can('list_users')) {
440 return $users;
441 }
442
443 if (is_object($users) && method_exists($users, 'makeHidden')) {
444 $users->makeHidden(['user_email', 'user_nicename', 'user_registered', 'user_url', 'user_status']);
445 } elseif (is_array($users)) {
446 foreach ($users as &$user) {
447 if (is_array($user)) {
448 unset($user['user_email'], $user['user_nicename'], $user['user_registered'], $user['user_url'], $user['user_status']);
449 }
450 }
451 unset($user);
452 }
453
454 return $users;
455 }
456
457 // Callers formatting multiple lists may supply a resolved board-manager result.
458 public static function sanitizeUsersArray($users, $boardId = null, $isBoardManager = null)
459 {
460 if (current_user_can('list_users')) {
461 return $users;
462 }
463
464 $sanitizedUsers = [];
465
466 if (!($isBoardManager ?? PermissionManager::isBoardManager($boardId)))
467 {
468 $currentUser = wp_get_current_user();
469 if($currentUser && isset($currentUser->user_email)){
470 $currentUserEmail = $currentUser->user_email;
471 }
472 foreach ($users as $user) {
473
474 if($user['email'] === $currentUserEmail){
475 $sanitizedUsers[] = $user;
476 continue;
477 }
478
479 if (isset($user['email'])) {
480 $user['email'] = self::obfuscateEmail($user['email']);
481 }
482 if (isset($user['user_email'])) {
483 $user['user_email'] = self::obfuscateEmail($user['user_email']);
484 }
485
486 $sanitizedUsers[] = $user;
487 }
488 } else {
489 foreach ($users as $user) {
490 $sanitizedUsers[] = $user;
491 }
492 }
493
494 return $sanitizedUsers;
495 }
496
497 public static function obfuscateEmail($email)
498 {
499 if (!is_string($email) || filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
500 return $email; // Not a valid email, return as is
501 }
502
503 list($name, $domain) = explode('@', $email, 2);
504
505 // Local part: show first 3 chars, then fixed ****
506 $visibleLocal = substr($name, 0, 3);
507 $maskedLocal = $visibleLocal . '****';
508
509 // Domain: mask the main label to **** + last 2 chars, keep rest (TLDs) intact
510 $domainParts = explode('.', $domain);
511 $mainLabel = $domainParts[0] ?? '';
512 $tail = strlen($mainLabel) >= 2 ? substr($mainLabel, -2) : $mainLabel;
513 $domainParts[0] = '****' . $tail; // e.g., example.com -> ****le.com
514 $maskedDomain = implode('.', $domainParts);
515
516 return $maskedLocal . '@' . $maskedDomain;
517 }
518
519 public static function getPriorityOptions()
520 {
521 return [
522 [
523 'id' => '',
524 'title' => 'No priority'
525 ],
526 [
527 'id' => 'urgent',
528 'title' => 'Urgent'
529 ],
530 [
531 'id' => 'high',
532 'title' => 'High'
533 ],
534 [
535 'id' => 'medium',
536 'title' => 'Medium'
537 ],
538 [
539 'id' => 'low',
540 'title' => 'Low'
541 ],
542 ];
543 }
544
545 public static function dueDateConversion($due_time, $unit)
546 {
547 if($due_time <= 0) {
548 return null;
549 }
550 $currentTime = current_time('mysql');
551 $readyString = '+' . $due_time . ' ' . $unit;
552 return gmdate('Y-m-d H:i:s',strtotime($readyString,strtotime($currentTime)));
553 }
554
555 public static function searchWordPressUsers($searchQuery, $limit = 20)
556 {
557 $search = sanitize_text_field($searchQuery);
558
559 // Search by user login, email, and nicename
560 $args = array(
561 'role' => '',
562 'search' => '*' . $search . '*',
563 'number' => $limit,
564 );
565
566 // Get users by login, email, and nicename
567 $user_query = new \WP_User_Query($args);
568 $users_by_login = $user_query->get_results();
569
570 // Search by first name and last name
571 $meta_query_args = array(
572 'relation' => 'OR',
573 array(
574 'key' => 'first_name',
575 'value' => $search,
576 'compare' => 'LIKE',
577 ),
578 array(
579 'key' => 'last_name',
580 'value' => $search,
581 'compare' => 'LIKE',
582 ),
583 );
584
585 $meta_query = array(
586 'role' => '',
587 'meta_query' => $meta_query_args,
588 'number' => $limit,
589 );
590
591 // Get users by first name and last name
592 $users_by_meta = get_users($meta_query);
593
594 // Merge and remove duplicates
595 $users = array_merge($users_by_login, $users_by_meta);
596 $users = array_unique($users, SORT_REGULAR);
597
598 return $users;
599
600 }
601
602 public static function sanitizeTaskAttachment($data)
603 {
604 $fieldMaps = [
605 'title' => 'sanitize_text_field',
606 'url' => 'sanitize_url',
607 ];
608
609 return self::sanitizeData($data, $fieldMaps);
610 }
611
612 public static function sanitizeTaskRepeatData($data)
613 {
614 $fieldMaps = [
615 'create_new' => 'intval',
616 'repeat_in' => 'intval',
617 'repeat_type' => 'sanitize_text_field',
618 'repeat_when_complete' => 'intval',
619 'selected_month' => 'sanitize_text_field',
620 'selected_stage' => 'intval',
621 'board_id' => 'intval',
622 'time' => 'sanitize_text_field',
623 'time_zone' => 'sanitize_text_field',
624 'next_repeat_date' => 'sanitize_text_field',
625 'repeat_in_month_type' => 'sanitize_text_field',
626 ];
627
628 return self::sanitizeData($data, $fieldMaps);
629 }
630
631 /**
632 * Sanitize the author snapshot supplied by an external task integration.
633 *
634 * @param mixed $author
635 * @return array
636 */
637 private static function sanitizeExternalTaskAuthor($author)
638 {
639 if (!is_array($author)) {
640 return [];
641 }
642
643 return array_filter([
644 'name' => sanitize_text_field($author['name'] ?? ''),
645 'email' => sanitize_email($author['email'] ?? ''),
646 'photo' => esc_url_raw($author['photo'] ?? ''),
647 ]);
648 }
649
650 public static function sanitizeTaskForWebHook($data)
651 {
652 $fieldMaps = [
653 'title' => 'sanitize_text_field',
654 'board_id' => 'intval',
655 'parent_id' => 'intval',
656 'crm_contact_id' => 'intval',
657 'type' => 'sanitize_text_field',
658 'stage' => 'sanitize_text_field',
659 'reminder_type' => 'sanitize_text_field',
660 'priority' => 'sanitize_text_field',
661 'lead_value' => 'doubleval',
662 'remind_at' => 'sanitize_text_field',
663 'scope' => 'sanitize_text_field',
664 'source' => 'sanitize_text_field',
665 'source_id' => 'sanitize_text_field',
666 'description' => 'fluent_boards_sanitize_description',
667 'due_date' => 'sanitize_text_field',
668 'start_at' => 'sanitize_text_field',
669 'log_minutes' => 'sanitize_text_field',
670 'last_completed' => 'sanitize_text_field',
671 'is_archived' => 'intval',
672 'previous_stage' => 'sanitize_text_field',
673 'new_stage' => 'sanitize_text_field',
674 'new_index' => 'intval',
675 'old_index' => 'intval',
676 'new_board_id' => 'intval',
677 'position' => 'intval'
678
679 ];
680
681 $data = self::sanitizeData($data, $fieldMaps);
682
683 if (isset($data['settings']) && is_array($data['settings']) && isset($data['settings']['author'])) {
684 $data['settings'] = [
685 'author' => self::sanitizeExternalTaskAuthor($data['settings']['author']),
686 ];
687 } else {
688 unset($data['settings']);
689 }
690
691 return $data;
692 }
693
694
695 public static function taskReminderTypes()
696 {
697 $allowedTypes = [
698 '30_minutes_before' => __('30 minutes before', 'fluent-boards'),
699 '1_hour_before' => __('1 hour before', 'fluent-boards'),
700 '2_hours_before' => __('2 hours before', 'fluent-boards'),
701 '1_day_before' => __('1 day before', 'fluent-boards'),
702 '2_days_before' => __('2 days before', 'fluent-boards'),
703 '1_week_before' => __('1 week before', 'fluent-boards'),
704 ];
705
706 $allowedTypes = apply_filters('fluent_boards/task_reminder_types', $allowedTypes);
707
708 return $allowedTypes;
709 }
710
711 public static function translateActivities($activities)
712 {
713 $actionTranslations = [
714 'changed' => __('changed', 'fluent-boards'),
715 'updated' => __('updated', 'fluent-boards'),
716 'added' => __('added', 'fluent-boards'),
717 'removed' => __('removed', 'fluent-boards'),
718 'created' => __('created', 'fluent-boards'),
719 'closed' => __('closed', 'fluent-boards'),
720 'reopened' => __('reopened', 'fluent-boards'),
721 'joined' => __('joined', 'fluent-boards'),
722 'left' => __('left', 'fluent-boards'),
723 'cloned' => __('cloned', 'fluent-boards'),
724 'deleted' => __('deleted', 'fluent-boards'),
725 'archived' => __('archived', 'fluent-boards'),
726 'restored' => __('restored', 'fluent-boards'),
727 'set' => __('set', 'fluent-boards'),
728 'moved' => __('moved', 'fluent-boards'),
729 ];
730
731 $columnTranslations = [
732 Constant::ACTIVITY_CUSTOM_FIELD => __('custom field', 'fluent-boards'),
733 'task' => __('task', 'fluent-boards'),
734 'description' => __('description', 'fluent-boards'),
735 'board' => __('board', 'fluent-boards'),
736 'assignee' => __('assignee', 'fluent-boards'),
737 'label' => __('label', 'fluent-boards'),
738 'Due Date' => __('Due Date', 'fluent-boards'),
739 'Start Date' => __('Start Date', 'fluent-boards'),
740 'priority' => __('priority', 'fluent-boards'),
741 'comment' => __('comment', 'fluent-boards'),
742 'a reply' => __('a reply', 'fluent-boards'),
743 'subtask' => __('subtask', 'fluent-boards'),
744 'subtask group' => __('subtask group', 'fluent-boards'),
745 'subtask group title' => __('subtask group title', 'fluent-boards'),
746 'stage' => __('stage', 'fluent-boards'),
747 'the associate email' => __('the associate email', 'fluent-boards'),
748 'attachment' => __('attachment', 'fluent-boards'),
749 'repeat task' => __('repeat task', 'fluent-boards'),
750 'Repeat Task' => __('Repeat Task', 'fluent-boards'),
751 'tasks' => __('tasks', 'fluent-boards'),
752 ];
753
754 foreach ($activities as $activity) {
755 $activity->action_key = $activity->action;
756 $activity->column_key = $activity->column;
757
758 if (isset($actionTranslations[$activity->action])) {
759 $activity->action = $actionTranslations[$activity->action];
760 }
761 if (isset($columnTranslations[$activity->column])) {
762 $activity->column = $columnTranslations[$activity->column];
763 }
764 }
765 }
766 }
767