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
← All changes | app/Services/CommentService.php +564 -32 1.21 → 2.2.0 View file →
@@ -1,20 +1,49 @@
1 1 <?php
2 2
3 3 namespace FluentBoards\App\Services;
4 4
5 +use FluentBoards\App\App;
5 6 use FluentBoards\App\Models\Comment;
7 +use FluentBoards\App\Models\CommentImage;
6 8 use FluentBoards\App\Models\Task;
7 9 use FluentBoards\App\Models\TaskActivity;
10 +use FluentBoardsPro\App\Services\AttachmentService;
11 +use FluentBoardsPro\App\Services\RemoteUrlParser;
12 +use RuntimeException;
8 13
9 14 class CommentService
10 15 {
11 - public function getComments($id, $per_page, $filter)
16 + /**
17 + * Resolve a comment only when its task belongs to the requested board.
18 + *
19 + * @param int $commentId
20 + * @param int $boardId
21 + * @return Comment
22 + * @throws \Exception
23 + */
24 + public function findCommentOnBoard($commentId, $boardId)
12 25 {
13 - $task = Task::findOrFail($id);
26 + $comment = Comment::findOrFail($commentId);
27 + (new TaskService())->findTaskOnBoard($comment->task_id, $boardId);
14 28
29 + if ($comment->board_id && (int) $comment->board_id !== absint($boardId)) {
30 + throw new \Exception(esc_html__('Comment not found', 'fluent-boards'));
31 + }
32 +
33 + return $comment;
34 + }
35 +
36 + /**
37 + * Get paginated parent comments with users, images, and replies preloaded.
38 + */
39 + public function getComments($id, $per_page, $filter, $boardId = null)
40 + {
41 + $per_page = Sanitize::perPage($per_page);
42 + $task = $boardId ? (new TaskService())->findTaskOnBoard($id, $boardId) : Task::findOrFail($id);
43 +
15 44 $commentsQuery = $task->comments()->whereNull('parent_id')
16 - ->with(['user']);
45 + ->with(['user', 'images', 'replies.user', 'replies.images']);
17 46
18 47 if ($filter == 'oldest') {
19 48 $commentsQuery = $commentsQuery->oldest();
20 49 } else { // latest or newest
@@ -22,9 +51,8 @@
22 51 }
23 52 $comments = $commentsQuery->paginate($per_page);
24 53
25 54 foreach ($comments as $comment) {
26 - $comment->replies = $this->getReplies($comment);
27 55 $comment->replies_count = count($comment->replies);
28 56 }
29 57
30 58 return $comments;
@@ -29,11 +57,11 @@
29 57
30 58 return $comments;
31 59 }
32 60
33 - public function getTotal($id)
61 + public function getTotal($id, $boardId = null)
34 62 {
35 - $task = Task::findOrFail($id);
63 + $task = $boardId ? (new TaskService())->findTaskOnBoard($id, $boardId) : Task::findOrFail($id);
36 64 $totalComment = Comment::where('task_id', $task->id)
37 65 ->type('comment')
38 66 ->count();
39 67 $totalReply = Comment::where('task_id', $task->id)
@@ -48,55 +76,492 @@
48 76 $replies = Comment::where('parent_id', $comment->id)->with(['user'])->get();
49 77 return $replies;
50 78 }
51 79
52 - public function create($commentData, $id)
80 + public function create($commentData, $id, $boardId = null)
53 81 {
82 + if ($boardId) {
83 + (new TaskService())->findTaskOnBoard($id, $boardId);
84 +
85 + if (!empty($commentData['parent_id'])) {
86 + $parentComment = $this->findCommentOnBoard($commentData['parent_id'], $boardId);
87 +
88 + if ((int) $parentComment->task_id !== (int) $id) {
89 + throw new \Exception(esc_html__('Comment not found', 'fluent-boards'));
90 + }
91 + }
92 + }
93 +
54 94 $comment = Comment::create($commentData);
55 - do_action('fluent_boards/task_comment_added', $comment);
95 + do_action('fluent_boards/comment_created', $comment);
56 96 return $comment;
57 97 }
58 98
59 - public function update($commentData, $comment_id)
99 + /** Preserve comment formatting while stripping executable HTML and unsafe attributes. */
100 + public function sanitizeContent($content)
60 101 {
61 - $comment = Comment::findOrFail($comment_id);
102 + if (!is_string($content) || trim(wp_strip_all_tags($content)) === '') {
103 + return '';
104 + }
62 105
63 - if ($comment->created_by != get_current_user_id()) {
106 + return trim(wp_kses($content, [
107 + 'p' => [], 'br' => [], 'strong' => [], 'b' => [],
108 + 'em' => [], 'i' => [], 'del' => [], 's' => [], 'code' => [],
109 + 'a' => ['href' => true, 'title' => true],
110 + 'ul' => [], 'ol' => ['start' => true], 'li' => [],
111 + 'h1' => [], 'h2' => [], 'h3' => [], 'h4' => [], 'h5' => [], 'h6' => [],
112 + 'blockquote' => [], 'pre' => ['data-language' => true],
113 + ]));
114 + }
115 +
116 + /** Resolve mentions and bare URLs in text without rewriting link attributes or code. */
117 + public function renderContent($content, $mentionData = [])
118 + {
119 + $parts = wp_html_split($this->sanitizeContent($content));
120 + $skipDepth = 0;
121 + foreach ($parts as &$part) {
122 + if (preg_match('~^</?(a|code|pre)\b~i', $part)) {
123 + $skipDepth += strpos($part, '</') === 0 ? -1 : 1;
124 + $skipDepth = max(0, $skipDepth);
125 + } elseif ($skipDepth === 0 && $part !== '' && $part[0] !== '<') {
126 + $part = $this->processMentionAndLink($part, $mentionData);
127 + }
128 + }
129 + unset($part);
130 +
131 + return wp_kses_post(implode('', $parts));
132 + }
133 +
134 + private function startsWithAt($word) {
135 + return mb_strpos($word, '@') === 0;
136 + }
137 +
138 + private function isValidUrl($url)
139 + {
140 + try {
141 + if (empty($url)) {
142 + return false;
143 + }
144 +
145 + $url = trim($url);
146 +
147 + // Early return for obviously invalid formats
148 + if ($url === 'http://' || $url === 'https://') {
149 + return false;
150 + }
151 +
152 + // Handle www. URLs
153 + if (strpos($url, 'www.') === 0) {
154 + $url = 'http://' . $url;
155 + }
156 + // If it's not already a URL, make it one
157 + elseif (!preg_match('~^(?:f|ht)tps?://~i', $url)) {
158 + $url = 'https://' . $url;
159 + }
160 +
161 + $components = wp_parse_url($url);
162 +
163 + if (empty($components) || !isset($components['host'])) {
164 + return false;
165 + }
166 +
167 + // Additional validation with filter_var
168 + $isValid = filter_var($url, FILTER_VALIDATE_URL) !== false;
169 + return $isValid;
170 +
171 + } catch (\Exception $e) {
64 172 return false;
65 173 }
174 + }
66 175
67 - $oldComment = $comment->description;
68 - $comment->description = $commentData['description'];
69 - $comment->save();
70 - do_action('fluent_boards/task_comment_updated', $comment->task_id, $oldComment, $comment->description);
176 + private function extractUrls($text) {
177 + try {
178 + // More permissive URL pattern that handles international domains and various formats
179 + $urlPattern = '%\b(?:(?:https?|ftp):\/\/|www\.)[^\s<>\[\]{}"\']+'
180 + . '(?:\([^\s<>\[\]{}"\')]*\)|[^\s<>\[\]{}"\'\)])*%iu';
181 +
182 + if (preg_match_all($urlPattern, $text, $matches)) {
183 + $urls = array_filter($matches[0], function($url) {
184 + $trimmed = trim($url);
185 + return !empty($trimmed);
186 + });
187 + return array_values($urls); // Re-index array
188 + }
189 + return [];
190 + } catch (\Exception $e) {
191 + return [];
192 + }
193 + }
71 194
72 - return $comment;
195 + private function extractMentions($text) {
196 + try {
197 + // Pattern that combines zero-width delimiters with international username support
198 + $pattern = '/@\x{200B}([\p{L}\p{N}_. -@]+)\x{200C}/u';
199 +
200 + if (preg_match_all($pattern, $text, $matches)) {
201 + $mentions = array_filter($matches[1], function($mention) {
202 + $trimmed = trim($mention);
203 + return !empty($trimmed);
204 + });
205 + return array_values($mentions); // Re-index array
206 + }
207 + return [];
208 + } catch (\Exception $e) {
209 + return [];
210 + }
73 211 }
74 212
75 - public function delete($comment_id)
213 + public function processMentionAndLink($commentDescription, $mentionData = [])
76 214 {
77 - $comment = Comment::findOrFail($comment_id);
78 - $taskId = $comment->task_id;
215 + if ($commentDescription === '' || $commentDescription === null) {
216 + return '';
217 + }
79 218
219 + try {
220 + // Ensure UTF-8 encoding with error handling
221 + $commentDescription = mb_convert_encoding($commentDescription, 'UTF-8', 'auto');
222 +
223 + // Extract all URLs from the text
224 + $urls = $this->extractUrls($commentDescription);
225 + $urlReplacements = [];
226 +
227 + if (!empty($urls)) {
228 + foreach ($urls as $url) {
229 + if ($this->isValidUrl($url)) {
230 + $urlReplacements[$url] = sprintf(
231 + '<a class="fbs_link" target="_blank" rel="noopener noreferrer" href="%1$s">%1$s</a>',
232 + esc_url($url)
233 + );
234 + }
235 + }
236 + }
237 +
238 + // Process mentions
239 + $mentionedUsernames = [];
240 + if (!empty($mentionData) && is_array($mentionData)) {
241 + foreach ($mentionData as $mentionedId) {
242 + $user = get_userdata($mentionedId);
243 + if ($user) {
244 + $mentionedUsernames[$user->user_login] = [
245 + 'user_id' => $user->ID,
246 + 'display_name' => htmlspecialchars($user->display_name, ENT_QUOTES, 'UTF-8')
247 + ];
248 + }
249 + }
250 + }
251 +
252 + // Extract all mentions from the text
253 + $mentions = $this->extractMentions($commentDescription);
254 + $mentionReplacements = [];
255 +
256 + if (!empty($mentions)) {
257 + foreach ($mentions as $mention) {
258 + if (array_key_exists($mention, $mentionedUsernames)) {
259 + $mentionReplacements['@' . "\u{200B}" . $mention . "\u{200C}"] = sprintf(
260 + '<a class="fbs_mention" href="%smember/%d/tasks">%s</a>',
261 + esc_url(fluent_boards_page_url()),
262 + $mentionedUsernames[$mention]['user_id'],
263 + $mentionedUsernames[$mention]['display_name']
264 + );
265 + }
266 + }
267 + }
268 +
269 + // Apply replacements
270 + $originalText = $commentDescription;
271 +
272 + // First replace URLs (longer strings first to avoid partial replacements)
273 + if (!empty($urls)) {
274 + usort($urls, function($a, $b) {
275 + return strlen($b) - strlen($a);
276 + });
277 + foreach ($urls as $url) {
278 + if (isset($urlReplacements[$url])) {
279 + $commentDescription = str_replace($url, $urlReplacements[$url], $commentDescription);
280 + }
281 + }
282 + }
283 +
284 + // Then replace mentions
285 + if (!empty($mentionReplacements)) {
286 + foreach ($mentionReplacements as $mention => $replacement) {
287 + $commentDescription = str_replace($mention, $replacement, $commentDescription);
288 + }
289 + }
290 +
291 + return $commentDescription;
292 +
293 + } catch (\Exception $e) {
294 + return $commentDescription; // Return original text if processing fails
295 + }
296 + }
297 +
298 + public function checkIfCommentHaveLinks($comment)
299 + {
300 + if (empty($comment)) {
301 + return '';
302 + }
303 +
304 + try {
305 + // Ensure UTF-8 encoding
306 + $comment = mb_convert_encoding($comment, 'UTF-8', 'auto');
307 +
308 + // Extract all URLs from the text
309 + $urls = $this->extractUrls($comment);
310 + $hasLinks = false;
311 +
312 + // Replace URLs with links
313 + if (!empty($urls)) {
314 + foreach ($urls as $url) {
315 + if ($this->isValidUrl($url)) {
316 + $hasLinks = true;
317 + $replacement = sprintf(
318 + '<a class="fbs_link" target="_blank" rel="noopener noreferrer" href="%1$s">%1$s</a>',
319 + esc_url($url)
320 + );
321 + $comment = str_replace($url, $replacement, $comment);
322 + }
323 + }
324 + }
325 +
326 + return $hasLinks ? $comment : $comment;
327 +
328 + } catch (\Exception $e) {
329 + return $comment; // Return original text if processing fails
330 + }
331 + }
332 +
333 + /**
334 + * Retain this comment's images or claim the current user's pending uploads on its board.
335 + * Validate the complete list before changing attachments or removing omitted images.
336 + */
337 + public function attachCommentImages($comment, $imageIds)
338 + {
339 + $imageIds = $this->normalizeCommentImageIds($imageIds);
340 + $commentId = absint($comment->id);
341 + $boardId = absint($comment->board_id);
342 + $taskId = absint($comment->task_id);
343 + $userId = get_current_user_id();
344 +
345 + if (!$commentId || !$boardId || !$userId) {
346 + throw new RuntimeException(__('Invalid comment image selection.', 'fluent-boards'));
347 + }
348 +
349 + App::getInstance('db')->transaction(function () use ($imageIds, $commentId, $boardId, $taskId, $userId) {
350 + // Serialize edits to this comment and prevent concurrent claims of the same upload.
351 + Comment::withoutGlobalScopes()->where('id', $commentId)->where('board_id', $boardId)->lockForUpdate()->firstOrFail();
352 + $images = CommentImage::whereIn('id', $imageIds)
353 + ->where('object_type', Constant::COMMENT_IMAGE)
354 + ->orderBy('id')
355 + ->lockForUpdate()
356 + ->get();
357 +
358 + if ($images->count() !== count($imageIds)) {
359 + throw new RuntimeException(__('Invalid comment image selection.', 'fluent-boards'));
360 + }
361 +
362 + foreach ($images as $image) {
363 + if ((int) $image->object_id === $commentId) {
364 + continue;
365 + }
366 +
367 + if ((int) $image->object_id !== 0
368 + || !$this->commentImageScopeMatches($image, $boardId, $taskId, $userId)) {
369 + throw new RuntimeException(__('Invalid comment image selection.', 'fluent-boards'));
370 + }
371 + }
372 +
373 + foreach ($images as $image) {
374 + if ((int) $image->object_id === 0) {
375 + $image->object_id = $commentId;
376 + if (!$image->save()) {
377 + throw new RuntimeException(__('Could not attach comment image.', 'fluent-boards'));
378 + }
379 + }
380 + }
381 +
382 + $removedImages = CommentImage::where('object_id', $commentId)
383 + ->where('object_type', Constant::COMMENT_IMAGE)
384 + ->whereNotIn('id', $imageIds)
385 + ->get();
386 +
387 + foreach ($removedImages as $image) {
388 + if (!$image->delete()) {
389 + throw new RuntimeException(__('Could not remove comment image.', 'fluent-boards'));
390 + }
391 + }
392 + });
393 + }
394 +
395 + /**
396 + * Allow retained images on this comment and validate all newly supplied uploads.
397 + */
398 + public function assertCommentImagesAttachableForComment($comment, $imageIds)
399 + {
400 + $imageIds = $this->normalizeCommentImageIds($imageIds);
401 +
402 + if (empty($imageIds)) {
403 + return [];
404 + }
405 +
406 + $commentImages = CommentImage::where('object_id', $comment->id)
407 + ->where('object_type', Constant::COMMENT_IMAGE)
408 + ->get();
409 +
410 + foreach ($commentImages as $commentImage) {
411 + $key = array_search((int) $commentImage->id, $imageIds, true);
412 + if ($key !== false) {
413 + unset($imageIds[$key]);
414 + }
415 + }
416 +
417 + return $this->assertCommentImagesAttachable(
418 + array_values($imageIds),
419 + $comment->board_id,
420 + $comment->task_id
421 + );
422 + }
423 +
424 + /**
425 + * Reject the entire image list unless every upload is unbound and owned by this actor, board, and task.
426 + */
427 + public function assertCommentImagesAttachable($imageIds, $boardId, $taskId)
428 + {
429 + $imageIds = $this->normalizeCommentImageIds($imageIds);
430 +
431 + if (empty($imageIds)) {
432 + return [];
433 + }
434 +
435 + $currentUserId = get_current_user_id();
436 + if (!$currentUserId) {
437 + throw new \Exception(esc_html__('Invalid comment image attachment', 'fluent-boards'));
438 + }
439 +
440 + $attachments = CommentImage::whereIn('id', $imageIds)
441 + ->where('object_id', 0)
442 + ->where('object_type', Constant::COMMENT_IMAGE)
443 + ->get();
444 +
445 + if (count($attachments) !== count($imageIds)) {
446 + throw new \Exception(esc_html__('Invalid comment image attachment', 'fluent-boards'));
447 + }
448 +
449 + foreach ($attachments as $attachment) {
450 + if (!$this->commentImageScopeMatches($attachment, $boardId, $taskId, $currentUserId)) {
451 + throw new \Exception(esc_html__('Invalid comment image attachment', 'fluent-boards'));
452 + }
453 + }
454 +
455 + return $attachments;
456 + }
457 +
458 + /**
459 + * Fail closed for legacy uploads without recorded board, task, and uploader ownership.
460 + */
461 + private function commentImageScopeMatches($attachment, $boardId, $taskId, $userId)
462 + {
463 + $settings = is_array($attachment->settings) ? $attachment->settings : [];
464 + $scope = isset($settings['comment_image_scope']) && is_array($settings['comment_image_scope'])
465 + ? $settings['comment_image_scope']
466 + : [];
467 +
468 + return intval($scope['board_id'] ?? 0) === intval($boardId)
469 + && intval($scope['task_id'] ?? 0) === intval($taskId)
470 + && intval($scope['created_by'] ?? 0) === intval($userId);
471 + }
472 +
473 + /**
474 + * Record trusted upload ownership in attachment metadata before saving.
475 + */
476 + public function applyCommentImageScope($attachment, $boardId, $taskId, $createdBy = null)
477 + {
478 + $settings = is_array($attachment->settings) ? $attachment->settings : [];
479 + $settings['comment_image_scope'] = [
480 + 'board_id' => intval($boardId),
481 + 'task_id' => intval($taskId),
482 + 'created_by' => intval($createdBy === null ? get_current_user_id() : $createdBy),
483 + ];
484 + $attachment->settings = $settings;
485 +
486 + return $attachment;
487 + }
488 +
489 + /**
490 + * Normalize submitted image IDs and remove duplicates before validating the full list.
491 + */
492 + private function normalizeCommentImageIds($imageIds)
493 + {
494 + if (!is_array($imageIds)) {
495 + return [];
496 + }
497 +
498 + return array_values(array_filter(array_unique(array_map('intval', $imageIds))));
499 + }
500 +
501 + public function update($commentData, $comment_id, $mentionData, $boardId = null)
502 + {
503 + $comment = $boardId ? $this->findCommentOnBoard($comment_id, $boardId) : Comment::findOrFail($comment_id);
504 +
80 505 if ($comment->created_by != get_current_user_id()) {
81 506 return false;
82 507 }
83 508
84 - $commentDescription = strip_tags($comment->description);
509 + $effectiveBoardId = absint($comment->board_id ?: $boardId);
510 + $notificationService = new NotificationService();
511 + $existingMentionedIds = array_values(array_unique(array_filter(array_map('absint', (array) ($comment->settings['mentioned_id'] ?? [])))));
512 + $newMentionedIds = array_values(array_unique(array_filter(array_map('absint', (array) $mentionData))));
513 + $requestedMentionedIds = array_values(array_unique(array_merge($existingMentionedIds, $newMentionedIds)));
514 + $allMentionedIds = $notificationService->resolveBoardMentionUserIds($effectiveBoardId, $requestedMentionedIds);
85 515
86 - $deleted = $comment->delete();
516 + if (array_diff($newMentionedIds, $allMentionedIds)) {
517 + throw new \Exception(esc_html__('One or more mentioned users are not members of this board', 'fluent-boards'), 403);
518 + }
87 519
88 - if ($deleted) {
89 - $this->relatedReplyDelete($comment_id);
520 + $commentData['description'] = $this->sanitizeContent($commentData['description']);
521 + $processedDescription = $this->renderContent($commentData['description'], $allMentionedIds);
90 522
91 - $task = Task::findOrFail($taskId);
92 - $task->comments_count = $task->comments_count - 1;
93 - $task->save();
523 + $oldComment = $comment->settings['raw_description'] ?? $comment->description;
524 + $comment->description = $processedDescription;
525 +
526 + if($comment->settings != null)
527 + {
528 + $tempSettings = $comment->settings;
529 + $tempSettings['raw_description'] = $commentData['description'];
530 + $tempSettings['mentioned_id'] = $allMentionedIds;
531 + $comment->settings = $tempSettings;
532 + } else {
533 + $comment->settings = [
534 + 'raw_description' => $commentData['description'],
535 + 'mentioned_id' => $allMentionedIds
536 + ];
94 537 }
538 + $comment->save();
95 539
96 - do_action('fluent_boards/task_comment_deleted', $taskId, $commentDescription);
540 + if(!$comment->parent_id) {
541 + do_action('fluent_boards/comment_updated', $comment, $oldComment);
542 + }
543 +
544 + return $comment;
97 545 }
98 546
547 + public function delete($comment_id, $boardId = null)
548 + {
549 + $comment = $boardId ? $this->findCommentOnBoard($comment_id, $boardId) : Comment::findOrFail($comment_id);
550 +
551 + if ($comment->created_by != get_current_user_id()) {
552 + return false;
553 + }
554 +
555 + // Delete related replies first (model event will handle their images)
556 + $this->relatedReplyDelete($comment_id);
557 +
558 + // Delete the comment (model deleting event will handle images and comments_count)
559 + $comment->delete();
560 +
561 + do_action('fluent_boards/comment_deleted', $comment);
562 + }
563 +
99 564 public function relatedReplyDelete($comment_id)
100 565 {
101 566 $replies = Comment::where('parent_id', $comment_id)
102 567 ->type('reply')
@@ -101,15 +566,16 @@
101 566 $replies = Comment::where('parent_id', $comment_id)
102 567 ->type('reply')
103 568 ->get();
104 569 foreach ($replies as $reply) {
570 + // Delete reply (model deleting event will handle images)
105 571 $reply->delete();
106 572 }
107 573 }
108 574
109 - public function updateReply($replyData, $id)
575 + public function updateReply($replyData, $id, $boardId = null)
110 576 {
111 - $reply = Comment::findOrFail($id);
577 + $reply = $boardId ? $this->findCommentOnBoard($id, $boardId) : Comment::findOrFail($id);
112 578
113 579 if ($reply->created_by != get_current_user_id()) {
114 580 return false;
115 581 }
@@ -121,11 +587,11 @@
121 587
122 588 return $reply;
123 589 }
124 590
125 - public function deleteReply($id)
591 + public function deleteReply($id, $boardId = null)
126 592 {
127 - $reply = Comment::findOrFail($id);
593 + $reply = $boardId ? $this->findCommentOnBoard($id, $boardId) : Comment::findOrFail($id);
128 594 // $taskId = $reply->task_id;
129 595
130 596 if ($reply->created_by != get_current_user_id()) {
131 597 return false;
@@ -130,9 +596,75 @@
130 596 if ($reply->created_by != get_current_user_id()) {
131 597 return false;
132 598 }
133 599
600 + // Delete reply (model deleting event will handle images)
134 601 $reply->delete();
135 602
136 -// do_action('fluent_boards/task_comment_deleted', $taskId);
603 +// do_action('fluent_boards/comment_deleted', $taskId);
604 + }
605 +
606 + /**
607 + * Persist an unbound comment upload with trusted board, task, and uploader metadata.
608 + * Legacy uploads without this scope cannot be newly attached to a comment.
609 + *
610 + * @return CommentImage
611 + */
612 + public function createCommentImage($data, $boardId, $taskId = null)
613 + {
614 + /*
615 + * I will refactor this function later- within March 2024 Last Week
616 + */
617 + $initialDataData = [
618 + 'type' => 'url',
619 + 'url' => '',
620 + 'name' => '',
621 + 'size' => 0,
622 + ];
623 +
624 + $attachData = array_merge($initialDataData, $data);
625 + $UrlMeta = [];
626 + if($attachData['type'] == 'url') {
627 + $UrlMeta = RemoteUrlParser::parse($attachData['url']);
628 + }
629 + $attachment = new CommentImage();
630 + $attachment->object_id = 0;
631 + $attachment->object_type = Constant::COMMENT_IMAGE;
632 + $attachment->attachment_type = $attachData['type'];
633 + $attachment->title = $this->setTitle($attachData['type'], $attachData['name'], $UrlMeta);
634 + $attachment->file_path = $attachData['type'] != 'url' ? $attachData['file'] : null;
635 + $attachment->full_url = esc_url($attachData['url']);
636 + $attachment->file_size = $attachData['size'];
637 + $settings = $attachData['type'] == 'url' ? [
638 + 'meta' => $UrlMeta
639 + ] : [];
640 + $settings['board_id'] = absint($boardId);
641 + $attachment->settings = $settings + [
642 + Constant::ATTACHMENT_UPLOAD_BOARD_ID => absint($boardId),
643 + Constant::ATTACHMENT_UPLOAD_USER_ID => get_current_user_id(),
644 + ];
645 + $this->applyCommentImageScope($attachment, $boardId, $taskId);
646 + $attachment->driver = 'local';
647 + $attachment->save();
648 +
649 + return $attachment;
650 + }
651 +
652 + public function createPublicUrl($attachment, $boardId)
653 + {
654 + $boardId = absint($boardId);
655 +
656 + return add_query_arg([
657 + 'fbs' => 1,
658 + 'fbs_type' => 'public_url',
659 + 'fbs_comment_image' => $attachment->file_hash,
660 + ], site_url('/index.php'));
661 + }
662 +
663 + private function setTitle($type, $title, $UrlMeta)
664 + {
665 + if($type != 'url') {
666 + return sanitize_file_name($title);
667 + }
668 + return $title ?? $UrlMeta['title'] ?? '';
137 669 }
138 670 }