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 +91 -38 1.95.22.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,
@@ -181,9 +227,9 @@
181 227 }
182 228
183 229 protected function cloneTaskAttachmentsToBoard(Task $sourceTask, Task $targetTask, $targetBoardId)
184 230 {
185 - if (!class_exists('\FluentBoardsPro\App\Models\TaskAttachment')) {
231 + if (!defined('FLUENT_BOARDS_PRO_VERSION')) {
186 232 return;
187 233 }
188 234
189 235 $attachments = \FluentBoardsPro\App\Models\TaskAttachment::where('object_id', $sourceTask->id)
@@ -203,9 +249,9 @@
203 249 }
204 250
205 251 protected function getTaskAttachmentsForMove(Task $task)
206 252 {
207 - if (!class_exists('\FluentBoardsPro\App\Models\TaskAttachment')) {
253 + if (!defined('FLUENT_BOARDS_PRO_VERSION')) {
208 254 return [];
209 255 }
210 256
211 257 return \FluentBoardsPro\App\Models\TaskAttachment::where('object_id', $task->id)
@@ -269,9 +315,9 @@
269 315 return $result;
270 316 }
271 317
272 318 $sourcePath = $this->resolveLocalPath($source, $sourceBoardId);
273 - if (!$sourcePath || !file_exists($sourcePath)) {
319 + if (!$sourcePath || !is_file($sourcePath) || !is_readable($sourcePath)) {
274 320 return $result;
275 321 }
276 322
277 323 $targetDir = $this->getBoardDir($targetBoardId);
@@ -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'])) {
@@ -338,9 +418,9 @@
338 418 }
339 419
340 420 protected function refreshTaskAttachmentCount(Task $task)
341 421 {
342 - if (!class_exists('\FluentBoardsPro\App\Models\TaskAttachment')) {
422 + if (!defined('FLUENT_BOARDS_PRO_VERSION')) {
343 423 return;
344 424 }
345 425
346 426 $settings = $task->settings ?: [];
@@ -351,36 +431,9 @@
351 431 }
352 432
353 433 protected function resolveLocalPath(Attachment $attachment, $boardId)
354 434 {
355 - if (!empty($attachment->file_path) && file_exists($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 (file_exists($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 && file_exists($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 (file_exists($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 {
@@ -425,9 +478,9 @@
425 478 }
426 479
427 480 protected function shouldStoreAbsolutePath(Attachment $attachment)
428 481 {
429 - return !empty($attachment->file_path) && file_exists($attachment->file_path);
482 + return !empty($attachment->file_path) && is_file($attachment->file_path);
430 483 }
431 484
432 485 protected function getCoverImageId(Task $task)
433 486 {