mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 00:55:25 +00:00
feat: imgur proxying for blocked countries (#7104)
This commit is contained in:
@@ -300,7 +300,7 @@ const {
|
|||||||
setModpackAlreadyInstalledModal,
|
setModpackAlreadyInstalledModal,
|
||||||
handleModpackDuplicateCreateAnyway,
|
handleModpackDuplicateCreateAnyway,
|
||||||
handleModpackDuplicateGoToInstance,
|
handleModpackDuplicateGoToInstance,
|
||||||
} = setupProviders(notificationManager, popupNotificationManager)
|
} = setupProviders(tauriApiClient, notificationManager, popupNotificationManager)
|
||||||
|
|
||||||
const news = ref([])
|
const news = ref([])
|
||||||
const displayedServerInviteNotifications = new Set()
|
const displayedServerInviteNotifications = new Set()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { AbstractModrinthClient } from '@modrinth/api-client'
|
||||||
import type { AbstractPopupNotificationManager, AbstractWebNotificationManager } from '@modrinth/ui'
|
import type { AbstractPopupNotificationManager, AbstractWebNotificationManager } from '@modrinth/ui'
|
||||||
|
|
||||||
import { setupCreationModal } from './setup/creation-modal'
|
import { setupCreationModal } from './setup/creation-modal'
|
||||||
@@ -5,11 +6,14 @@ import { setupFileDropProvider } from './setup/file-drop'
|
|||||||
import { setupFilePickerProvider } from './setup/file-picker'
|
import { setupFilePickerProvider } from './setup/file-picker'
|
||||||
import { setupInstanceImportProvider } from './setup/instance-import'
|
import { setupInstanceImportProvider } from './setup/instance-import'
|
||||||
import { setupTagsProvider } from './setup/tags'
|
import { setupTagsProvider } from './setup/tags'
|
||||||
|
import { setupUserCountryProvider } from './setup/user-country'
|
||||||
|
|
||||||
export function setupProviders(
|
export function setupProviders(
|
||||||
|
client: AbstractModrinthClient,
|
||||||
notificationManager: AbstractWebNotificationManager,
|
notificationManager: AbstractWebNotificationManager,
|
||||||
_popupNotificationManager: AbstractPopupNotificationManager,
|
_popupNotificationManager: AbstractPopupNotificationManager,
|
||||||
) {
|
) {
|
||||||
|
setupUserCountryProvider(client)
|
||||||
setupTagsProvider(notificationManager)
|
setupTagsProvider(notificationManager)
|
||||||
setupFileDropProvider()
|
setupFileDropProvider()
|
||||||
setupFilePickerProvider()
|
setupFilePickerProvider()
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { AbstractModrinthClient } from '@modrinth/api-client'
|
||||||
|
import { provideUserCountry } from '@modrinth/ui'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export function setupUserCountryProvider(client: AbstractModrinthClient) {
|
||||||
|
const country = ref('US')
|
||||||
|
|
||||||
|
void client.labrinth.geoip
|
||||||
|
.getCountry()
|
||||||
|
.then((detectedCountry) => {
|
||||||
|
country.value = detectedCountry ?? country.value
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
|
||||||
|
return provideUserCountry(country)
|
||||||
|
}
|
||||||
@@ -15,9 +15,12 @@ import { I18nDebugPanel, LoadingBar, NotificationPanel } from '@modrinth/ui'
|
|||||||
|
|
||||||
import AdsConsentNotification from '~/components/ui/AdsConsentNotification.vue'
|
import AdsConsentNotification from '~/components/ui/AdsConsentNotification.vue'
|
||||||
import { setupProviders } from '~/providers/setup.ts'
|
import { setupProviders } from '~/providers/setup.ts'
|
||||||
|
import { setupUserCountryProvider } from '~/providers/setup/user-country.ts'
|
||||||
|
|
||||||
import { useAuth } from './composables/auth'
|
import { useAuth } from './composables/auth'
|
||||||
|
|
||||||
|
setupUserCountryProvider()
|
||||||
|
|
||||||
const auth = await useAuth()
|
const auth = await useAuth()
|
||||||
setupProviders(auth)
|
setupProviders(auth)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ISO3166 } from '@modrinth/api-client'
|
import type { ISO3166 } from '@modrinth/api-client'
|
||||||
|
import { useUserCountry as useInjectedUserCountry } from '@modrinth/ui'
|
||||||
|
|
||||||
import { useRequestHeaders, useState } from '#imports'
|
|
||||||
import { countries, subdivisions } from '~/generated/state.json'
|
import { countries, subdivisions } from '~/generated/state.json'
|
||||||
|
|
||||||
export const useCountries = () => {
|
export const useCountries = () => {
|
||||||
@@ -35,38 +35,4 @@ export const useSubdivisions = (countryCode: ComputedRef<string> | Ref<string> |
|
|||||||
return computed(() => byCountry[unref(code)] ?? [])
|
return computed(() => byCountry[unref(code)] ?? [])
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUserCountry = () => {
|
export const useUserCountry = useInjectedUserCountry
|
||||||
const country = useState<string>('userCountry', () => 'US')
|
|
||||||
const fromServer = useState<boolean>('userCountryFromServer', () => false)
|
|
||||||
|
|
||||||
if (import.meta.server) {
|
|
||||||
const headers = useRequestHeaders(['cf-ipcountry', 'accept-language'])
|
|
||||||
const cf = headers['cf-ipcountry']
|
|
||||||
if (cf) {
|
|
||||||
country.value = cf.toUpperCase()
|
|
||||||
fromServer.value = true
|
|
||||||
} else {
|
|
||||||
const al = headers['accept-language'] || ''
|
|
||||||
const tag = al.split(',')[0]
|
|
||||||
const val = tag.split('-')[1]?.toLowerCase()
|
|
||||||
if (val) {
|
|
||||||
country.value = val
|
|
||||||
fromServer.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (import.meta.client) {
|
|
||||||
onMounted(() => {
|
|
||||||
if (fromServer.value) return
|
|
||||||
// @ts-expect-error - ignore TS not knowing about navigator.userLanguage
|
|
||||||
const lang = navigator.language || navigator.userLanguage || ''
|
|
||||||
const region = lang.split('-')[1]
|
|
||||||
if (region) {
|
|
||||||
country.value = region.toUpperCase()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return country
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { provideUserCountry } from '@modrinth/ui'
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
|
import { useRequestHeaders, useState } from '#imports'
|
||||||
|
|
||||||
|
function getCountryFromLocale(locale: string | undefined) {
|
||||||
|
if (!locale) return undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new Intl.Locale(locale).region?.toUpperCase()
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupUserCountryProvider() {
|
||||||
|
const country = useState<string>('userCountry', () => 'US')
|
||||||
|
const fromServer = useState<boolean>('userCountryFromServer', () => false)
|
||||||
|
|
||||||
|
if (import.meta.server) {
|
||||||
|
const headers = useRequestHeaders(['cf-ipcountry', 'accept-language'])
|
||||||
|
const cloudflareCountry = headers['cf-ipcountry']
|
||||||
|
const locale = headers['accept-language']?.split(',')[0]?.split(';')[0]?.trim()
|
||||||
|
const detectedCountry = cloudflareCountry?.toUpperCase() ?? getCountryFromLocale(locale)
|
||||||
|
|
||||||
|
if (detectedCountry) {
|
||||||
|
country.value = detectedCountry
|
||||||
|
fromServer.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.client) {
|
||||||
|
onMounted(() => {
|
||||||
|
if (!fromServer.value) {
|
||||||
|
country.value = getCountryFromLocale(navigator.language) ?? country.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return provideUserCountry(country)
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ import { LabrinthCollectionsModule } from './labrinth/collections'
|
|||||||
import { LabrinthContentV3Module } from './labrinth/content/v3'
|
import { LabrinthContentV3Module } from './labrinth/content/v3'
|
||||||
import { LabrinthExternalProjectsInternalModule } from './labrinth/external-projects/internal'
|
import { LabrinthExternalProjectsInternalModule } from './labrinth/external-projects/internal'
|
||||||
import { LabrinthFriendsV3Module } from './labrinth/friends/v3'
|
import { LabrinthFriendsV3Module } from './labrinth/friends/v3'
|
||||||
|
import { LabrinthGeoIpModule } from './labrinth/geoip'
|
||||||
import { LabrinthGlobalsInternalModule } from './labrinth/globals/internal'
|
import { LabrinthGlobalsInternalModule } from './labrinth/globals/internal'
|
||||||
import { LabrinthImagesV3Module } from './labrinth/images/v3'
|
import { LabrinthImagesV3Module } from './labrinth/images/v3'
|
||||||
import { LabrinthLimitsV3Module } from './labrinth/limits/v3'
|
import { LabrinthLimitsV3Module } from './labrinth/limits/v3'
|
||||||
@@ -111,6 +112,7 @@ export const MODULE_REGISTRY = {
|
|||||||
labrinth_content_v3: LabrinthContentV3Module,
|
labrinth_content_v3: LabrinthContentV3Module,
|
||||||
labrinth_external_projects_internal: LabrinthExternalProjectsInternalModule,
|
labrinth_external_projects_internal: LabrinthExternalProjectsInternalModule,
|
||||||
labrinth_friends_v3: LabrinthFriendsV3Module,
|
labrinth_friends_v3: LabrinthFriendsV3Module,
|
||||||
|
labrinth_geoip: LabrinthGeoIpModule,
|
||||||
labrinth_globals_internal: LabrinthGlobalsInternalModule,
|
labrinth_globals_internal: LabrinthGlobalsInternalModule,
|
||||||
labrinth_images_v3: LabrinthImagesV3Module,
|
labrinth_images_v3: LabrinthImagesV3Module,
|
||||||
labrinth_moderation_internal: LabrinthModerationInternalModule,
|
labrinth_moderation_internal: LabrinthModerationInternalModule,
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { AbstractModule } from '../../core/abstract-module'
|
||||||
|
|
||||||
|
export class LabrinthGeoIpModule extends AbstractModule {
|
||||||
|
public getModuleID(): string {
|
||||||
|
return 'labrinth_geoip'
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getCountry(): Promise<string | undefined> {
|
||||||
|
const trace = await this.client.request<string>('/trace', {
|
||||||
|
api: 'https://api.modrinth.com',
|
||||||
|
version: 'cdn-cgi',
|
||||||
|
method: 'GET',
|
||||||
|
skipAuth: true,
|
||||||
|
})
|
||||||
|
const country = trace
|
||||||
|
.split('\n')
|
||||||
|
.find((line) => line.startsWith('loc='))
|
||||||
|
?.slice(4)
|
||||||
|
.toUpperCase()
|
||||||
|
|
||||||
|
return country?.match(/^[A-Z]{2}$/) ? country : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ export * from './collections'
|
|||||||
export * from './content/v3'
|
export * from './content/v3'
|
||||||
export * from './external-projects/internal'
|
export * from './external-projects/internal'
|
||||||
export * from './friends/v3'
|
export * from './friends/v3'
|
||||||
|
export * from './geoip'
|
||||||
export * from './globals/internal'
|
export * from './globals/internal'
|
||||||
export * from './images/v3'
|
export * from './images/v3'
|
||||||
export * from './limits/v3'
|
export * from './limits/v3'
|
||||||
|
|||||||
@@ -18,4 +18,5 @@ export * from './project-page-new'
|
|||||||
export * from './server-context'
|
export * from './server-context'
|
||||||
export * from './server-settings-modal'
|
export * from './server-settings-modal'
|
||||||
export * from './tags'
|
export * from './tags'
|
||||||
|
export * from './user-country'
|
||||||
export * from './web-notifications'
|
export * from './web-notifications'
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { setMarkdownUserCountryResolver } from '@modrinth/utils'
|
||||||
|
import type { Ref } from 'vue'
|
||||||
|
import { hasInjectionContext } from 'vue'
|
||||||
|
|
||||||
|
import { createContext } from './create-context'
|
||||||
|
|
||||||
|
const [injectUserCountryContext, provideUserCountryContext] = createContext<Ref<string>>(
|
||||||
|
'root',
|
||||||
|
'userCountry',
|
||||||
|
)
|
||||||
|
|
||||||
|
let clientCountry: Ref<string> | null = null
|
||||||
|
|
||||||
|
export const injectUserCountry = injectUserCountryContext
|
||||||
|
export const useUserCountry = injectUserCountryContext
|
||||||
|
|
||||||
|
export function provideUserCountry(country: Ref<string>) {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
clientCountry = country
|
||||||
|
}
|
||||||
|
|
||||||
|
return provideUserCountryContext(country)
|
||||||
|
}
|
||||||
|
|
||||||
|
setMarkdownUserCountryResolver(() => {
|
||||||
|
const injectedCountry = hasInjectionContext() ? injectUserCountryContext(null) : null
|
||||||
|
return injectedCountry?.value ?? clientCountry?.value
|
||||||
|
})
|
||||||
@@ -4,6 +4,39 @@ import xss from 'xss'
|
|||||||
// @ts-expect-error xss types don't reflect CJS default export shape
|
// @ts-expect-error xss types don't reflect CJS default export shape
|
||||||
const { escapeAttrValue, FilterXSS, safeAttrValue, whiteList } = xss
|
const { escapeAttrValue, FilterXSS, safeAttrValue, whiteList } = xss
|
||||||
|
|
||||||
|
// 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.replaceAll('&', '&'))
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const configuredXss = new FilterXSS({
|
export const configuredXss = new FilterXSS({
|
||||||
whiteList: {
|
whiteList: {
|
||||||
...whiteList,
|
...whiteList,
|
||||||
@@ -84,6 +117,18 @@ export const configuredXss = new FilterXSS({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
safeAttrValue(tag, name, value, cssFilter) {
|
safeAttrValue(tag, name, value, cssFilter) {
|
||||||
|
if (
|
||||||
|
shouldProxyImgur() &&
|
||||||
|
((tag === 'a' && name === 'href') ||
|
||||||
|
((tag === 'img' || tag === 'video' || tag === 'audio' || tag === 'source') &&
|
||||||
|
(name === 'src' || name === 'srcset' || name === 'poster')))
|
||||||
|
) {
|
||||||
|
const proxiedUrl = getImgurProxyUrl(value)
|
||||||
|
if (proxiedUrl) {
|
||||||
|
return safeAttrValue(tag, name, proxiedUrl, cssFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(tag === 'img' || tag === 'video' || tag === 'audio' || tag === 'source') &&
|
(tag === 'img' || tag === 'video' || tag === 'audio' || tag === 'source') &&
|
||||||
(name === 'src' || name === 'srcset' || name === 'poster') &&
|
(name === 'src' || name === 'srcset' || name === 'poster') &&
|
||||||
|
|||||||
Reference in New Issue
Block a user