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 +39 -5 2.0.152.1.0 View file →
@@ -351,19 +351,53 @@
351 351 if (!$urlMap || empty($task->description)) {
352 352 return;
353 353 }
354 354
355 - $description = $task->description;
356 -
355 + $replacements = [];
357 356 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);
357 + $identity = $this->getPublicAttachmentUrlIdentity($oldUrl);
358 + if ($identity !== null) {
359 + $replacements[$identity] = $newUrl;
360 + }
361 361 }
362 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 +
363 375 if ($description !== $task->description) {
364 376 $task->description = $description;
365 377 }
378 + }
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);
366 400 }
367 401
368 402 protected function updateTaskCoverFromFileResults(Task $task, array $fileResults, $targetBoardId, $sourceCoverImageId = null)
369 403 {