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

684 lines 21.8 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 'created_by' => 'intval',
167 ];
168
169 return self::sanitizeData($data, $fieldMaps);
170 }
171
172 public static function sanitizeStage($data)
173 {
174 $fieldMaps = [
175 'title' => 'sanitize_text_field',
176 'slug' => 'sanitize_text_field',
177 'type' => 'sanitize_text_field',
178 'status' => 'sanitize_text_field',
179 ];
180
181 return self::sanitizeData($data, $fieldMaps);
182 }
183
184 public static function sanitizeComment($data)
185 {
186 $fieldMaps = [
187 'description' => 'wp_kses_post',
188 'created_by' => 'intval',
189 'task_id' => 'intval',
190 'type' => 'sanitize_text_field',
191 ];
192
193 return self::sanitizeData($data, $fieldMaps);
194 }
195
196 public static function sanitizeLabel($data)
197 {
198 $fieldMaps = [
199 'bg_color' => 'sanitize_text_field',
200 'color' => 'sanitize_text_field',
201 'label' => 'sanitize_text_field',
202 'boardId' => 'intval',
203 'task_id' => 'intval',
204 'meta_value' => 'intval',
205 ];
206
207 return self::sanitizeData($data, $fieldMaps);
208 }
209
210 public static function sanitizeSubtask($data)
211 {
212 $fieldMaps = [
213 'title' => 'sanitize_text_field',
214 'stage' => 'sanitize_text_field',
215 'newPosition' => 'intval',
216 'priority' => 'sanitize_text_field',
217 'type' => 'sanitize_text_field',
218 'board_id' => 'intval',
219 'created_by' => 'intval',
220 'due_date' => 'sanitize_text_field',
221 ];
222
223 return self::sanitizeData($data, $fieldMaps);
224 }
225
226 public static function createActivity($data)
227 {
228 return Activity::create($data);
229 }
230
231 public static function sanitizeTaskMeta($data)
232 {
233 $fieldMaps = [
234 'title' => 'sanitize_text_field',
235 'url' => 'sanitize_url',
236
237 ];
238
239 return self::sanitizeData($data, $fieldMaps);
240 }
241
242 public static function getFormattedStagesByBoardId($boardId)
243 {
244 if (!$boardId) {
245 return [];
246 }
247
248 $board = Board::findOrFail($boardId);
249
250 $stages = $board->stages()->get();
251 // return static::formateStage($stages);
252 return $stages;
253 }
254
255 public static function getStagesByBoardId($boardId)
256 {
257 if (!$boardId) {
258 return [];
259 }
260
261 $board = Board::findOrFail($boardId);
262
263 $stages = $board->stages()->get();
264 return static::formateStage($stages);
265 // return $stages;
266 }
267
268 public static function formateStage($stages)
269 {
270 if (!$stages) return [];
271
272 $formattedStages = [];
273 foreach ($stages as $stage) {
274 $boardName = $stage->board->title;
275 $formattedStages[] = [
276 'id' => strval($stage->id),
277 'title' => $boardName . ' - ' . $stage->title
278 ];
279 }
280 return $formattedStages;
281 }
282
283 public static function getIdTitleArray($data)
284 {
285 $array = [];
286 foreach ($data as $item) {
287 $array[] = [
288 'id' => $item->id,
289 'title' => $item->title
290 ];
291 }
292 return $array;
293 }
294
295 // get Task Url by task id and board id
296 public static function getTaskUrl($taskId, $boardId): string
297 {
298 if (!$taskId || !$boardId) {
299 return '';
300 }
301 return fluent_boards_page_url() . 'boards/' . $boardId . '/tasks/' . $taskId;
302 }
303
304 public static function getTaskUrlByTask($task): string
305 {
306 if (!$task) {
307 return '';
308 }
309 return fluent_boards_page_url() . 'boards/' . $task->board_id . '/tasks/' . $task->id;
310 }
311
312 public static function getBoardUrl($boardId): string
313 {
314 if (!$boardId) {
315 return '';
316 }
317 return fluent_boards_page_url() . 'boards/' . $boardId;
318 }
319
320 public static function crm_contact($id)
321 {
322 if (!defined('FLUENTCRM')) {
323 return '';
324 }
325
326 $contact = \FluentCrm\App\Models\Subscriber::with(['tags', 'lists'])->find($id);
327
328 if (!$contact) {
329 return null;
330 }
331
332 return [
333 'id' => $contact->id,
334 'email' => $contact->email,
335 'first_name' => $contact->first_name,
336 'last_name' => $contact->last_name,
337 'full_name' => $contact->full_name,
338 'avatar' => $contact->avatar,
339 'photo' => $contact->photo,
340 'status' => $contact->status,
341 'contact_type' => $contact->contact_type,
342 'last_activity' => $contact->last_activity,
343 'life_time_value' => $contact->life_time_value,
344 'total_points' => $contact->total_points,
345 'user_id' => $contact->user_id,
346 'created_at' => $contact->created_at,
347 'tags' => Helper::getIdTitleArray($contact->tags),
348 'lists' => Helper::getIdTitleArray($contact->lists)
349
350 ];
351
352 }
353
354 public static function getStagesByBoardGroup()
355 {
356 $boards = self::getBoards();
357
358 $groups = [];
359 foreach ($boards as $board) {
360 $groups[] = [
361 'title' => $board->title,
362 'slug' => 'aaa' . '_' . $board->id,
363 'options' => self::getStagesByBoardId($board->id)
364 ];
365 }
366 return $groups;
367 }
368
369 public static function getBoards()
370 {
371 return Board::orderBy('created_at', 'ASC')->get();
372 }
373
374 public static function getStage($stageId)
375 {
376 return Stage::findOrFail($stageId);
377 }
378
379 public static function getBoardByStageId($stageId)
380 {
381 $stage = self::getStage($stageId);
382 return $stage->board;
383 }
384
385 public static function sanitizeUserCollections($users)
386 {
387 if (empty($users)) {
388 return $users;
389 }
390
391 foreach ($users as $key => $user) {
392 if (is_object($user) && isset($user->pivot)) {
393 $settings = maybe_unserialize($user->pivot->settings);
394 $user->role = Arr::get($settings, 'is_admin')
395 ? 'Admin'
396 : (Arr::has($settings, 'is_viewer_only') && Arr::get($settings, 'is_viewer_only')
397 ? 'Viewer'
398 : 'Member');
399 }
400 }
401
402 if (current_user_can('list_users')) {
403 return $users;
404 }
405
406 if (is_object($users) && method_exists($users, 'makeHidden')) {
407 $users->makeHidden(['user_email', 'user_nicename', 'user_registered', 'user_url', 'user_status']);
408 } elseif (is_array($users)) {
409 foreach ($users as &$user) {
410 if (is_array($user)) {
411 unset($user['user_email'], $user['user_nicename'], $user['user_registered'], $user['user_url'], $user['user_status']);
412 }
413 }
414 unset($user);
415 }
416
417 return $users;
418 }
419
420 public static function sanitizeUsersArray($users, $boardId = null)
421 {
422 if (current_user_can('list_users')) {
423 return $users;
424 }
425
426 $sanitizedUsers = [];
427
428 if(!PermissionManager::isBoardManager($boardId)) //Todo: may create permission security issue, will be modified later
429 {
430 $currentUser = wp_get_current_user();
431 if($currentUser && isset($currentUser->user_email)){
432 $currentUserEmail = $currentUser->user_email;
433 }
434 foreach ($users as $user) {
435
436 if($user['email'] === $currentUserEmail){
437 $sanitizedUsers[] = $user;
438 continue;
439 }
440
441 if (isset($user['email'])) {
442 $user['email'] = self::obfuscateEmail($user['email']);
443 }
444 if (isset($user['user_email'])) {
445 $user['user_email'] = self::obfuscateEmail($user['user_email']);
446 }
447
448 $sanitizedUsers[] = $user;
449 }
450 } else {
451 foreach ($users as $user) {
452 $sanitizedUsers[] = $user;
453 }
454 }
455
456 return $sanitizedUsers;
457 }
458
459 public static function obfuscateEmail($email)
460 {
461 if (!is_string($email) || filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
462 return $email; // Not a valid email, return as is
463 }
464
465 list($name, $domain) = explode('@', $email, 2);
466
467 // Local part: show first 3 chars, then fixed ****
468 $visibleLocal = substr($name, 0, 3);
469 $maskedLocal = $visibleLocal . '****';
470
471 // Domain: mask the main label to **** + last 2 chars, keep rest (TLDs) intact
472 $domainParts = explode('.', $domain);
473 $mainLabel = $domainParts[0] ?? '';
474 $tail = strlen($mainLabel) >= 2 ? substr($mainLabel, -2) : $mainLabel;
475 $domainParts[0] = '****' . $tail; // e.g., example.com -> ****le.com
476 $maskedDomain = implode('.', $domainParts);
477
478 return $maskedLocal . '@' . $maskedDomain;
479 }
480
481 public static function getPriorityOptions()
482 {
483 return [
484 [
485 'id' => 'low',
486 'title' => 'Low'
487 ],
488 [
489 'id' => 'medium',
490 'title' => 'Medium'
491 ],
492 [
493 'id' => 'high',
494 'title' => 'High'
495 ],
496 ];
497 }
498
499 public static function dueDateConversion($due_time, $unit)
500 {
501 if($due_time <= 0) {
502 return null;
503 }
504 $currentTime = current_time('mysql');
505 $readyString = '+' . $due_time . ' ' . $unit;
506 return gmdate('Y-m-d H:i:s',strtotime($readyString,strtotime($currentTime)));
507 }
508
509 public static function searchWordPressUsers($searchQuery, $limit = 20)
510 {
511 $search = sanitize_text_field($searchQuery);
512
513 // Search by user login, email, and nicename
514 $args = array(
515 'role' => '',
516 'search' => '*' . $search . '*',
517 'number' => $limit,
518 );
519
520 // Get users by login, email, and nicename
521 $user_query = new \WP_User_Query($args);
522 $users_by_login = $user_query->get_results();
523
524 // Search by first name and last name
525 $meta_query_args = array(
526 'relation' => 'OR',
527 array(
528 'key' => 'first_name',
529 'value' => $search,
530 'compare' => 'LIKE',
531 ),
532 array(
533 'key' => 'last_name',
534 'value' => $search,
535 'compare' => 'LIKE',
536 ),
537 );
538
539 $meta_query = array(
540 'role' => '',
541 'meta_query' => $meta_query_args,
542 'number' => $limit,
543 );
544
545 // Get users by first name and last name
546 $users_by_meta = get_users($meta_query);
547
548 // Merge and remove duplicates
549 $users = array_merge($users_by_login, $users_by_meta);
550 $users = array_unique($users, SORT_REGULAR);
551
552 return $users;
553
554 }
555
556 public static function sanitizeTaskAttachment($data)
557 {
558 $fieldMaps = [
559 'title' => 'sanitize_text_field',
560 'url' => 'sanitize_url',
561 ];
562
563 return self::sanitizeData($data, $fieldMaps);
564 }
565
566 public static function sanitizeTaskRepeatData($data)
567 {
568 $fieldMaps = [
569 'create_new' => 'intval',
570 'repeat_in' => 'intval',
571 'repeat_type' => 'sanitize_text_field',
572 'repeat_when_complete' => 'intval',
573 'selected_month' => 'sanitize_text_field',
574 'board_id' => 'intval',
575 'time' => 'sanitize_text_field',
576 'time_zone' => 'sanitize_text_field',
577 'next_repeat_date' => 'sanitize_text_field',
578 'repeat_in_month_type' => 'sanitize_text_field',
579 ];
580
581 return self::sanitizeData($data, $fieldMaps);
582 }
583
584 public static function sanitizeTaskForWebHook($data)
585 {
586 $fieldMaps = [
587 'title' => 'sanitize_text_field',
588 'board_id' => 'intval',
589 'parent_id' => 'intval',
590 'crm_contact_id' => 'intval',
591 'type' => 'sanitize_text_field',
592 'stage' => 'sanitize_text_field',
593 'reminder_type' => 'sanitize_text_field',
594 'priority' => 'sanitize_text_field',
595 'lead_value' => 'doubleval',
596 'remind_at' => 'sanitize_text_field',
597 'scope' => 'sanitize_text_field',
598 'source' => 'sanitize_text_field',
599 'description' => 'wp_kses_post',
600 'due_date' => 'sanitize_text_field',
601 'start_at' => 'sanitize_text_field',
602 'log_minutes' => 'sanitize_text_field',
603 'last_completed' => 'sanitize_text_field',
604 'is_archived' => 'intval',
605 'previous_stage' => 'sanitize_text_field',
606 'new_stage' => 'sanitize_text_field',
607 'new_index' => 'intval',
608 'old_index' => 'intval',
609 'new_board_id' => 'intval',
610 'position' => 'intval'
611
612 ];
613
614 return self::sanitizeData($data, $fieldMaps);
615 }
616
617
618 public static function taskReminderTypes()
619 {
620 $allowedTypes = [
621 '30_minutes_before' => __('30 minutes before', 'fluent-boards'),
622 '1_hour_before' => __('1 hour before', 'fluent-boards'),
623 '2_hours_before' => __('2 hours before', 'fluent-boards'),
624 '1_day_before' => __('1 day before', 'fluent-boards'),
625 '2_days_before' => __('2 days before', 'fluent-boards'),
626 '1_week_before' => __('1 week before', 'fluent-boards'),
627 ];
628
629 $allowedTypes = apply_filters('fluent_boards/task_reminder_types', $allowedTypes);
630
631 return $allowedTypes;
632 }
633
634 public static function translateActivities($activities)
635 {
636 $actionTranslations = [
637 'changed' => __('changed', 'fluent-boards'),
638 'updated' => __('updated', 'fluent-boards'),
639 'added' => __('added', 'fluent-boards'),
640 'removed' => __('removed', 'fluent-boards'),
641 'created' => __('created', 'fluent-boards'),
642 'closed' => __('closed', 'fluent-boards'),
643 'reopened' => __('reopened', 'fluent-boards'),
644 'joined' => __('joined', 'fluent-boards'),
645 'left' => __('left', 'fluent-boards'),
646 'cloned' => __('cloned', 'fluent-boards'),
647 'deleted' => __('deleted', 'fluent-boards'),
648 'archived' => __('archived', 'fluent-boards'),
649 'restored' => __('restored', 'fluent-boards'),
650 'set' => __('set', 'fluent-boards'),
651 ];
652
653 $columnTranslations = [
654 'task' => __('task', 'fluent-boards'),
655 'description' => __('description', 'fluent-boards'),
656 'board' => __('board', 'fluent-boards'),
657 'assignee' => __('assignee', 'fluent-boards'),
658 'label' => __('label', 'fluent-boards'),
659 'Due Date' => __('Due Date', 'fluent-boards'),
660 'Start Date' => __('Start Date', 'fluent-boards'),
661 'priority' => __('priority', 'fluent-boards'),
662 'comment' => __('comment', 'fluent-boards'),
663 'a reply' => __('a reply', 'fluent-boards'),
664 'subtask' => __('subtask', 'fluent-boards'),
665 'subtask group' => __('subtask group', 'fluent-boards'),
666 'subtask group title' => __('subtask group title', 'fluent-boards'),
667 'stage' => __('stage', 'fluent-boards'),
668 'the associate email' => __('the associate email', 'fluent-boards'),
669 'attachment' => __('attachment', 'fluent-boards'),
670 'repeat task' => __('repeat task', 'fluent-boards'),
671 'Repeat Task' => __('Repeat Task', 'fluent-boards'),
672 ];
673
674 foreach ($activities as $activity) {
675 if (isset($actionTranslations[$activity->action])) {
676 $activity->action = $actionTranslations[$activity->action];
677 }
678 if (isset($columnTranslations[$activity->column])) {
679 $activity->column = $columnTranslations[$activity->column];
680 }
681 }
682 }
683 }
684