diff --git a/admin/src/pages/PadPage.tsx b/admin/src/pages/PadPage.tsx index c216ffab79f..d1f851a58f0 100644 --- a/admin/src/pages/PadPage.tsx +++ b/admin/src/pages/PadPage.tsx @@ -418,7 +418,7 @@ export const PadPage = () => { diff --git a/src/static/js/pad_userlist.ts b/src/static/js/pad_userlist.ts index d3ee825276c..7366348a1f0 100644 --- a/src/static/js/pad_userlist.ts +++ b/src/static/js/pad_userlist.ts @@ -557,7 +557,7 @@ const paduserlist = (() => { if (localStorage.getItem('recentPads') != null) { const recentPadsList = JSON.parse(localStorage.getItem('recentPads')); const pathSegments = window.location.pathname.split('/'); - const padName = pathSegments[pathSegments.length - 1]; + const padName = decodeURIComponent(pathSegments[pathSegments.length - 1]); const existingPad = recentPadsList.find((pad) => pad.name === padName); if (existingPad) { existingPad.members = online; diff --git a/src/static/skins/colibris/index.js b/src/static/skins/colibris/index.js index f36edf84180..01591d5d833 100644 --- a/src/static/skins/colibris/index.js +++ b/src/static/skins/colibris/index.js @@ -70,12 +70,21 @@ window.customStart = () => { li.style.cursor = 'pointer'; li.className = 'recent-pad'; - const padPath = `${window.location.href}p/${pad.name}`; + // Normalize the stored name to its decoded form. Entries saved by older + // versions may already be URL-encoded; decoding first avoids + // double-encoding (e.g. %2F -> %252F) when we build the href below. + let padName = pad.name; + try { + padName = decodeURIComponent(pad.name); + } catch { + // pad.name is not valid percent-encoding; use it as-is. + } + const padPath = `${window.location.href}p/${encodeURIComponent(padName)}`; const link = document.createElement('a'); link.style.textDecoration = 'none'; link.href = padPath; - link.innerText = pad.name; + link.innerText = padName; li.appendChild(link); diff --git a/src/static/skins/colibris/pad.js b/src/static/skins/colibris/pad.js index 1e7a85b3faa..ce509a8892b 100644 --- a/src/static/skins/colibris/pad.js +++ b/src/static/skins/colibris/pad.js @@ -8,7 +8,7 @@ window.customStart = () => { $('.buttonicon').on('mouseup', function () { $(this).parent().removeClass('pressed'); }); const pathSegments = window.location.pathname.split('/'); - const padName = pathSegments[pathSegments.length - 1]; + const padName = decodeURIComponent(pathSegments[pathSegments.length - 1]); const recentPads = localStorage.getItem('recentPads'); if (recentPads == null) { localStorage.setItem('recentPads', JSON.stringify([])); diff --git a/src/tests/frontend-new/specs/embed_value.spec.ts b/src/tests/frontend-new/specs/embed_value.spec.ts index c4abe8201ba..316bed72809 100644 --- a/src/tests/frontend-new/specs/embed_value.spec.ts +++ b/src/tests/frontend-new/specs/embed_value.spec.ts @@ -133,4 +133,17 @@ test.describe('embed links', function () { await checkiFrameCode(embedCode, true, page); }); }) + + test.describe('URL encoding of pad names', function () { + test('share link encodes special characters in pad name', async function ({page}) { + const padName = 'test%2Fencoding'; + await page.goto(`http://localhost:9001/p/${padName}`); + + const shareButton = page.locator('.buttonicon-embed'); + await shareButton.click(); + + const shareLink = await page.locator('#linkinput').inputValue(); + expect(shareLink).toContain(padName); + }); + }) }) diff --git a/src/tests/frontend-new/specs/recent_pads.spec.ts b/src/tests/frontend-new/specs/recent_pads.spec.ts new file mode 100644 index 00000000000..5f83d3f6407 --- /dev/null +++ b/src/tests/frontend-new/specs/recent_pads.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Recent Pads', () => { + test('should display correctly encoded URLs for recent pads', async ({ page }) => { + const padName = 'test pad with spaces & / chars'; + const recentPads = [ + { + name: padName, + timestamp: new Date().toISOString(), + members: 1, + }, + ]; + + await page.addInitScript((data) => { + window.localStorage.setItem('recentPads', data); + }, JSON.stringify(recentPads)); + + await page.goto('http://localhost:9001/'); + + const recentPad = page.locator('.recent-pad').first(); + await expect(recentPad).toBeVisible(); + + const link = recentPad.locator('a'); + await expect(link).toHaveText(padName); + + const expectedEncodedName = encodeURIComponent(padName); + const expectedHrefRegex = new RegExp(`p/${expectedEncodedName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`); + await expect(link).toHaveAttribute('href', expectedHrefRegex); + }); +});