[APP-2160] Centralize known publisher lookup with strict host matching
Move the publisher list and host detection into a single publishers.ts module so both PublicationMetaRow and the Subscribe button consult the same source. Match exact hosts and proper subdomains only, rejecting lookalikes like evilleaflet.pub.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import {Fragment, type ReactNode, useMemo} from 'react'
|
||||
import {Fragment, type ReactNode} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
@@ -8,6 +8,7 @@ import {toNiceDomain} from '#/lib/strings/url-helpers'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import {matchStandardSitePublisher} from '#/components/Post/Embed/StandardSiteEmbed/publishers'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function PublicationMetaRow({
|
||||
@@ -25,18 +26,7 @@ export function PublicationMetaRow({
|
||||
const {t: l} = useLingui()
|
||||
const profileQuery = useProfileQuery({did: author.did ?? undefined})
|
||||
const handle = author.did ? profileQuery.data?.handle : undefined
|
||||
const highlightedPublisher = useMemo(() => {
|
||||
try {
|
||||
const u = new URL(view.source?.uri || '')
|
||||
return (
|
||||
u.host.endsWith('leaflet.pub') ||
|
||||
u.host.endsWith('pckt.blog') ||
|
||||
u.host.endsWith('offprint.app')
|
||||
)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}, [view])
|
||||
const highlightedPublisher = !!matchStandardSitePublisher(view)
|
||||
|
||||
const metaTextStyle = [
|
||||
a.text_xs,
|
||||
|
||||
@@ -14,14 +14,12 @@ import {ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {Clock_Stroke2_Corner0_Rounded as Clock} from '#/components/icons/Clock'
|
||||
import {Leaflet} from '#/components/icons/community/Leaflet'
|
||||
import {Offprint} from '#/components/icons/community/Offprint'
|
||||
import {Pckt} from '#/components/icons/community/Pckt'
|
||||
import {StandardSite} from '#/components/icons/community/StandardSite'
|
||||
import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
||||
import {Link} from '#/components/Link'
|
||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import {PublicationMetaRow} from '#/components/Post/Embed/StandardSiteEmbed/PublicationMetaRow'
|
||||
import {matchStandardSitePublisher} from '#/components/Post/Embed/StandardSiteEmbed/publishers'
|
||||
import {StandardSiteThemeProvider} from '#/components/Post/Embed/StandardSiteEmbed/StandardSiteThemeProvider'
|
||||
import {isStandardSitePublicationEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils'
|
||||
import {Text} from '#/components/Typography'
|
||||
@@ -37,33 +35,6 @@ const PUBLICATION_AVATAR_STYLE = {
|
||||
borderRadius: a.rounded_sm.borderRadius,
|
||||
}
|
||||
|
||||
export function useStandardSitePublisherConfig(
|
||||
view: AppBskyEmbedExternal.ViewExternal,
|
||||
) {
|
||||
try {
|
||||
const u = new URL(view.source?.uri || '')
|
||||
if (u.host.endsWith('leaflet.pub')) {
|
||||
return {
|
||||
name: 'Leaflet',
|
||||
Icon: Leaflet,
|
||||
}
|
||||
} else if (u.host.endsWith('pckt.blog')) {
|
||||
return {
|
||||
name: 'pckt',
|
||||
Icon: Pckt,
|
||||
}
|
||||
} else if (u.host.endsWith('offprint.app')) {
|
||||
return {
|
||||
name: 'Offprint',
|
||||
Icon: Offprint,
|
||||
}
|
||||
}
|
||||
return null
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const StandardSiteEmbed = ({
|
||||
view,
|
||||
onOpen,
|
||||
@@ -431,7 +402,7 @@ export function SubscribeButton({
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const {t: l} = useLingui()
|
||||
const highlightedPublisher = useStandardSitePublisherConfig(view)
|
||||
const highlightedPublisher = matchStandardSitePublisher(view)
|
||||
const cta = highlightedPublisher
|
||||
? l`Subscribe on ${highlightedPublisher.name}`
|
||||
: l`View publication`
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import {type AppBskyEmbedExternal} from '@atproto/api'
|
||||
|
||||
import {Leaflet} from '#/components/icons/community/Leaflet'
|
||||
import {Offprint} from '#/components/icons/community/Offprint'
|
||||
import {Pckt} from '#/components/icons/community/Pckt'
|
||||
|
||||
export type StandardSitePublisher = {
|
||||
host: string
|
||||
name: string
|
||||
Icon: typeof Leaflet
|
||||
}
|
||||
|
||||
const STANDARD_SITE_PUBLISHERS: StandardSitePublisher[] = [
|
||||
{host: 'leaflet.pub', name: 'Leaflet', Icon: Leaflet},
|
||||
{host: 'pckt.blog', name: 'pckt', Icon: Pckt},
|
||||
{host: 'offprint.app', name: 'Offprint', Icon: Offprint},
|
||||
]
|
||||
|
||||
export function getStandardSitePublisherHost(
|
||||
view: AppBskyEmbedExternal.ViewExternal,
|
||||
): string | null {
|
||||
try {
|
||||
return new URL(view.source?.uri || '').host
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function hostMatches(host: string, target: string): boolean {
|
||||
return host === target || host.endsWith('.' + target)
|
||||
}
|
||||
|
||||
export function matchStandardSitePublisher(
|
||||
view: AppBskyEmbedExternal.ViewExternal,
|
||||
): StandardSitePublisher | null {
|
||||
const host = getStandardSitePublisherHost(view)
|
||||
if (!host) return null
|
||||
return STANDARD_SITE_PUBLISHERS.find(p => hostMatches(host, p.host)) ?? null
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import {type AppBskyEmbedExternal} from '@atproto/api'
|
||||
|
||||
import {isStandardSiteEmbed, isStandardSitePublicationEmbed} from './utils'
|
||||
|
||||
function makeView(
|
||||
partial: Record<string, unknown>,
|
||||
): AppBskyEmbedExternal.ViewExternal {
|
||||
return {
|
||||
uri: 'https://example.com/post',
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
...partial,
|
||||
}
|
||||
}
|
||||
|
||||
describe('isStandardSiteEmbed', () => {
|
||||
it('returns true when any associated ref is in the site.standard.* namespace', () => {
|
||||
const view = makeView({
|
||||
associatedRefs: [{uri: 'at://did:plc:abc/site.standard.publication/foo'}],
|
||||
})
|
||||
expect(isStandardSiteEmbed(view)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when no associated refs are in the site.standard.* namespace', () => {
|
||||
const view = makeView({
|
||||
associatedRefs: [{uri: 'at://did:plc:abc/app.bsky.feed.post/foo'}],
|
||||
})
|
||||
expect(isStandardSiteEmbed(view)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns falsy when associatedRefs is missing', () => {
|
||||
expect(isStandardSiteEmbed(makeView({}))).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('isStandardSitePublicationEmbed', () => {
|
||||
it('returns true with at least one publication ref and no document refs', () => {
|
||||
const view = makeView({
|
||||
associatedRefs: [{uri: 'at://did:plc:abc/site.standard.publication/foo'}],
|
||||
})
|
||||
expect(isStandardSitePublicationEmbed(view)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when any ref is a document', () => {
|
||||
const view = makeView({
|
||||
associatedRefs: [
|
||||
{uri: 'at://did:plc:abc/site.standard.publication/foo'},
|
||||
{uri: 'at://did:plc:abc/site.standard.document/bar'},
|
||||
],
|
||||
})
|
||||
expect(isStandardSitePublicationEmbed(view)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false when there are no publication refs', () => {
|
||||
const view = makeView({
|
||||
associatedRefs: [{uri: 'at://did:plc:abc/site.standard.other/foo'}],
|
||||
})
|
||||
expect(isStandardSitePublicationEmbed(view)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns falsy for an empty associatedRefs array', () => {
|
||||
expect(isStandardSitePublicationEmbed(makeView({associatedRefs: []}))).toBe(
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
it('returns falsy when associatedRefs is missing', () => {
|
||||
expect(isStandardSitePublicationEmbed(makeView({}))).toBeFalsy()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user