mirror of
https://github.com/modrinth/code.git
synced 2026-08-30 19:46:33 +00:00
180 lines
4.2 KiB
TypeScript
180 lines
4.2 KiB
TypeScript
import type { ElementNode } from 'comark'
|
|
import { defineComarkPlugin } from 'comark/parse'
|
|
import { visitAsync } from 'comark/utils'
|
|
|
|
// Imgur is blocked in UK and Indonesia
|
|
const imgurProxyCountries = new Set(['GB', 'ID'])
|
|
|
|
type MarkdownUserCountryResolver = () => string | null | undefined
|
|
|
|
let resolveMarkdownUserCountry: MarkdownUserCountryResolver = () => undefined
|
|
|
|
export const setMarkdownUserCountryResolver = (resolver: MarkdownUserCountryResolver) => {
|
|
resolveMarkdownUserCountry = resolver
|
|
}
|
|
|
|
const shouldProxyImgur = () => {
|
|
try {
|
|
const country = resolveMarkdownUserCountry()
|
|
return country ? imgurProxyCountries.has(country.toUpperCase()) : false
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
const getImgurProxyUrl = (value: string) => {
|
|
try {
|
|
const url = new URL(value)
|
|
if (url.hostname !== 'imgur.com' && !url.hostname.endsWith('.imgur.com')) {
|
|
return undefined
|
|
}
|
|
|
|
return `https://external-content.duckduckgo.com/iu/?u=${encodeURIComponent(url.toString())}.png`
|
|
} catch {
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
const allowedMediaHostnames = [
|
|
'imgur.com',
|
|
'i.imgur.com',
|
|
'cdn-raw.modrinth.com',
|
|
'cdn.modrinth.com',
|
|
'staging-cdn-raw.modrinth.com',
|
|
'staging-cdn.modrinth.com',
|
|
'github.com',
|
|
'raw.githubusercontent.com',
|
|
'user-images.githubusercontent.com',
|
|
'img.shields.io',
|
|
'i.postimg.cc',
|
|
'wsrv.nl',
|
|
'cf.way2muchnoise.eu',
|
|
'bstats.org',
|
|
'cdn.serilum.com',
|
|
'workflow.serilum.com',
|
|
'modfolio.creeperkatze.dev',
|
|
'badges.crowdin.net',
|
|
]
|
|
|
|
const allowedMediaHostnameSuffixes = ['.github.io']
|
|
|
|
export function resolveMediaSrc(value: unknown): string | undefined {
|
|
if (typeof value !== 'string' || !value || value.startsWith('data:')) {
|
|
return typeof value === 'string' ? value : undefined
|
|
}
|
|
|
|
if (shouldProxyImgur()) {
|
|
const proxied = getImgurProxyUrl(value)
|
|
if (proxied) return proxied
|
|
}
|
|
|
|
try {
|
|
const url = new URL(value)
|
|
|
|
if (url.hostname.includes('wsrv.nl')) {
|
|
url.searchParams.delete('errorredirect')
|
|
url.searchParams.delete('default')
|
|
}
|
|
|
|
const allowed =
|
|
allowedMediaHostnames.includes(url.hostname) ||
|
|
allowedMediaHostnameSuffixes.some((suffix) => url.hostname.endsWith(suffix))
|
|
|
|
if (!allowed) {
|
|
return `https://wsrv.nl/?url=${encodeURIComponent(url.toString())}&n=-1`
|
|
}
|
|
|
|
return url.toString()
|
|
} catch {
|
|
return value
|
|
}
|
|
}
|
|
|
|
const iframeAllowedSources: {
|
|
url: RegExp
|
|
allowedParameters: string[]
|
|
}[] = [
|
|
{
|
|
url: /^https?:\/\/(www\.)?youtube(-nocookie)?\.com\/embed\/[a-zA-Z0-9_-]{11}/,
|
|
allowedParameters: ['start', 'end'],
|
|
},
|
|
{
|
|
url: /^https?:\/\/(www\.)?discord\.com\/widget/,
|
|
allowedParameters: ['id'],
|
|
},
|
|
]
|
|
|
|
export function resolveIframeSrc(value: unknown): string | undefined {
|
|
if (typeof value !== 'string' || !value) return undefined
|
|
|
|
try {
|
|
const url = new URL(value)
|
|
|
|
for (const source of iframeAllowedSources) {
|
|
if (!source.url.test(url.href)) continue
|
|
|
|
const newParams = new URLSearchParams()
|
|
url.searchParams.forEach((val, key) => {
|
|
if (source.allowedParameters.includes(key)) newParams.set(key, val)
|
|
})
|
|
url.search = newParams.toString()
|
|
return url.toString()
|
|
}
|
|
} catch {
|
|
/* empty */
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
|
|
export function resolveLinkRel(href: unknown): string | undefined {
|
|
if (typeof href !== 'string' || !href) return 'noopener nofollow ugc'
|
|
|
|
try {
|
|
const url = new URL(href)
|
|
if (url.hostname === 'modrinth.com') return undefined
|
|
} catch {
|
|
/* empty */
|
|
}
|
|
|
|
return 'noopener nofollow ugc'
|
|
}
|
|
|
|
export const modrinthResolveMedia = defineComarkPlugin(() => ({
|
|
name: 'modrinth-resolve-media',
|
|
async post(state) {
|
|
await visitAsync(
|
|
state.tree,
|
|
(node) => typeof node !== 'string' && node[0] !== null,
|
|
(node) => {
|
|
const element = node as ElementNode
|
|
const attrs = element[1]
|
|
|
|
switch (element[0].toLowerCase()) {
|
|
case 'a':
|
|
attrs.rel = resolveLinkRel(attrs.href)
|
|
return
|
|
case 'img':
|
|
case 'audio':
|
|
attrs.src = resolveMediaSrc(attrs.src)
|
|
return
|
|
case 'video':
|
|
attrs.src = resolveMediaSrc(attrs.src)
|
|
attrs.poster = resolveMediaSrc(attrs.poster)
|
|
return
|
|
case 'source':
|
|
attrs.src = resolveMediaSrc(attrs.src)
|
|
attrs.srcset = resolveMediaSrc(attrs.srcset)
|
|
return
|
|
case 'iframe': {
|
|
const src = resolveIframeSrc(attrs.src)
|
|
if (!src) return false
|
|
attrs.src = src
|
|
return
|
|
}
|
|
}
|
|
},
|
|
)
|
|
},
|
|
}))
|