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 +47 -5 2.0.15 → 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 = [];
@@ -298,8 +299,11 @@
298 299
299 300 return array_fill_keys($sharedUrls, true);
300 301 }
301 302
303 + /**
304 + * Copy a local file into a protected board directory and track it for rollback.
305 + */
302 306 protected function copyAttachmentFileData(Attachment $source, Attachment $target, $sourceBoardId, $targetBoardId)
303 307 {
304 308 $result = [
305 309 'old_path' => null,
@@ -319,8 +323,12 @@
319 323 if (!$sourcePath || !is_file($sourcePath) || !is_readable($sourcePath)) {
320 324 return $result;
321 325 }
322 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 +
323 331 $targetDir = $this->getBoardDir($targetBoardId);
324 332 if (!is_dir($targetDir)) {
325 333 wp_mkdir_p($targetDir);
326 334 }
@@ -351,19 +359,53 @@
351 359 if (!$urlMap || empty($task->description)) {
352 360 return;
353 361 }
354 362
355 - $description = $task->description;
356 -
363 + $replacements = [];
357 364 foreach ($urlMap as $oldUrl => $newUrl) {
358 - $description = str_replace($oldUrl, $newUrl, $description);
359 - $description = str_replace(esc_url($oldUrl), esc_url($newUrl), $description);
360 - $description = str_replace(esc_attr($oldUrl), esc_attr($newUrl), $description);
365 + $identity = $this->getPublicAttachmentUrlIdentity($oldUrl);
366 + if ($identity !== null) {
367 + $replacements[$identity] = $newUrl;
368 + }
361 369 }
362 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 +
363 383 if ($description !== $task->description) {
364 384 $task->description = $description;
365 385 }
386 + }
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);
366 408 }
367 409
368 410 protected function updateTaskCoverFromFileResults(Task $task, array $fileResults, $targetBoardId, $sourceCoverImageId = null)
369 411 {