From 69b5637bd6c138d24df75ba42d700db6cb7760fc Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 13 Jul 2026 17:50:07 -0400 Subject: [PATCH] fix(extension): show Add-to-FC button on subscribed Patreon creators (#1485) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symptom (operator-flagged): the extension injected the Add-as-source button on a Patreon creator you had NOT subscribed to, but it disappeared once you were subscribed — the opposite of when it's useful. Root cause (extension logic, not Patreon security): Patreon serves a creator under three URL shapes — bare patreon.com/Atole, patreon.com/c/Atole, and patreon.com/cw/Atole (the 'creator workspace' URL you land on once subscribed; documented in patreon_resolver._VANITY_RE). The button's artist-page gate (PLATFORM_ARTIST_PATTERNS.patreon in platforms.js) and its byte-mirror probe pattern (_PLATFORM_PATTERNS in extension_service._derive) only matched the bare single-segment form and explicitly excluded c/. So the subscribed-view URL failed the gate → no button. The ingestion resolver already handled all three; only these two gates were too narrow. Fix: both regexes now accept optional cw/ and c/ prefixes and drop the strict single-segment end-anchor, so a creator's inner page (/cw/Atole/posts, /Atole/membership) also matches — robust to whatever exact shape the subscribed view uses. Nav-page exclusions (home/search/messages/notifications/library/ settings/posts + post permalinks) preserved. New unit test covers all three prefixes, sub-paths, and nav-page rejection (both regexes validated identically). Bump extension 1.0.7→1.0.8 so a fresh signed XPI ships the fix (also exercises batch-5 web-ext-10's AMO sign path end-to-end on the main build). Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/app/services/extension_service.py | 9 +++- extension/lib/platforms.js | 11 +++- extension/manifest.json | 2 +- extension/package.json | 2 +- tests/test_extension_url_derive.py | 65 +++++++++++++++++++++++ 5 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 tests/test_extension_url_derive.py diff --git a/backend/app/services/extension_service.py b/backend/app/services/extension_service.py index f8fea43..25cb03a 100644 --- a/backend/app/services/extension_service.py +++ b/backend/app/services/extension_service.py @@ -35,9 +35,14 @@ class InvalidUrlError(Exception): # reviewers catch drift. _PLATFORM_PATTERNS: list[tuple[str, re.Pattern[str]]] = [ ("patreon", re.compile( + # Three creator URL shapes — bare (patreon.com/Atole), `c/`, and `cw/` + # (the "creator workspace" URL served once subscribed, see + # patreon_resolver._VANITY_RE). A trailing sub-path is allowed so a + # creator's inner page still derives the slug. Nav pages stay excluded. r"^https?://(?:www\.)?patreon\.com/" - r"(?!home$|search\b|messages\b|notifications\b|library\b|settings\b|posts\b|c/)" - r"(?P[^/?#]+)/?$", + r"(?:cw/|c/)?" + r"(?!(?:home|search|messages|notifications|library|settings|posts)(?:[/?#]|$))" + r"(?P[^/?#]+)", re.IGNORECASE, )), ("subscribestar", re.compile( diff --git a/extension/lib/platforms.js b/extension/lib/platforms.js index 3a9ed2a..2f37d93 100644 --- a/extension/lib/platforms.js +++ b/extension/lib/platforms.js @@ -86,7 +86,16 @@ const PLATFORMS = { * script to decide whether to show the floating "Add as source" button. */ const PLATFORM_ARTIST_PATTERNS = { - patreon: /^https?:\/\/(www\.)?patreon\.com\/(?!home$|search\b|messages\b|notifications\b|library\b|settings\b|posts\b|c\/)[^/?#]+\/?$/i, + // Patreon serves the same creator under three URL shapes (see backend + // patreon_resolver._VANITY_RE): bare `patreon.com/Atole`, `c/` prefix, and + // `cw/` "creator workspace" — the last is the URL you land on once you're + // SUBSCRIBED, which is exactly when the button matters. Match all three, and + // drop the single-segment end-anchor so a creator's inner page + // (…/cw/Atole/posts, …/Atole/membership) also injects the button. Nav pages + // (home/search/…/posts permalink) stay excluded. Mirrors extension_service + // ._PLATFORM_PATTERNS — keep in sync (operator-flagged 2026-07-13: button + // vanished once subscribed because the old pattern only matched the bare root). + patreon: /^https?:\/\/(www\.)?patreon\.com\/(?:cw\/|c\/)?(?!(?:home|search|messages|notifications|library|settings|posts)(?:[\/?#]|$))[^/?#]+/i, subscribestar: /^https?:\/\/(www\.)?subscribestar\.(com|adult)\/(?!feed$|messages$|library$)[^/?#]+\/?$/i, hentaifoundry: /^https?:\/\/(www\.)?hentai-foundry\.com\/user\/[^/?#]+/i, deviantart: /^https?:\/\/(www\.)?deviantart\.com\/(?!home$|watch\b|tag\b|browse\b)[^/?#]+\/?$/i, diff --git a/extension/manifest.json b/extension/manifest.json index fc930bc..c5f12c9 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "FabledCurator", - "version": "1.0.7", + "version": "1.0.8", "description": "Export cookies from supported platforms to FabledCurator and add creators as sources in one click.", "browser_specific_settings": { diff --git a/extension/package.json b/extension/package.json index 08a4f53..0f96df1 100644 --- a/extension/package.json +++ b/extension/package.json @@ -1,6 +1,6 @@ { "name": "fabledcurator-extension", - "version": "1.0.7", + "version": "1.0.8", "private": true, "description": "Firefox extension for FabledCurator", "scripts": { diff --git a/tests/test_extension_url_derive.py b/tests/test_extension_url_derive.py new file mode 100644 index 0000000..b1819be --- /dev/null +++ b/tests/test_extension_url_derive.py @@ -0,0 +1,65 @@ +"""Unit tests for ExtensionService._derive — the URL → (platform, slug) +parser that gates the browser extension's "Add as source" button and pulls +the creator slug on probe/add. + +Regression cover for #1485: Patreon serves the same creator under three URL +shapes — bare `patreon.com/Atole`, `c/`, and `cw/` (the "creator workspace" +URL you land on once SUBSCRIBED). The button used to vanish while subscribed +because the pattern only matched the bare root and excluded `c/`. + +_derive is pure URL parsing (no DB / no async), so a session-less instance is +fine to exercise directly. +""" + +import pytest + +from backend.app.services.extension_service import ( + ExtensionService, + InvalidUrlError, + UnknownPlatformError, +) + +_svc = ExtensionService(None) + + +@pytest.mark.parametrize( + "url, slug", + [ + # All three Patreon creator prefixes resolve to the same vanity slug. + ("https://www.patreon.com/Atole", "Atole"), + ("https://www.patreon.com/c/Atole", "Atole"), + ("https://www.patreon.com/cw/Atole", "Atole"), # subscribed-view URL + # A creator's inner page still derives the slug (trailing sub-path). + ("https://www.patreon.com/cw/Atole/posts", "Atole"), + ("https://www.patreon.com/Atole/membership", "Atole"), + ("https://patreon.com/c/Atole", "Atole"), # bare host, no www + ], +) +def test_derive_patreon_creator_urls(url, slug): + platform, got = _svc._derive(url) + assert platform == "patreon" + assert got == slug + + +@pytest.mark.parametrize( + "url", + [ + # Patreon's own nav pages must never read as a creator slug. + "https://www.patreon.com/home", + "https://www.patreon.com/settings", + "https://www.patreon.com/search", + "https://www.patreon.com/messages", + "https://www.patreon.com/library", + "https://www.patreon.com/notifications", + "https://www.patreon.com/posts/12345", # post permalink + "https://www.patreon.com/settings/billing", # nav sub-page + ], +) +def test_derive_patreon_nav_pages_rejected(url): + with pytest.raises(UnknownPlatformError): + _svc._derive(url) + + +def test_derive_rejects_missing_scheme(): + with pytest.raises(InvalidUrlError): + _svc._derive("patreon.com/Atole")