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 +51 -34 2.0.122.1.0 View file →
@@ -70,12 +70,22 @@
70 70
71 71 $images = CommentImage::whereIn('object_id', $commentIds)
72 72 ->where('object_type', Constant::COMMENT_IMAGE)
73 73 ->get();
74 + $commentCreators = Comment::whereIn('id', $commentIds)
75 + ->pluck('created_by', 'id')
76 + ->toArray();
74 77 $sharedFullUrls = $this->getSharedFullUrlLookup($this->combineAttachmentCollections($images));
78 + $commentService = new CommentService();
75 79
76 - CommentImage::withoutTimestamps(function () use ($images, $sourceBoardId, $targetBoardId, $sharedFullUrls) {
80 + CommentImage::withoutTimestamps(function () use ($images, $sourceBoardId, $targetBoardId, $taskId, $sharedFullUrls, $commentCreators, $commentService) {
77 81 foreach ($images as $image) {
82 + $commentService->applyCommentImageScope(
83 + $image,
84 + $targetBoardId,
85 + $taskId,
86 + $commentCreators[$image->object_id] ?? null
87 + );
78 88 $this->moveAttachmentToBoard($image, $sourceBoardId, $targetBoardId, $sharedFullUrls);
79 89 }
80 90 });
81 91 }
@@ -341,21 +351,55 @@
341 351 if (!$urlMap || empty($task->description)) {
342 352 return;
343 353 }
344 354
345 - $description = $task->description;
346 -
355 + $replacements = [];
347 356 foreach ($urlMap as $oldUrl => $newUrl) {
348 - $description = str_replace($oldUrl, $newUrl, $description);
349 - $description = str_replace(esc_url($oldUrl), esc_url($newUrl), $description);
350 - $description = str_replace(esc_attr($oldUrl), esc_attr($newUrl), $description);
357 + $identity = $this->getPublicAttachmentUrlIdentity($oldUrl);
358 + if ($identity !== null) {
359 + $replacements[$identity] = $newUrl;
360 + }
351 361 }
352 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 +
353 375 if ($description !== $task->description) {
354 376 $task->description = $description;
355 377 }
356 378 }
357 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 +
358 402 protected function updateTaskCoverFromFileResults(Task $task, array $fileResults, $targetBoardId, $sourceCoverImageId = null)
359 403 {
360 404 $settings = $task->settings;
361 405 if (empty($settings['cover']) || !is_array($settings['cover'])) {
@@ -387,36 +431,9 @@
387 431 }
388 432
389 433 protected function resolveLocalPath(Attachment $attachment, $boardId)
390 434 {
391 - if (!empty($attachment->file_path) && is_file($attachment->file_path)) {
392 - return $attachment->file_path;
393 - }
394 -
395 - if (!empty($attachment->full_url)) {
396 - $uploadDir = wp_upload_dir();
397 - $pathFromUrl = rawurldecode(str_replace($uploadDir['baseurl'], $uploadDir['basedir'], $attachment->full_url));
398 - if (is_file($pathFromUrl)) {
399 - return $pathFromUrl;
400 - }
401 -
402 - if ($boardId) {
403 - $urlPath = wp_parse_url($attachment->full_url, PHP_URL_PATH);
404 - $pathFromBoardUrl = $urlPath ? $this->getBoardDir($boardId) . DIRECTORY_SEPARATOR . basename(rawurldecode($urlPath)) : null;
405 - if ($pathFromBoardUrl && is_file($pathFromBoardUrl)) {
406 - return $pathFromBoardUrl;
407 - }
408 - }
409 - }
410 -
411 - if ($boardId && !empty($attachment->file_path)) {
412 - $pathFromBoard = $this->getBoardDir($boardId) . DIRECTORY_SEPARATOR . basename(rawurldecode($attachment->file_path));
413 - if (is_file($pathFromBoard)) {
414 - return $pathFromBoard;
415 - }
416 - }
417 -
418 - return null;
435 + return FileSystem::resolveLocalAttachmentPath($attachment->file_path, $boardId);
419 436 }
420 437
421 438 protected function getAttachmentFilename(Attachment $attachment)
422 439 {