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/AttachmentFileService.php +59 -34 2.0.12 → 2.2.0 View file →
@@ -7,8 +7,9 @@
7 7 use FluentBoards\App\Models\CommentImage;
8 8 use FluentBoards\App\Models\Task;
9 9 use FluentBoards\App\Models\TaskImage;
10 10 use FluentBoards\App\Services\Libs\FileSystem;
11 +use Exception;
11 12
12 13 class AttachmentFileService
13 14 {
14 15 protected $createdFiles = [];
@@ -70,12 +71,22 @@
70 71
71 72 $images = CommentImage::whereIn('object_id', $commentIds)
72 73 ->where('object_type', Constant::COMMENT_IMAGE)
73 74 ->get();
75 + $commentCreators = Comment::whereIn('id', $commentIds)
76 + ->pluck('created_by', 'id')
77 + ->toArray();
74 78 $sharedFullUrls = $this->getSharedFullUrlLookup($this->combineAttachmentCollections($images));
79 + $commentService = new CommentService();
75 80
76 - CommentImage::withoutTimestamps(function () use ($images, $sourceBoardId, $targetBoardId, $sharedFullUrls) {
81 + CommentImage::withoutTimestamps(function () use ($images, $sourceBoardId, $targetBoardId, $taskId, $sharedFullUrls, $commentCreators, $commentService) {
77 82 foreach ($images as $image) {
83 + $commentService->applyCommentImageScope(
84 + $image,
85 + $targetBoardId,
86 + $taskId,
87 + $commentCreators[$image->object_id] ?? null
88 + );
78 89 $this->moveAttachmentToBoard($image, $sourceBoardId, $targetBoardId, $sharedFullUrls);
79 90 }
80 91 });
81 92 }
@@ -288,8 +299,11 @@
288 299
289 300 return array_fill_keys($sharedUrls, true);
290 301 }
291 302
303 + /**
304 + * Copy a local file into a protected board directory and track it for rollback.
305 + */
292 306 protected function copyAttachmentFileData(Attachment $source, Attachment $target, $sourceBoardId, $targetBoardId)
293 307 {
294 308 $result = [
295 309 'old_path' => null,
@@ -309,8 +323,12 @@
309 323 if (!$sourcePath || !is_file($sourcePath) || !is_readable($sourcePath)) {
310 324 return $result;
311 325 }
312 326
327 + if (!FileSystem::installUploadProtectionRules()) {
328 + throw new Exception(__('Could not prepare upload-directory protection. Please ask your hosting provider to block direct file access and check directory permissions.', 'fluent-boards'));
329 + }
330 +
313 331 $targetDir = $this->getBoardDir($targetBoardId);
314 332 if (!is_dir($targetDir)) {
315 333 wp_mkdir_p($targetDir);
316 334 }
@@ -341,21 +359,55 @@
341 359 if (!$urlMap || empty($task->description)) {
342 360 return;
343 361 }
344 362
345 - $description = $task->description;
346 -
363 + $replacements = [];
347 364 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);
365 + $identity = $this->getPublicAttachmentUrlIdentity($oldUrl);
366 + if ($identity !== null) {
367 + $replacements[$identity] = $newUrl;
368 + }
351 369 }
352 370
371 + // Keep HTML and Markdown delimiters outside the URL being replaced.
372 + $description = preg_replace_callback('~https?://[^\s<>"\')\]]+~i', function ($matches) use ($replacements) {
373 + $url = html_entity_decode($matches[0], ENT_QUOTES | ENT_HTML5, 'UTF-8');
374 + $identity = $this->getPublicAttachmentUrlIdentity($url);
375 + if ($identity === null || !isset($replacements[$identity])) {
376 + return $matches[0];
377 + }
378 +
379 + $newUrl = $replacements[$identity];
380 + return $url === $matches[0] ? $newUrl : esc_attr($newUrl);
381 + }, $task->description);
382 +
353 383 if ($description !== $task->description) {
354 384 $task->description = $description;
355 385 }
356 386 }
357 387
388 + /**
389 + * Match stored public image URLs by endpoint and query, excluding renewable credentials.
390 + */
391 + protected function getPublicAttachmentUrlIdentity($url)
392 + {
393 + $parts = wp_parse_url($url);
394 + if (!$parts || empty($parts['query'])) {
395 + return null;
396 + }
397 +
398 + parse_str($parts['query'], $query);
399 + if (($query['fbs_type'] ?? null) !== 'public_url' || empty($query['fbs_comment_image'])) {
400 + return null;
401 + }
402 +
403 + unset($query[Constant::ATTACHMENT_LEGACY_SIGNATURE_QUERY_KEY], $query[Constant::ATTACHMENT_LEGACY_EXPIRES_QUERY_KEY]);
404 + ksort($query);
405 + $parts['query'] = $query;
406 +
407 + return serialize($parts);
408 + }
409 +
358 410 protected function updateTaskCoverFromFileResults(Task $task, array $fileResults, $targetBoardId, $sourceCoverImageId = null)
359 411 {
360 412 $settings = $task->settings;
361 413 if (empty($settings['cover']) || !is_array($settings['cover'])) {
@@ -387,36 +439,9 @@
387 439 }
388 440
389 441 protected function resolveLocalPath(Attachment $attachment, $boardId)
390 442 {
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;
443 + return FileSystem::resolveLocalAttachmentPath($attachment->file_path, $boardId);
419 444 }
420 445
421 446 protected function getAttachmentFilename(Attachment $attachment)
422 447 {