PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 1.91.6
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v1.91.6
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 / CommentService.php

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

450 lines 14.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\Comment;
6 use FluentBoards\App\Models\CommentImage;
7 use FluentBoards\App\Models\Task;
8 use FluentBoards\App\Models\TaskActivity;
9 use FluentBoardsPro\App\Services\AttachmentService;
10 use FluentBoardsPro\App\Services\RemoteUrlParser;
11
12 class CommentService
13 {
14 public function getComments($id, $per_page, $filter)
15 {
16 $task = Task::findOrFail($id);
17
18 $commentsQuery = $task->comments()->whereNull('parent_id')
19 ->with(['user']);
20
21 if ($filter == 'oldest') {
22 $commentsQuery = $commentsQuery->oldest();
23 } else { // latest or newest
24 $commentsQuery = $commentsQuery->latest();
25 }
26 $comments = $commentsQuery->paginate($per_page);
27
28 foreach ($comments as $comment) {
29 $comment->replies = $this->getReplies($comment);
30 $comment->replies_count = count($comment->replies);
31 $comment->load('images');
32 }
33
34 return $comments;
35 }
36
37 public function getTotal($id)
38 {
39 $task = Task::findOrFail($id);
40 $totalComment = Comment::where('task_id', $task->id)
41 ->type('comment')
42 ->count();
43 $totalReply = Comment::where('task_id', $task->id)
44 ->type('reply')
45 ->count();
46
47 return $totalComment + $totalReply;
48 }
49
50 public function getReplies($comment)
51 {
52 $replies = Comment::where('parent_id', $comment->id)->with(['user'])->get();
53 return $replies;
54 }
55
56 public function create($commentData, $id)
57 {
58 $comment = Comment::create($commentData);
59 do_action('fluent_boards/comment_created', $comment);
60 return $comment;
61 }
62
63 private function startsWithAt($word) {
64 return mb_strpos($word, '@') === 0;
65 }
66
67 private function isValidUrl($url)
68 {
69 try {
70 if (empty($url)) {
71 return false;
72 }
73
74 $url = trim($url);
75
76 // Early return for obviously invalid formats
77 if ($url === 'http://' || $url === 'https://') {
78 return false;
79 }
80
81 // Handle www. URLs
82 if (strpos($url, 'www.') === 0) {
83 $url = 'http://' . $url;
84 }
85 // If it's not already a URL, make it one
86 elseif (!preg_match('~^(?:f|ht)tps?://~i', $url)) {
87 $url = 'https://' . $url;
88 }
89
90 $components = wp_parse_url($url);
91
92 if (empty($components) || !isset($components['host'])) {
93 return false;
94 }
95
96 // Additional validation with filter_var
97 $isValid = filter_var($url, FILTER_VALIDATE_URL) !== false;
98 return $isValid;
99
100 } catch (\Exception $e) {
101 return false;
102 }
103 }
104
105 private function extractUrls($text) {
106 try {
107 // More permissive URL pattern that handles international domains and various formats
108 $urlPattern = '%\b(?:(?:https?|ftp):\/\/|www\.)[^\s<>\[\]{}"\']+'
109 . '(?:\([^\s<>\[\]{}"\')]*\)|[^\s<>\[\]{}"\'\)])*%iu';
110
111 if (preg_match_all($urlPattern, $text, $matches)) {
112 $urls = array_filter($matches[0], function($url) {
113 $trimmed = trim($url);
114 return !empty($trimmed);
115 });
116 return array_values($urls); // Re-index array
117 }
118 return [];
119 } catch (\Exception $e) {
120 return [];
121 }
122 }
123
124 private function extractMentions($text) {
125 try {
126 // Pattern that combines zero-width delimiters with international username support
127 $pattern = '/@\x{200B}([\p{L}\p{N}_. -@]+)\x{200C}/u';
128
129 if (preg_match_all($pattern, $text, $matches)) {
130 $mentions = array_filter($matches[1], function($mention) {
131 $trimmed = trim($mention);
132 return !empty($trimmed);
133 });
134 return array_values($mentions); // Re-index array
135 }
136 return [];
137 } catch (\Exception $e) {
138 return [];
139 }
140 }
141
142 public function processMentionAndLink($commentDescription, $mentionData = [])
143 {
144 if (empty($commentDescription)) {
145 return '';
146 }
147
148 try {
149 // Ensure UTF-8 encoding with error handling
150 $commentDescription = mb_convert_encoding($commentDescription, 'UTF-8', 'auto');
151
152 // Extract all URLs from the text
153 $urls = $this->extractUrls($commentDescription);
154 $urlReplacements = [];
155
156 if (!empty($urls)) {
157 foreach ($urls as $url) {
158 if ($this->isValidUrl($url)) {
159 $urlReplacements[$url] = sprintf(
160 '<a class="fbs_link" target="_blank" rel="noopener noreferrer" href="%1$s">%1$s</a>',
161 esc_url($url)
162 );
163 }
164 }
165 }
166
167 // Process mentions
168 $mentionedUsernames = [];
169 if (!empty($mentionData) && is_array($mentionData)) {
170 foreach ($mentionData as $mentionedId) {
171 $user = get_userdata($mentionedId);
172 if ($user) {
173 $mentionedUsernames[$user->user_login] = [
174 'user_id' => $user->ID,
175 'display_name' => htmlspecialchars($user->display_name, ENT_QUOTES, 'UTF-8')
176 ];
177 }
178 }
179 }
180
181 // Extract all mentions from the text
182 $mentions = $this->extractMentions($commentDescription);
183 $mentionReplacements = [];
184
185 if (!empty($mentions)) {
186 foreach ($mentions as $mention) {
187 if (array_key_exists($mention, $mentionedUsernames)) {
188 $mentionReplacements['@' . "\u{200B}" . $mention . "\u{200C}"] = sprintf(
189 '<a class="fbs_mention" href="%smember/%d/tasks">%s</a>',
190 esc_url(fluent_boards_page_url()),
191 $mentionedUsernames[$mention]['user_id'],
192 $mentionedUsernames[$mention]['display_name']
193 );
194 }
195 }
196 }
197
198 // Apply replacements
199 $originalText = $commentDescription;
200
201 // First replace URLs (longer strings first to avoid partial replacements)
202 if (!empty($urls)) {
203 usort($urls, function($a, $b) {
204 return strlen($b) - strlen($a);
205 });
206 foreach ($urls as $url) {
207 if (isset($urlReplacements[$url])) {
208 $commentDescription = str_replace($url, $urlReplacements[$url], $commentDescription);
209 }
210 }
211 }
212
213 // Then replace mentions
214 if (!empty($mentionReplacements)) {
215 foreach ($mentionReplacements as $mention => $replacement) {
216 $commentDescription = str_replace($mention, $replacement, $commentDescription);
217 }
218 }
219
220 return $commentDescription;
221
222 } catch (\Exception $e) {
223 return $commentDescription; // Return original text if processing fails
224 }
225 }
226
227 public function checkIfCommentHaveLinks($comment)
228 {
229 if (empty($comment)) {
230 return '';
231 }
232
233 try {
234 // Ensure UTF-8 encoding
235 $comment = mb_convert_encoding($comment, 'UTF-8', 'auto');
236
237 // Extract all URLs from the text
238 $urls = $this->extractUrls($comment);
239 $hasLinks = false;
240
241 // Replace URLs with links
242 if (!empty($urls)) {
243 foreach ($urls as $url) {
244 if ($this->isValidUrl($url)) {
245 $hasLinks = true;
246 $replacement = sprintf(
247 '<a class="fbs_link" target="_blank" rel="noopener noreferrer" href="%1$s">%1$s</a>',
248 esc_url($url)
249 );
250 $comment = str_replace($url, $replacement, $comment);
251 }
252 }
253 }
254
255 return $hasLinks ? $comment : $comment;
256
257 } catch (\Exception $e) {
258 return $comment; // Return original text if processing fails
259 }
260 }
261
262 public function attachCommentImages($comment, $imageIds)
263 {
264
265 foreach ($imageIds as $imageId)
266 {
267 $attachmentObject = CommentImage::findOrFail($imageId);
268 if($attachmentObject) {
269 if ($attachmentObject->object_id == $comment->id && $attachmentObject->object_type == Constant::COMMENT_IMAGE) {
270 continue;
271 }
272 $attachmentObject->object_id = $comment->id;
273 $attachmentObject->object_type = Constant::COMMENT_IMAGE;
274 $attachmentObject->save();
275 }
276 }
277 //if(in_array("banana", $imageIds))
278 $commentImages = CommentImage::where('object_id', $comment->id)->where('object_type', Constant::COMMENT_IMAGE)->get();
279
280 foreach ($commentImages as $commentImage) {
281 if(!in_array($commentImage->id, $imageIds)) {
282 $deletedImage = clone $commentImage;
283 $commentImage->delete();
284 //do_action('fluent_boards/comment_image_deleted', $deletedImage);
285 }
286 }
287 }
288
289 public function update($commentData, $comment_id, $mentionData)
290 {
291 $comment = Comment::findOrFail($comment_id);
292
293 if ($comment->created_by != get_current_user_id()) {
294 return false;
295 }
296
297 $allMentionedIds = array_unique(array_merge($comment->settings['mentioned_id'] ?? [], is_array($mentionData) ? $mentionData : []));
298
299 if ($allMentionedIds) {
300 $processedDescription = $this->processMentionAndLink($commentData['description'], $allMentionedIds);
301 } elseif(!$allMentionedIds) {
302 $processedDescription = $this->checkIfCommentHaveLinks($commentData['description']);
303 }
304
305 $oldComment = $comment->settings['raw_description'] ?? $comment->description;
306 $comment->description = $processedDescription;
307
308 if($comment->settings != null)
309 {
310 $tempSettings = $comment->settings;
311 $tempSettings['raw_description'] = $commentData['description'];
312 $tempSettings['mentioned_id'] = $allMentionedIds;
313 $comment->settings = $tempSettings;
314 } else {
315 $comment->settings = [
316 'raw_description' => $commentData['description'],
317 'mentioned_id' => $allMentionedIds
318 ];
319 }
320 $comment->save();
321
322 if(!$comment->parent_id) {
323 do_action('fluent_boards/comment_updated', $comment, $oldComment);
324 }
325
326 return $comment;
327 }
328
329 public function delete($comment_id)
330 {
331 $comment = Comment::findOrFail($comment_id);
332
333 if ($comment->created_by != get_current_user_id()) {
334 return false;
335 }
336
337 // Delete related replies first (model event will handle their images)
338 $this->relatedReplyDelete($comment_id);
339
340 // Delete the comment (model deleting event will handle images and comments_count)
341 $comment->delete();
342
343 do_action('fluent_boards/comment_deleted', $comment);
344 }
345
346 public function relatedReplyDelete($comment_id)
347 {
348 $replies = Comment::where('parent_id', $comment_id)
349 ->type('reply')
350 ->get();
351 foreach ($replies as $reply) {
352 // Delete reply (model deleting event will handle images)
353 $reply->delete();
354 }
355 }
356
357 public function updateReply($replyData, $id)
358 {
359 $reply = Comment::findOrFail($id);
360
361 if ($reply->created_by != get_current_user_id()) {
362 return false;
363 }
364
365 $oldReply = $reply->description;
366 $reply->description = $replyData['description'];
367 $reply->save();
368 // do_action('fluent_boards/task_comment_updated', $comment->task_id, $oldComment, $comment->description);
369
370 return $reply;
371 }
372
373 public function deleteReply($id)
374 {
375 $reply = Comment::findOrFail($id);
376 // $taskId = $reply->task_id;
377
378 if ($reply->created_by != get_current_user_id()) {
379 return false;
380 }
381
382 // Delete reply (model deleting event will handle images)
383 $reply->delete();
384
385 // do_action('fluent_boards/comment_deleted', $taskId);
386 }
387
388 /**
389 * Adds a task attachment to the specified task.
390 *
391 * @param int $taskId The ID of the task to which the attachment is added.
392 * @param string $title The title of the attachment.
393 * @param string $url The URL of the attachment.
394 *
395 * @return Attachment The updated list of task attachments.
396 * @throws \Exception
397 */
398 public function createCommentImage($data, $boardId)
399 {
400 /*
401 * I will refactor this function later- within March 2024 Last Week
402 */
403 $initialDataData = [
404 'type' => 'url',
405 'url' => '',
406 'name' => '',
407 'size' => 0,
408 ];
409
410 $attachData = array_merge($initialDataData, $data);
411 $UrlMeta = [];
412 if($attachData['type'] == 'url') {
413 $UrlMeta = RemoteUrlParser::parse($attachData['url']);
414 }
415 $attachment = new CommentImage();
416 $attachment->object_id = 0;
417 $attachment->object_type = Constant::COMMENT_IMAGE;
418 $attachment->attachment_type = $attachData['type'];
419 $attachment->title = $this->setTitle($attachData['type'], $attachData['name'], $UrlMeta);
420 $attachment->file_path = $attachData['type'] != 'url' ? $attachData['file'] : null;
421 $attachment->full_url = esc_url($attachData['url']);
422 $attachment->file_size = $attachData['size'];
423 $attachment->settings = $attachData['type'] == 'url' ? [
424 'meta' => $UrlMeta
425 ] : '';
426 $attachment->driver = 'local';
427 $attachment->save();
428
429 return $attachment;
430 }
431
432 public function createPublicUrl($attachment, $boardId)
433 {
434 return add_query_arg([
435 'fbs' => 1,
436 'fbs_type' => 'public_url',
437 'fbs_bid' => $boardId,
438 'fbs_comment_image' => $attachment->file_hash
439 ], site_url('/index.php'));
440 }
441
442 private function setTitle($type, $title, $UrlMeta)
443 {
444 if($type != 'url') {
445 return sanitize_file_name($title);
446 }
447 return $title ?? $UrlMeta['title'] ?? '';
448 }
449 }
450