[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 {View} from 'react-native'
|
||||||
import {type AppBskyEmbedExternal} from '@atproto/api'
|
import {type AppBskyEmbedExternal} from '@atproto/api'
|
||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
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 {useProfileQuery} from '#/state/queries/profile'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
|
import {matchStandardSitePublisher} from '#/components/Post/Embed/StandardSiteEmbed/publishers'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
export function PublicationMetaRow({
|
export function PublicationMetaRow({
|
||||||
@@ -25,18 +26,7 @@ export function PublicationMetaRow({
|
|||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const profileQuery = useProfileQuery({did: author.did ?? undefined})
|
const profileQuery = useProfileQuery({did: author.did ?? undefined})
|
||||||
const handle = author.did ? profileQuery.data?.handle : undefined
|
const handle = author.did ? profileQuery.data?.handle : undefined
|
||||||
const highlightedPublisher = useMemo(() => {
|
const highlightedPublisher = !!matchStandardSitePublisher(view)
|
||||||
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 metaTextStyle = [
|
const metaTextStyle = [
|
||||||
a.text_xs,
|
a.text_xs,
|
||||||
|
|||||||
@@ -14,14 +14,12 @@ import {ButtonIcon, ButtonText} from '#/components/Button'
|
|||||||
import {Divider} from '#/components/Divider'
|
import {Divider} from '#/components/Divider'
|
||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
import {Clock_Stroke2_Corner0_Rounded as Clock} from '#/components/icons/Clock'
|
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 {StandardSite} from '#/components/icons/community/StandardSite'
|
||||||
import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
||||||
import {Link} from '#/components/Link'
|
import {Link} from '#/components/Link'
|
||||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||||
import {PublicationMetaRow} from '#/components/Post/Embed/StandardSiteEmbed/PublicationMetaRow'
|
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 {StandardSiteThemeProvider} from '#/components/Post/Embed/StandardSiteEmbed/StandardSiteThemeProvider'
|
||||||
import {isStandardSitePublicationEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils'
|
import {isStandardSitePublicationEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
@@ -37,33 +35,6 @@ const PUBLICATION_AVATAR_STYLE = {
|
|||||||
borderRadius: a.rounded_sm.borderRadius,
|
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 = ({
|
export const StandardSiteEmbed = ({
|
||||||
view,
|
view,
|
||||||
onOpen,
|
onOpen,
|
||||||
@@ -431,7 +402,7 @@ export function SubscribeButton({
|
|||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
}) {
|
}) {
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const highlightedPublisher = useStandardSitePublisherConfig(view)
|
const highlightedPublisher = matchStandardSitePublisher(view)
|
||||||
const cta = highlightedPublisher
|
const cta = highlightedPublisher
|
||||||
? l`Subscribe on ${highlightedPublisher.name}`
|
? l`Subscribe on ${highlightedPublisher.name}`
|
||||||
: l`View publication`
|
: 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