diff --git a/app.vue b/app.vue
index 81d6056d7..3c02bdebc 100644
--- a/app.vue
+++ b/app.vue
@@ -40,6 +40,7 @@ else {
app.vueApp.use(datagouv, {
name: runtimeConfig.public.title,
baseUrl: runtimeConfig.public.baseUrl,
+ trustedDomains: runtimeConfig.public.trustedDomains,
apiBase: runtimeConfig.public.apiBase,
devApiKey: runtimeConfig.public.devApiKey,
datasetQualityGuideUrl: runtimeConfig.public.datasetQualityGuideUrl,
diff --git a/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue b/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue
index 58308120a..a2b22a4d4 100644
--- a/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue
+++ b/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue
@@ -28,13 +28,21 @@
: t("L'aperçu n'est pas disponible car la taille du fichier est inconnue. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
}}
+
+
+ {{ t("Ce fichier JSON ne peut pas être prévisualisé car il est hébergé sur un site distant qui restreint l'accès (CORS).") }}
+
- {{ t("Ce fichier JSON ne peut pas être prévisualisé, peut-être parce qu'il est hébergé sur un autre site qui ne l'autorise pas. Pour le consulter, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.") }}
+ {{ t("Impossible de charger l'aperçu. Vérifiez votre connexion ou l'accessibilité du fichier.") }}
import('vue3-json-viewer').then((module) => {
@@ -79,36 +87,37 @@ const fileTooLarge = ref(false)
const fileSizeBytes = computed(() => getResourceFilesize(props.resource))
-const shouldLoadJson = computed(() => {
- const size = fileSizeBytes.value
- if (!size) {
- // If we don't know the size, don't risk loading a potentially huge file
- return false
- }
-
- // Check if maxJsonPreviewCharSize is configured
- if (!config.maxJsonPreviewCharSize) {
- // If no limit is set, don't load unknown files
- return false
- }
+const corsStatus = computed(() => getResourceCorsStatus(props.resource))
+const isSizeAllowed = computed(() => {
+ const size = fileSizeBytes.value
// Convert maxJsonPreviewCharSize from characters to bytes (rough estimate)
// Assuming average 1 byte per character for JSON
const maxByteSize = config.maxJsonPreviewCharSize
+ // If we don't know the size or the max size, don't risk loading a potentially huge file
+ if (!size || !maxByteSize) return false
+
return size <= maxByteSize
})
const fetchJsonData = async () => {
- // Check if file is too large or size is unknown before making the request
- if (!shouldLoadJson.value) {
+ error.value = null
+ fileTooLarge.value = false
+
+ // Check if file is too large or size is unknown
+ if (!isSizeAllowed.value) {
fileTooLarge.value = true
return
}
- loading.value = true
- error.value = null
+ // Check if CORS is allowed
+ if (corsStatus.value === 'blocked') {
+ error.value = 'cors'
+ return
+ }
+ loading.value = true
try {
const response = await fetch(props.resource.url)
// const response = await fetch('/test-data.json') // For testing locally without CORS issues
diff --git a/datagouv-components/src/components/ResourceAccordion/Metadata.vue b/datagouv-components/src/components/ResourceAccordion/Metadata.vue
index d3667561e..2ed2e0417 100644
--- a/datagouv-components/src/components/ResourceAccordion/Metadata.vue
+++ b/datagouv-components/src/components/ResourceAccordion/Metadata.vue
@@ -7,9 +7,8 @@ import DescriptionTerm from '../DescriptionTerm.vue'
import { useFormatDate } from '../../functions/dates'
import { filesize } from '../../functions/helpers'
import ExtraAccordion from '../ExtraAccordion.vue'
-import { getResourceTitleId, getResourceLabel } from '../../functions/resources'
+import { getResourceTitleId, getResourceLabel, getResourceFilesize } from '../../functions/resources'
import { useTranslation } from '../../composables/useTranslation'
-import { getResourceFilesize } from '../../functions/datasets'
const props = defineProps<{
resource: Resource
diff --git a/datagouv-components/src/components/ResourceAccordion/PdfPreview.client.vue b/datagouv-components/src/components/ResourceAccordion/PdfPreview.client.vue
index 0b9cab3aa..adcd7106d 100644
--- a/datagouv-components/src/components/ResourceAccordion/PdfPreview.client.vue
+++ b/datagouv-components/src/components/ResourceAccordion/PdfPreview.client.vue
@@ -44,13 +44,21 @@
: t("L'aperçu n'est pas disponible car la taille du fichier est inconnue. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
}}
+
+
+ {{ t("Ce fichier PDF ne peut pas être prévisualisé car il est hébergé sur un site distant qui restreint l'accès (CORS).") }}
+
-
- {{ t("Ce fichier PDF ne peut pas être prévisualisé, peut-être parce qu'il est hébergé sur un autre site qui ne l'autorise pas. Pour le consulter, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.") }}
+
+ {{ t("Impossible de charger l'aperçu. Vérifiez votre connexion ou l'accessibilité du fichier.") }}
import('pdf-vue3').then((module) => {
@@ -92,28 +100,36 @@ const fileTooLarge = ref(false)
const fileSizeBytes = computed(() => getResourceFilesize(props.resource))
-const shouldLoadPdf = computed(() => {
- const size = fileSizeBytes.value
- if (!size) {
- // If we don't know the size, don't risk loading a potentially huge file
- return false
- }
+const corsStatus = computed(() => getResourceCorsStatus(props.resource))
+const isSizeAllowed = computed(() => {
+ const size = fileSizeBytes.value
// Use maxPdfPreviewByteSize from config, fallback to 10 MB if not set
const maxByteSize = config.maxPdfPreviewByteSize ?? 10_000_000
+
+ // If we don't know the size, don't risk loading a potentially huge file
+ if (!size) return false
+
return size <= maxByteSize
})
const loadPdf = async () => {
- // Check if file is too large or size is unknown before loading
- if (!shouldLoadPdf.value) {
+ error.value = null
+ fileTooLarge.value = false
+
+ // Check if file is too large or size is unknown
+ if (!isSizeAllowed.value) {
fileTooLarge.value = true
return
}
- loading.value = true
- error.value = null
+ // Check if CORS is allowed
+ if (corsStatus.value === 'blocked') {
+ error.value = 'cors'
+ return
+ }
+ loading.value = true
try {
// Test if the PDF URL is accessible
const response = await fetch(props.resource.url, { method: 'HEAD' })
diff --git a/datagouv-components/src/components/ResourceAccordion/ResourceAccordion.vue b/datagouv-components/src/components/ResourceAccordion/ResourceAccordion.vue
index 20f11b71b..043df4496 100644
--- a/datagouv-components/src/components/ResourceAccordion/ResourceAccordion.vue
+++ b/datagouv-components/src/components/ResourceAccordion/ResourceAccordion.vue
@@ -386,9 +386,8 @@ import { trackEvent } from '../../functions/matomo'
import CopyButton from '../CopyButton.vue'
import { useComponentsConfig } from '../../config'
import { getOwnerName } from '../../functions/owned'
-import { getResourceFormatIcon, getResourceTitleId, detectOgcService } from '../../functions/resources'
+import { getResourceFormatIcon, getResourceTitleId, detectOgcService, getResourceExternalUrl, getResourceFilesize } from '../../functions/resources'
import BrandedButton from '../BrandedButton.vue'
-import { getResourceExternalUrl, getResourceFilesize } from '../../functions/datasets'
import { useTranslation } from '../../composables/useTranslation'
import { useHasTabularData } from '../../composables/useHasTabularData'
import Metadata from './Metadata.vue'
diff --git a/datagouv-components/src/components/ResourceAccordion/XmlPreview.client.vue b/datagouv-components/src/components/ResourceAccordion/XmlPreview.client.vue
index 5c011e2bb..de6b8f2ba 100644
--- a/datagouv-components/src/components/ResourceAccordion/XmlPreview.client.vue
+++ b/datagouv-components/src/components/ResourceAccordion/XmlPreview.client.vue
@@ -20,13 +20,21 @@
: t("L'aperçu n'est pas disponible car la taille du fichier est inconnue. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
}}
+
+
+ {{ t("Ce fichier XML ne peut pas être prévisualisé car il est hébergé sur un site distant qui restreint l'accès (CORS).") }}
+
- {{ t("Ce fichier XML ne peut pas être prévisualisé, peut-être parce qu'il est hébergé sur un autre site qui ne l'autorise pas. Pour le consulter, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.") }}
+ {{ t("Impossible de charger l'aperçu. Vérifiez votre connexion ou l'accessibilité du fichier.") }}
import('vue3-xml-viewer').then((module) => {
@@ -70,36 +78,37 @@ const fileTooLarge = ref(false)
const fileSizeBytes = computed(() => getResourceFilesize(props.resource))
-const shouldLoadXml = computed(() => {
- const size = fileSizeBytes.value
- if (!size) {
- // If we don't know the size, don't risk loading a potentially huge file
- return false
- }
-
- // Check if maxXmlPreviewCharSize is configured
- if (!config.maxXmlPreviewCharSize) {
- // If no limit is set, don't load unknown files
- return false
- }
+const corsStatus = computed(() => getResourceCorsStatus(props.resource))
+const isSizeAllowed = computed(() => {
+ const size = fileSizeBytes.value
// Convert maxXmlPreviewCharSize from characters to bytes (rough estimate)
// Assuming average 1 byte per character for XML
const maxByteSize = config.maxXmlPreviewCharSize
+ // If we don't know the size or the max size, don't risk loading a potentially huge file
+ if (!size || !maxByteSize) return false
+
return size <= maxByteSize
})
const fetchXmlData = async () => {
- // Check if file is too large or size is unknown before making the request
- if (!shouldLoadXml.value) {
+ error.value = null
+ fileTooLarge.value = false
+
+ // Check if file is too large or size is unknown
+ if (!isSizeAllowed.value) {
fileTooLarge.value = true
return
}
- loading.value = true
- error.value = null
+ // Check if CORS is allowed
+ if (corsStatus.value === 'blocked') {
+ error.value = 'cors'
+ return
+ }
+ loading.value = true
try {
const response = await fetch(props.resource.url)
// const response = await fetch('/test-data.xml') // For testing locally without CORS issues
diff --git a/datagouv-components/src/components/ResourceExplorer/ResourceExplorerViewer.vue b/datagouv-components/src/components/ResourceExplorer/ResourceExplorerViewer.vue
index 4b5f6ac69..9de8eaad0 100644
--- a/datagouv-components/src/components/ResourceExplorer/ResourceExplorerViewer.vue
+++ b/datagouv-components/src/components/ResourceExplorer/ResourceExplorerViewer.vue
@@ -267,8 +267,7 @@ import Preview from '../ResourceAccordion/Preview.vue'
import DataStructure from '../ResourceAccordion/DataStructure.vue'
import Metadata from '../ResourceAccordion/Metadata.vue'
import { filesize, summarize } from '../../functions/helpers'
-import { getResourceFormatIcon } from '../../functions/resources'
-import { getResourceExternalUrl, getResourceFilesize } from '../../functions/datasets'
+import { getResourceFormatIcon, getResourceExternalUrl, getResourceFilesize } from '../../functions/resources'
import { trackEvent } from '../../functions/matomo'
import { useComponentsConfig } from '../../config'
import { useFormatDate } from '../../functions/dates'
diff --git a/datagouv-components/src/functions/datasets.ts b/datagouv-components/src/functions/datasets.ts
index ebba78794..2592406ad 100644
--- a/datagouv-components/src/functions/datasets.ts
+++ b/datagouv-components/src/functions/datasets.ts
@@ -1,6 +1,4 @@
import { useComponentsConfig } from '../config'
-import type { Dataset, DatasetV2 } from '../types/datasets'
-import type { CommunityResource, Resource } from '../types/resources'
function constructUrl(baseUrl: string, path: string): string {
const url = new URL(baseUrl)
@@ -14,18 +12,3 @@ export function getDatasetOEmbedHtml(type: string, id: string): string {
const staticUrl = constructUrl(config.baseUrl, 'oembed.js')
return ``
}
-
-export function isCommunityResource(resource: Resource | CommunityResource): boolean {
- return 'organization' in resource || 'owner' in resource
-}
-
-export function getResourceExternalUrl(dataset: Dataset | DatasetV2 | Omit, resource: Resource | CommunityResource): string {
- return `${dataset.page}${isCommunityResource(resource) ? '/community-resources' : ''}?resource_id=${resource.id}`
-}
-
-export function getResourceFilesize(resource: Resource): null | number {
- if (resource.filesize) return resource.filesize
- if ('analysis:content-length' in resource.extras) return resource.extras['analysis:content-length'] as number
-
- return null
-}
diff --git a/datagouv-components/src/functions/resources.ts b/datagouv-components/src/functions/resources.ts
index 27b11ac87..e028c96ca 100644
--- a/datagouv-components/src/functions/resources.ts
+++ b/datagouv-components/src/functions/resources.ts
@@ -1,15 +1,19 @@
import { readonly, type Component } from 'vue'
import { RiEarthLine, RiMap2Line } from '@remixicon/vue'
+import { useComponentsConfig } from '../config'
import Archive from '../components/Icons/Archive.vue'
import Code from '../components/Icons/Code.vue'
+import type { Dataset, DatasetV2 } from '../types/datasets'
import Documentation from '../components/Icons/Documentation.vue'
import Image from '../components/Icons/Image.vue'
import Link from '../components/Icons/Link.vue'
import Table from '../components/Icons/Table.vue'
-import type { Resource } from '../types/resources'
+import type { CommunityResource, Resource } from '../types/resources'
import { useTranslation } from '../composables/useTranslation'
+const config = useComponentsConfig()
+
export function getResourceFormatIcon(format: string): Component | null {
switch (format?.trim()?.toLowerCase()) {
case 'txt':
@@ -129,3 +133,47 @@ export const detectOgcService = (resource: Resource) => {
}
return false
}
+
+export function isCommunityResource(resource: Resource | CommunityResource): boolean {
+ return 'organization' in resource || 'owner' in resource
+}
+
+export function getResourceExternalUrl(dataset: Dataset | DatasetV2 | Omit, resource: Resource | CommunityResource): string {
+ return `${dataset.page}${isCommunityResource(resource) ? '/community-resources' : ''}?resource_id=${resource.id}`
+}
+
+export function getResourceFilesize(resource: Resource): null | number {
+ if (resource.filesize) return resource.filesize
+ if ('analysis:content-length' in resource.extras) return resource.extras['analysis:content-length'] as number
+
+ return null
+}
+
+type CorsStatus = 'allowed' | 'blocked' | 'unknown'
+
+export const getResourceCorsStatus = (resource: Resource): CorsStatus => {
+ const extras = resource.extras
+ if (!extras || !('check:cors:allow-origin' in extras)) {
+ return 'unknown'
+ }
+
+ const allowOrigin = extras['check:cors:allow-origin'] as string | undefined
+ const rawMethods = extras['check:cors:allow-methods'] as string | undefined
+
+ // Check if allow-origin is '*' or contains one of our trusted domains
+ const trustedDomains = config.trustedDomains
+ const hasPublicCors = allowOrigin === '*'
+ const hasSpecificCors = allowOrigin
+ ? trustedDomains.some(domain => allowOrigin.includes(domain))
+ : false
+
+ const isOriginAllowed = hasPublicCors || hasSpecificCors
+
+ // Ensure GET method is allowed
+ const allowedMethods = rawMethods
+ ? rawMethods.split(',').map(m => m.trim().toUpperCase())
+ : []
+ const supportsGet = allowedMethods.length === 0 || allowedMethods.includes('GET')
+
+ return isOriginAllowed && supportsGet ? 'allowed' : 'blocked'
+}
diff --git a/nuxt.config.ts b/nuxt.config.ts
index eda88a9c4..8c8d72cac 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -52,6 +52,7 @@ export default defineNuxtConfig({
albertApiKey: '',
public: {
baseUrl: 'https://www.data.gouv.fr/',
+ trustedDomains: ['data.gouv.fr', 'www.data.gouv.fr'],
banner: undefined,
title: 'data.gouv.fr',