Fix PostIt attachment links to match uploaded filenames

Apply the shared filename filter before URL encoding, preserve escaped URLs and original link labels, and add regression coverage for accented and spaced filenames. Update the changelog.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Paul Schneider 2026-10-11 01:38:25 +01:00
commit 63b44b5a92
Signed by: notazof
GPG key ID: AD4B9B545FBD7D12
3 changed files with 22 additions and 9 deletions

View file

@ -24,6 +24,10 @@
### Fixed ### Fixed
* [PostIt] Les liens Markdown des pièces jointes utilisent désormais
le même filtre de nom que l'upload serveur, avant l'encodage URL.
Le libellé conserve le nom original, notamment ses accents.
* [Yavsc.Abstract][Yavsc.Blogs] Encodage des caractères non supportés * [Yavsc.Abstract][Yavsc.Blogs] Encodage des caractères non supportés
dans les noms de fichiers téléversés avec `_` au lieu de `#`, pour dans les noms de fichiers téléversés avec `_` au lieu de `#`, pour
éviter les fragments d'URL et permettre le téléchargement via les éviter les fragments d'URL et permettre le téléchargement via les

View file

@ -20,8 +20,13 @@ namespace PostIt.Tests;
/// </summary> /// </summary>
public class BlogAttachmentLinkTests public class BlogAttachmentLinkTests
{ {
[Fact] [Theory]
public async Task Save_with_attachment_appends_absolute_link_derived_from_the_oidc_authority() [InlineData("note.txt", "note.txt")]
[InlineData("INSC_E03686381_SCHNEIDER_000113378AA__Troisi\u00e8me__.PDF",
"INSC_E03686381_SCHNEIDER_000113378AA__Troisi_232me__.PDF")]
[InlineData("rapport final.pdf", "rapport%20final.pdf")]
public async Task Save_with_attachment_appends_absolute_link_derived_from_the_oidc_authority(
string originalName, string urlName)
{ {
const string authority = "https://idp.example.test"; const string authority = "https://idp.example.test";
@ -53,7 +58,7 @@ public class BlogAttachmentLinkTests
vm.DraftTitle = "Après"; vm.DraftTitle = "Après";
vm.DraftArticle = "Contenu modifié."; vm.DraftArticle = "Contenu modifié.";
vm.DraftAttachments.Add(new BlogUploadFile( vm.DraftAttachments.Add(new BlogUploadFile(
"note.txt", originalName,
System.Text.Encoding.UTF8.GetBytes("payload test"), System.Text.Encoding.UTF8.GetBytes("payload test"),
"text/plain")); "text/plain"));
@ -68,12 +73,14 @@ public class BlogAttachmentLinkTests
var sent = Assert.IsType<System.Func<HttpContent>>(linkPut.body); var sent = Assert.IsType<System.Func<HttpContent>>(linkPut.body);
var multipartContent = Assert.IsType<MultipartFormDataContent>(sent()); var multipartContent = Assert.IsType<MultipartFormDataContent>(sent());
var expectedUrl = $"{authority}/files/tester/blogs/42/note.txt"; var expectedUrl = $"{authority}/files/tester/blogs/42/{urlName}";
var sentStringContent = multipartContent.First() as StringContent ; var sentStringContent = Assert.IsType<StringContent>(multipartContent.First());
var sentString = await sentStringContent.ReadAsStringAsync(TestContext.Current.CancellationToken); var sentString = await sentStringContent.ReadAsStringAsync(TestContext.Current.CancellationToken);
Assert.Contains($"- [note.txt]({expectedUrl})", sentString); using var document = System.Text.Json.JsonDocument.Parse(sentString);
var article = document.RootElement.GetProperty("article").GetString();
Assert.Contains($"- [{originalName}]({expectedUrl})", article);
// The regression shape: a relative, authority-less link. // The regression shape: a relative, authority-less link.
Assert.DoesNotContain("](/files/", sentString); Assert.DoesNotContain("](/files/", article);
} }
} }

View file

@ -10,6 +10,7 @@ using Microsoft.Extensions.DependencyInjection;
using Yavsc.Blogspot; using Yavsc.Blogspot;
using Yavsc.Api.Client; using Yavsc.Api.Client;
using Yavsc.Abstract.Files; using Yavsc.Abstract.Files;
using Yavsc.Server.Helpers;
using AvaloniaEdit.Document; using AvaloniaEdit.Document;
namespace PostIt.ViewModels; namespace PostIt.ViewModels;
@ -611,7 +612,8 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel
foreach (var attachment in attachments) foreach (var attachment in attachments)
{ {
var relativePath = $"{EscapePathSegment(ownerSegment)}/blogs/{post.Id}/{EscapePathSegment(attachment.FileName)}"; var storedName = AbstractFileSystemHelpers.FilterFileName(attachment.FileName);
var relativePath = $"{EscapePathSegment(ownerSegment)}/blogs/{post.Id}/{EscapePathSegment(storedName)}";
var fileUrl = ResolveUserFileUrl(relativePath); var fileUrl = ResolveUserFileUrl(relativePath);
var markdownLine = $"- [{attachment.FileName}]({fileUrl})"; var markdownLine = $"- [{attachment.FileName}]({fileUrl})";
@ -636,7 +638,7 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel
if (!string.IsNullOrWhiteSpace(authority) if (!string.IsNullOrWhiteSpace(authority)
&& Uri.TryCreate(authority, UriKind.Absolute, out var baseUri)) && Uri.TryCreate(authority, UriKind.Absolute, out var baseUri))
{ {
return FileServerUrlHelpers.GetUserFilesUri(baseUri, relativePath).ToString(); return FileServerUrlHelpers.GetUserFilesUri(baseUri, relativePath).AbsoluteUri;
} }
return $"{Yavsc.Constants.UserFilesPath}/{relativePath}"; return $"{Yavsc.Constants.UserFilesPath}/{relativePath}";