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
← All changes | app/Services/CommentService.php +91 -35 2.0.152.1.0 View file →
@@ -1,8 +1,9 @@
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;
6 7 use FluentBoards\App\Models\CommentImage;
7 8 use FluentBoards\App\Models\Task;
8 9 use FluentBoards\App\Models\TaskActivity;
@@ -7,8 +8,9 @@
7 8 use FluentBoards\App\Models\Task;
8 9 use FluentBoards\App\Models\TaskActivity;
9 10 use FluentBoardsPro\App\Services\AttachmentService;
10 11 use FluentBoardsPro\App\Services\RemoteUrlParser;
12 +use RuntimeException;
11 13
12 14 class CommentService
13 15 {
14 16 /**
@@ -92,8 +94,40 @@
92 94 do_action('fluent_boards/comment_created', $comment);
93 95 return $comment;
94 96 }
95 97
98 + /** Allow only paragraph content and the comment editor's five inline formats. */
99 + public function sanitizeContent($content)
100 + {
101 + if (!is_string($content) || trim(wp_strip_all_tags($content)) === '') {
102 + return '';
103 + }
104 +
105 + return trim(wp_kses($content, [
106 + 'p' => [], 'br' => [], 'strong' => [], 'b' => [],
107 + 'em' => [], 'i' => [], 'del' => [], 's' => [], 'code' => [],
108 + 'a' => ['href' => true, 'title' => true],
109 + ]));
110 + }
111 +
112 + /** Resolve mentions and bare URLs in text without rewriting link attributes or code. */
113 + public function renderContent($content, $mentionData = [])
114 + {
115 + $parts = wp_html_split($this->sanitizeContent($content));
116 + $skipDepth = 0;
117 + foreach ($parts as &$part) {
118 + if (preg_match('~^</?(a|code)\b~i', $part)) {
119 + $skipDepth += strpos($part, '</') === 0 ? -1 : 1;
120 + $skipDepth = max(0, $skipDepth);
121 + } elseif ($skipDepth === 0 && $part !== '' && $part[0] !== '<') {
122 + $part = $this->processMentionAndLink($part, $mentionData);
123 + }
124 + }
125 + unset($part);
126 +
127 + return wp_kses_post(implode('', $parts));
128 + }
129 +
96 130 private function startsWithAt($word) {
97 131 return mb_strpos($word, '@') === 0;
98 132 }
99 133
@@ -173,9 +207,9 @@
173 207 }
174 208
175 209 public function processMentionAndLink($commentDescription, $mentionData = [])
176 210 {
177 - if (empty($commentDescription)) {
211 + if ($commentDescription === '' || $commentDescription === null) {
178 212 return '';
179 213 }
180 214
181 215 try {
@@ -292,47 +326,67 @@
292 326 }
293 327 }
294 328
295 329 /**
296 - * Validate every new image before attaching uploads or removing existing images.
330 + * Retain this comment's images or claim the current user's pending uploads on its board.
331 + * Validate the complete list before changing attachments or removing omitted images.
297 332 */
298 333 public function attachCommentImages($comment, $imageIds)
299 334 {
300 335 $imageIds = $this->normalizeCommentImageIds($imageIds);
336 + $commentId = absint($comment->id);
337 + $boardId = absint($comment->board_id);
338 + $taskId = absint($comment->task_id);
339 + $userId = get_current_user_id();
301 340
302 - $commentImages = CommentImage::where('object_id', $comment->id)
303 - ->where('object_type', Constant::COMMENT_IMAGE)
304 - ->get();
341 + if (!$commentId || !$boardId || !$userId) {
342 + throw new RuntimeException(__('Invalid comment image selection.', 'fluent-boards'));
343 + }
305 344
306 - $attachedImageIds = [];
307 - foreach ($commentImages as $commentImage) {
308 - if (in_array((int) $commentImage->id, $imageIds, true)) {
309 - $attachedImageIds[] = (int) $commentImage->id;
345 + App::getInstance('db')->transaction(function () use ($imageIds, $commentId, $boardId, $taskId, $userId) {
346 + // Serialize edits to this comment and prevent concurrent claims of the same upload.
347 + Comment::withoutGlobalScopes()->where('id', $commentId)->where('board_id', $boardId)->lockForUpdate()->firstOrFail();
348 + $images = CommentImage::whereIn('id', $imageIds)
349 + ->where('object_type', Constant::COMMENT_IMAGE)
350 + ->orderBy('id')
351 + ->lockForUpdate()
352 + ->get();
353 +
354 + if ($images->count() !== count($imageIds)) {
355 + throw new RuntimeException(__('Invalid comment image selection.', 'fluent-boards'));
310 356 }
311 - }
312 357
313 - $newImageIds = array_values(array_diff($imageIds, $attachedImageIds));
314 - if (!empty($newImageIds)) {
315 - $attachmentObjects = $this->assertCommentImagesAttachable(
316 - $newImageIds,
317 - $comment->board_id,
318 - $comment->task_id
319 - );
358 + foreach ($images as $image) {
359 + if ((int) $image->object_id === $commentId) {
360 + continue;
361 + }
320 362
321 - foreach ($attachmentObjects as $attachmentObject) {
322 - $attachmentObject->object_id = $comment->id;
323 - $attachmentObject->object_type = Constant::COMMENT_IMAGE;
324 - $attachmentObject->save();
363 + if ((int) $image->object_id !== 0
364 + || !$this->commentImageScopeMatches($image, $boardId, $taskId, $userId)) {
365 + throw new RuntimeException(__('Invalid comment image selection.', 'fluent-boards'));
366 + }
325 367 }
326 - }
327 368
328 - foreach ($commentImages as $commentImage) {
329 - if (!in_array((int) $commentImage->id, $imageIds, true)) {
330 - $deletedImage = clone $commentImage;
331 - $commentImage->delete();
332 - //do_action('fluent_boards/comment_image_deleted', $deletedImage);
369 + foreach ($images as $image) {
370 + if ((int) $image->object_id === 0) {
371 + $image->object_id = $commentId;
372 + if (!$image->save()) {
373 + throw new RuntimeException(__('Could not attach comment image.', 'fluent-boards'));
374 + }
375 + }
333 376 }
334 - }
377 +
378 + $removedImages = CommentImage::where('object_id', $commentId)
379 + ->where('object_type', Constant::COMMENT_IMAGE)
380 + ->whereNotIn('id', $imageIds)
381 + ->get();
382 +
383 + foreach ($removedImages as $image) {
384 + if (!$image->delete()) {
385 + throw new RuntimeException(__('Could not remove comment image.', 'fluent-boards'));
386 + }
387 + }
388 + });
335 389 }
336 390
337 391 /**
338 392 * Allow retained images on this comment and validate all newly supplied uploads.
@@ -458,13 +512,10 @@
458 512 if (array_diff($newMentionedIds, $allMentionedIds)) {
459 513 throw new \Exception(esc_html__('One or more mentioned users are not members of this board', 'fluent-boards'), 403);
460 514 }
461 515
462 - if ($allMentionedIds) {
463 - $processedDescription = $this->processMentionAndLink($commentData['description'], $allMentionedIds);
464 - } elseif(!$allMentionedIds) {
465 - $processedDescription = $this->checkIfCommentHaveLinks($commentData['description']);
466 - }
516 + $commentData['description'] = $this->sanitizeContent($commentData['description']);
517 + $processedDescription = $this->renderContent($commentData['description'], $allMentionedIds);
467 518
468 519 $oldComment = $comment->settings['raw_description'] ?? $comment->description;
469 520 $comment->description = $processedDescription;
470 521
@@ -582,9 +633,12 @@
582 633 $settings = $attachData['type'] == 'url' ? [
583 634 'meta' => $UrlMeta
584 635 ] : [];
585 636 $settings['board_id'] = absint($boardId);
586 - $attachment->settings = $settings;
637 + $attachment->settings = $settings + [
638 + Constant::ATTACHMENT_UPLOAD_BOARD_ID => absint($boardId),
639 + Constant::ATTACHMENT_UPLOAD_USER_ID => get_current_user_id(),
640 + ];
587 641 $this->applyCommentImageScope($attachment, $boardId, $taskId);
588 642 $attachment->driver = 'local';
589 643 $attachment->save();
590 644
@@ -592,12 +646,14 @@
592 646 }
593 647
594 648 public function createPublicUrl($attachment, $boardId)
595 649 {
650 + $boardId = absint($boardId);
651 +
596 652 return add_query_arg([
597 653 'fbs' => 1,
598 654 'fbs_type' => 'public_url',
599 - 'fbs_comment_image' => $attachment->file_hash
655 + 'fbs_comment_image' => $attachment->file_hash,
600 656 ], site_url('/index.php'));
601 657 }
602 658
603 659 private function setTitle($type, $title, $UrlMeta)