From 526e8abc3ea3e1a7b3b08b027808c5033514a40b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Aug 2026 19:46:50 +0000 Subject: [PATCH] fix(playlist): mask query passwords on M3U cards, drop stale auto-detect prefills, doc the assembled-host exception MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Copilot's suppressed review comments: - The m3u-url candidate card no longer shows a get.php password in clear — the display masks the query value while the untouched URL still prefills the form. - A picked candidate is dropped if the user switches to another method before its target form mounts, so it cannot lie in wait and prefill a later visit. - The parser contract now names its one assembled-value exception (labeledHostUrl completing a scheme-less host with http:// and a labeled port). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4 --- .../add-playlist-dialog.component.spec.ts | 22 +++++++++++++++++++ .../add-playlist-dialog.component.ts | 8 +++++++ .../auto-import/auto-import.component.spec.ts | 14 ++++++++++++ .../lib/auto-import/auto-import.component.ts | 11 +++++++++- .../src/lib/provider-import-detection.util.ts | 4 +++- 5 files changed, 57 insertions(+), 2 deletions(-) diff --git a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.spec.ts b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.spec.ts index 8e55c858c..2b3a42252 100644 --- a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.spec.ts +++ b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.spec.ts @@ -283,6 +283,28 @@ describe('AddPlaylistDialogComponent', () => { expect(patchValue).toHaveBeenCalledTimes(1); }); + it('drops the candidate when the user switches to another method first', () => { + const patchValue = jest.fn(); + (component as { xtreamImport: jest.Mock }).xtreamImport = jest.fn( + () => ({ form: { patchValue } }) + ); + + component.onCandidateSelected({ + kind: 'xtream', + confidence: 'high', + username: 'alice', + }); + // The user clicks another tile before the xtream form mounted. + component.method.set('file'); + applyPrefill(); + + // Coming back to the xtream form later must not prefill it. + component.method.set('xtream'); + applyPrefill(); + + expect(patchValue).not.toHaveBeenCalled(); + }); + it('keeps the candidate pending while the target form does not exist yet', () => { (component as { xtreamImport: jest.Mock }).xtreamImport = jest.fn( () => undefined diff --git a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.ts b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.ts index 4e4bd7fd3..24065dea8 100644 --- a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.ts +++ b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.ts @@ -181,6 +181,14 @@ export class AddPlaylistDialogComponent { if (!candidate) { return; } + // The user can click another method tile before the target form + // mounts; a candidate must not lie in wait and prefill a later visit + // to its form. Only the method the selection itself switched to may + // consume it. + if (this.method() !== METHOD_BY_CANDIDATE_KIND[candidate.kind]) { + this.pendingPrefill.set(null); + return; + } switch (candidate.kind) { case 'm3u-url': { const child = this.urlUpload(); diff --git a/libs/playlist/import/feature/src/lib/auto-import/auto-import.component.spec.ts b/libs/playlist/import/feature/src/lib/auto-import/auto-import.component.spec.ts index d41a7b0c9..0859f3d75 100644 --- a/libs/playlist/import/feature/src/lib/auto-import/auto-import.component.spec.ts +++ b/libs/playlist/import/feature/src/lib/auto-import/auto-import.component.spec.ts @@ -80,6 +80,20 @@ describe('AutoImportComponent', () => { expect(emitted).toEqual([candidate]); }); + it('masks the query password of an M3U link on the card', () => { + fixture.detectChanges(); + + const rows = component.summaryRows({ + kind: 'm3u-url', + confidence: 'low', + url: 'http://tv.example.com:8080/get.php?username=alice&password=s3cret&type=m3u_plus', + }); + + expect(rows[0].value).toContain('username=alice'); + expect(rows[0].value).toContain('password=••••••'); + expect(JSON.stringify(rows)).not.toContain('s3cret'); + }); + it('masks the password in the summary rows', () => { fixture.detectChanges(); diff --git a/libs/playlist/import/feature/src/lib/auto-import/auto-import.component.ts b/libs/playlist/import/feature/src/lib/auto-import/auto-import.component.ts index 66533af24..af6c3bf5b 100644 --- a/libs/playlist/import/feature/src/lib/auto-import/auto-import.component.ts +++ b/libs/playlist/import/feature/src/lib/auto-import/auto-import.component.ts @@ -130,7 +130,16 @@ export class AutoImportComponent implements OnInit { }; switch (candidate.kind) { case 'm3u-url': - push('HOME.URL_UPLOAD.PLAYLIST_URL', candidate.url); + // get.php links carry the password in their query — the card + // must not show it while every other password here is masked. + // The candidate keeps the untouched URL for prefilling. + push( + 'HOME.URL_UPLOAD.PLAYLIST_URL', + candidate.url?.replace( + /([?&]password=)[^&#\s]*/i, + '$1••••••' + ) + ); break; case 'xtream': push( diff --git a/libs/shared/interfaces/src/lib/provider-import-detection.util.ts b/libs/shared/interfaces/src/lib/provider-import-detection.util.ts index 299907735..a3a96db18 100644 --- a/libs/shared/interfaces/src/lib/provider-import-detection.util.ts +++ b/libs/shared/interfaces/src/lib/provider-import-detection.util.ts @@ -26,7 +26,9 @@ import { * Unicode math alphabets (`𝚄𝚂𝙴𝚁➤`, `𝙿𝙰𝚂𝚂➤`) to slip past chat spam * filters; the folding turns them back into the ASCII they denote, and * real credentials/URLs are ASCII already, so values pass through it - * unchanged. + * unchanged. The one deliberate exception is `labeledHostUrl`, which + * completes a scheme-less labeled host with `http://` and a separately + * labeled port — an assembled value, landing in an editable form field. * 2. **Detection proposes, it never decides.** The output prefills the * existing import forms; classification authority stays with the * behavioral probes those forms already run (portal discovery, Xtream