## 1. sanitizeUrl() (already existed, verified applied) - Applied to ShopDetailPage, CoachDetailPage with urlDomain() tooltip hints - Blocks javascript:, data:, vbscript: and all non-http(s) schemes - Returns null for invalid URLs — link simply doesn't render ## 2. Geohash validation in parsers - Added isValidGeohash() in types.ts using ngeohash.decode() - Min 4 chars (country-level precision rejected), max 12 chars - Rejects lat/lng outside Earth bounds - Rejects 0,0 "null island" — classic bot artifact - VPN/Tor unaffected: geohash comes from map pin, not user IP ## 3. Content length limits in all parsers - MAX_TITLE_LEN=100, MAX_DESCRIPTION_LEN=3000, MAX_LOCATION_LEN=200 - MIN_TITLE_LEN=2, MIN_GEOHASH_LEN=4 - Applied to parseSkateSpot, parseSkateShop, parseSkateCoach, parseSkateFilmer, parseSkateEditor, parseSkateEvent ## 4. Smart WoT default (already in codebase, verified correct) - Logged out / <5 follows → 'all' (don't punish new/anon users) - Logged in with ≥5 follows → 'trusted' (sort trusted first) - Soft mode: trusted content floats to top, nothing hidden - Active in MapPage and EventsPage ## 5. Trusted count stats in useGrowthStats - Added trustedSpots and trustedMappers to GrowthStats type - Heuristic: pubkeys with ≥2 published items = organic community member - CommunityStatsPage now shows "🛡 X from active accounts" below spot count - Purely additive — both numbers always visible ## 6. OfficialBadge component (already existed, wired up) - Added to SpotCard, EventCard, ShopDetailPage, CoachDetailPage - Only renders for FOUNDER_PUBKEY (20e7c953...) - Zero-friction: invisible for all other pubkeys - Impersonation attempts immediately visible ## 7. Leaflet marker clustering - Installed leaflet.markercluster v1.5.3 + @types - MarkerClusterGroup replaces direct marker.addTo(map) - Custom cluster icon: red/orange gradient circle with count - maxClusterRadius: 60px, spiderfyOnMaxZoom: true - MarkerCluster CSS loaded from CDN in index.html ## 8. DM phishing warning banner - PhishingWarningBanner component in DirectMessagesPage - One-time dismissible (localStorage key: dm-phishing-warning-dismissed) - Message: SkateSpots will never DM you asking for keys/payments - Uses useLocalStorage hook, Shield icon, X dismiss button Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
62 lines
1.7 KiB
TypeScript
62 lines
1.7 KiB
TypeScript
/**
|
|
* Sanitizes a user-supplied URL to prevent phishing and script injection.
|
|
*
|
|
* Rules:
|
|
* - Only allows https:// and http:// schemes
|
|
* - Blocks javascript:, data:, vbscript:, and all other schemes
|
|
* - Returns null for invalid / unsafe URLs (caller should hide the link)
|
|
*
|
|
* VPN/Tor users are unaffected — this validates the URL scheme & structure,
|
|
* not the user's network origin.
|
|
*/
|
|
export function sanitizeUrl(url: string | undefined | null): string | null {
|
|
if (!url) return null;
|
|
|
|
const trimmed = url.trim();
|
|
if (!trimmed) return null;
|
|
|
|
try {
|
|
const parsed = new URL(trimmed);
|
|
// Only allow safe schemes
|
|
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
return null;
|
|
}
|
|
return parsed.href;
|
|
} catch {
|
|
// Not a valid URL
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extracts just the hostname for display (e.g. "Opens: example.com")
|
|
*/
|
|
export function urlDomain(url: string): string {
|
|
try {
|
|
return new URL(url).hostname.replace(/^www\./, '');
|
|
} catch {
|
|
return url;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sanitizes a tel: or mailto: href.
|
|
* Only allows the specific scheme, strips anything else.
|
|
*/
|
|
export function sanitizeTel(value: string | undefined | null): string | null {
|
|
if (!value) return null;
|
|
const trimmed = value.trim();
|
|
// Allow digits, +, -, spaces, parentheses only
|
|
const digits = trimmed.replace(/[^0-9+\-\s().]/g, '');
|
|
return digits.length >= 5 ? `tel:${digits}` : null;
|
|
}
|
|
|
|
export function sanitizeEmail(value: string | undefined | null): string | null {
|
|
if (!value) return null;
|
|
const trimmed = value.trim();
|
|
// Basic email pattern
|
|
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
|
|
return `mailto:${trimmed}`;
|
|
}
|
|
return null;
|
|
}
|