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/AttachmentFileService.php +86 -33 2.0.102.1.0 View file →
@@ -2,8 +2,10 @@
2 2
3 3 namespace FluentBoards\App\Services;
4 4
5 5 use FluentBoards\App\Models\Attachment;
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\TaskImage;
8 10 use FluentBoards\App\Services\Libs\FileSystem;
9 11
@@ -43,8 +45,52 @@
43 45
44 46 return $task;
45 47 }
46 48
49 + /**
50 + * Move every comment and reply image for a task to another board.
51 + */
52 + public function moveCommentImagesToBoard($taskId, $sourceBoardId, $targetBoardId)
53 + {
54 + $taskId = absint($taskId);
55 + $sourceBoardId = absint($sourceBoardId);
56 + $targetBoardId = absint($targetBoardId);
57 +
58 + if (!$taskId || !$sourceBoardId || !$targetBoardId || $sourceBoardId === $targetBoardId) {
59 + return;
60 + }
61 +
62 + $commentIds = Comment::where('task_id', $taskId)
63 + ->pluck('id')
64 + ->toArray();
65 + $commentIds = array_filter(array_map('intval', $commentIds));
66 +
67 + if (!$commentIds) {
68 + return;
69 + }
70 +
71 + $images = CommentImage::whereIn('object_id', $commentIds)
72 + ->where('object_type', Constant::COMMENT_IMAGE)
73 + ->get();
74 + $commentCreators = Comment::whereIn('id', $commentIds)
75 + ->pluck('created_by', 'id')
76 + ->toArray();
77 + $sharedFullUrls = $this->getSharedFullUrlLookup($this->combineAttachmentCollections($images));
78 + $commentService = new CommentService();
79 +
80 + CommentImage::withoutTimestamps(function () use ($images, $sourceBoardId, $targetBoardId, $taskId, $sharedFullUrls, $commentCreators, $commentService) {
81 + foreach ($images as $image) {
82 + $commentService->applyCommentImageScope(
83 + $image,
84 + $targetBoardId,
85 + $taskId,
86 + $commentCreators[$image->object_id] ?? null
87 + );
88 + $this->moveAttachmentToBoard($image, $sourceBoardId, $targetBoardId, $sharedFullUrls);
89 + }
90 + });
91 + }
92 +
47 93 public function cloneTaskFilesToBoard(Task $sourceTask, Task $targetTask, $targetBoardId, array $options = [])
48 94 {
49 95 $options = array_merge([
50 96 'description_images' => true,
@@ -305,21 +351,55 @@
305 351 if (!$urlMap || empty($task->description)) {
306 352 return;
307 353 }
308 354
309 - $description = $task->description;
310 -
355 + $replacements = [];
311 356 foreach ($urlMap as $oldUrl => $newUrl) {
312 - $description = str_replace($oldUrl, $newUrl, $description);
313 - $description = str_replace(esc_url($oldUrl), esc_url($newUrl), $description);
314 - $description = str_replace(esc_attr($oldUrl), esc_attr($newUrl), $description);
357 + $identity = $this->getPublicAttachmentUrlIdentity($oldUrl);
358 + if ($identity !== null) {
359 + $replacements[$identity] = $newUrl;
360 + }
315 361 }
316 362
363 + // Keep HTML and Markdown delimiters outside the URL being replaced.
364 + $description = preg_replace_callback('~https?://[^\s<>"\')\]]+~i', function ($matches) use ($replacements) {
365 + $url = html_entity_decode($matches[0], ENT_QUOTES | ENT_HTML5, 'UTF-8');
366 + $identity = $this->getPublicAttachmentUrlIdentity($url);
367 + if ($identity === null || !isset($replacements[$identity])) {
368 + return $matches[0];
369 + }
370 +
371 + $newUrl = $replacements[$identity];
372 + return $url === $matches[0] ? $newUrl : esc_attr($newUrl);
373 + }, $task->description);
374 +
317 375 if ($description !== $task->description) {
318 376 $task->description = $description;
319 377 }
320 378 }
321 379
380 + /**
381 + * Match stored public image URLs by endpoint and query, excluding renewable credentials.
382 + */
383 + protected function getPublicAttachmentUrlIdentity($url)
384 + {
385 + $parts = wp_parse_url($url);
386 + if (!$parts || empty($parts['query'])) {
387 + return null;
388 + }
389 +
390 + parse_str($parts['query'], $query);
391 + if (($query['fbs_type'] ?? null) !== 'public_url' || empty($query['fbs_comment_image'])) {
392 + return null;
393 + }
394 +
395 + unset($query[Constant::ATTACHMENT_LEGACY_SIGNATURE_QUERY_KEY], $query[Constant::ATTACHMENT_LEGACY_EXPIRES_QUERY_KEY]);
396 + ksort($query);
397 + $parts['query'] = $query;
398 +
399 + return serialize($parts);
400 + }
401 +
322 402 protected function updateTaskCoverFromFileResults(Task $task, array $fileResults, $targetBoardId, $sourceCoverImageId = null)
323 403 {
324 404 $settings = $task->settings;
325 405 if (empty($settings['cover']) || !is_array($settings['cover'])) {
@@ -351,36 +431,9 @@
351 431 }
352 432
353 433 protected function resolveLocalPath(Attachment $attachment, $boardId)
354 434 {
355 - if (!empty($attachment->file_path) && is_file($attachment->file_path)) {
356 - return $attachment->file_path;
357 - }
358 -
359 - if (!empty($attachment->full_url)) {
360 - $uploadDir = wp_upload_dir();
361 - $pathFromUrl = rawurldecode(str_replace($uploadDir['baseurl'], $uploadDir['basedir'], $attachment->full_url));
362 - if (is_file($pathFromUrl)) {
363 - return $pathFromUrl;
364 - }
365 -
366 - if ($boardId) {
367 - $urlPath = wp_parse_url($attachment->full_url, PHP_URL_PATH);
368 - $pathFromBoardUrl = $urlPath ? $this->getBoardDir($boardId) . DIRECTORY_SEPARATOR . basename(rawurldecode($urlPath)) : null;
369 - if ($pathFromBoardUrl && is_file($pathFromBoardUrl)) {
370 - return $pathFromBoardUrl;
371 - }
372 - }
373 - }
374 -
375 - if ($boardId && !empty($attachment->file_path)) {
376 - $pathFromBoard = $this->getBoardDir($boardId) . DIRECTORY_SEPARATOR . basename(rawurldecode($attachment->file_path));
377 - if (is_file($pathFromBoard)) {
378 - return $pathFromBoard;
379 - }
380 - }
381 -
382 - return null;
435 + return FileSystem::resolveLocalAttachmentPath($attachment->file_path, $boardId);
383 436 }
384 437
385 438 protected function getAttachmentFilename(Attachment $attachment)
386 439 {