| @@ -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 | { |