From 8ef1b5659ab87f35abda4ca64583edbae6640cb3 Mon Sep 17 00:00:00 2001 From: Adrien Carpentier Date: Mon, 23 Feb 2026 17:26:43 +0100 Subject: [PATCH 01/10] feat: use resource extra info about its CORS headers before trying to display preview or not --- .../ResourceAccordion/JsonPreview.client.vue | 47 +++++++++++-------- .../ResourceAccordion/PdfPreview.client.vue | 42 ++++++++++++----- .../ResourceAccordion/XmlPreview.client.vue | 46 +++++++++++------- datagouv-components/src/functions/datasets.ts | 32 +++++++++++++ 4 files changed, 117 insertions(+), 50 deletions(-) diff --git a/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue b/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue index 90866c4fd..b8278396a 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 isCorsAllowed = computed(() => isResourceCorsEnabled(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 + + // If CORS is blocked, don't even try + if (!isCorsAllowed.value) { + error.value = 'cors' + return + } + + // Check if file is too large or size is unknown + if (!isSizeAllowed.value) { fileTooLarge.value = true return } loading.value = true - error.value = null - 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/PdfPreview.client.vue b/datagouv-components/src/components/ResourceAccordion/PdfPreview.client.vue index 0b9cab3aa..71176367d 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 isCorsAllowed = computed(() => isResourceCorsEnabled(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 + + // If CORS is blocked, don't even try + if (!isCorsAllowed.value) { + error.value = 'cors' + return + } + + // Check if file is too large or size is unknown + if (!isSizeAllowed.value) { fileTooLarge.value = true return } loading.value = true - error.value = null - 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/XmlPreview.client.vue b/datagouv-components/src/components/ResourceAccordion/XmlPreview.client.vue index 7a20be352..9725c137f 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.") }} 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 isCorsAllowed = computed(() => isResourceCorsEnabled(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 + + // If CORS is blocked, don't even try + if (!isCorsAllowed.value) { + error.value = 'cors' + return + } + + // Check if file is too large or size is unknown + if (!isSizeAllowed.value) { fileTooLarge.value = true return } loading.value = true - error.value = null - 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/functions/datasets.ts b/datagouv-components/src/functions/datasets.ts index ebba78794..2f3b0e73d 100644 --- a/datagouv-components/src/functions/datasets.ts +++ b/datagouv-components/src/functions/datasets.ts @@ -29,3 +29,35 @@ export function getResourceFilesize(resource: Resource): null | number { return null } + +export const isResourceCorsEnabled = (resource: Resource): boolean => { + const extras = resource.extras + if (!extras) return false + + const status = extras['check:status'] as number | undefined + const allowOrigin = extras['check:cors:allow-origin'] as string | undefined + const rawMethods = extras['check:cors:allow-methods'] as string | undefined + + // Verify the last cors probe was successful (HTTP 200) + const isHealthy = status === 200 + if (!isHealthy) return false + + // Validate Origin (Wildcard OR specific domain) + const trustedDomains = ['data.gouv.fr', 'www.data.gouv.fr'] + + // Check if allow-origin is '*' or contains one of our trusted domains + 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 +} From b956ca8cb019ad891d6958645672cb86c258b0c5 Mon Sep 17 00:00:00 2001 From: Adrien Carpentier Date: Mon, 23 Feb 2026 17:59:46 +0100 Subject: [PATCH 02/10] refactor: move getResourceFilesize and isResourceCorsEnabled to functions/resources.ts --- .../ResourceAccordion/JsonPreview.client.vue | 2 +- .../ResourceAccordion/PdfPreview.client.vue | 2 +- .../ResourceAccordion/XmlPreview.client.vue | 3 +- datagouv-components/src/functions/datasets.ts | 39 ------------------- .../src/functions/resources.ts | 39 +++++++++++++++++++ 5 files changed, 42 insertions(+), 43 deletions(-) diff --git a/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue b/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue index b8278396a..73aa5ea56 100644 --- a/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue +++ b/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue @@ -62,7 +62,7 @@ import { RiErrorWarningLine } from '@remixicon/vue' import { useComponentsConfig } from '../../config' import SimpleBanner from '../SimpleBanner.vue' import type { Resource } from '../../types/resources' -import { isResourceCorsEnabled, getResourceFilesize } from '../../functions/datasets' +import { getResourceFilesize, isResourceCorsEnabled } from '../../functions/resources' import { useTranslation } from '../../composables/useTranslation' const JsonViewer = defineAsyncComponent(() => diff --git a/datagouv-components/src/components/ResourceAccordion/PdfPreview.client.vue b/datagouv-components/src/components/ResourceAccordion/PdfPreview.client.vue index 71176367d..449f6cd59 100644 --- a/datagouv-components/src/components/ResourceAccordion/PdfPreview.client.vue +++ b/datagouv-components/src/components/ResourceAccordion/PdfPreview.client.vue @@ -77,7 +77,7 @@ import { RiErrorWarningLine } from '@remixicon/vue' import SimpleBanner from '../SimpleBanner.vue' import { useComponentsConfig } from '../../config' import type { Resource } from '../../types/resources' -import { isResourceCorsEnabled, getResourceFilesize } from '../../functions/datasets' +import { getResourceFilesize, isResourceCorsEnabled } from '../../functions/resources' import { useTranslation } from '../../composables/useTranslation' const PDF = defineAsyncComponent(() => diff --git a/datagouv-components/src/components/ResourceAccordion/XmlPreview.client.vue b/datagouv-components/src/components/ResourceAccordion/XmlPreview.client.vue index 9725c137f..a27e1712e 100644 --- a/datagouv-components/src/components/ResourceAccordion/XmlPreview.client.vue +++ b/datagouv-components/src/components/ResourceAccordion/XmlPreview.client.vue @@ -54,10 +54,9 @@ import { RiErrorWarningLine } from '@remixicon/vue' import { useComponentsConfig } from '../../config' import SimpleBanner from '../SimpleBanner.vue' import type { Resource } from '../../types/resources' -import { isResourceCorsEnabled } from '../../functions/datasets' +import { getResourceFilesize, isResourceCorsEnabled } from '../../functions/resources' import { useTranslation } from '../../composables/useTranslation' import '../../types/vue3-xml-viewer.d' -import { getResourceFilesize } from '../../main' const XmlViewer = defineAsyncComponent(() => import('vue3-xml-viewer').then((module) => { diff --git a/datagouv-components/src/functions/datasets.ts b/datagouv-components/src/functions/datasets.ts index 2f3b0e73d..2e38a6d45 100644 --- a/datagouv-components/src/functions/datasets.ts +++ b/datagouv-components/src/functions/datasets.ts @@ -22,42 +22,3 @@ export function isCommunityResource(resource: Resource | CommunityResource): boo 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 -} - -export const isResourceCorsEnabled = (resource: Resource): boolean => { - const extras = resource.extras - if (!extras) return false - - const status = extras['check:status'] as number | undefined - const allowOrigin = extras['check:cors:allow-origin'] as string | undefined - const rawMethods = extras['check:cors:allow-methods'] as string | undefined - - // Verify the last cors probe was successful (HTTP 200) - const isHealthy = status === 200 - if (!isHealthy) return false - - // Validate Origin (Wildcard OR specific domain) - const trustedDomains = ['data.gouv.fr', 'www.data.gouv.fr'] - - // Check if allow-origin is '*' or contains one of our trusted domains - 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 -} diff --git a/datagouv-components/src/functions/resources.ts b/datagouv-components/src/functions/resources.ts index 27b11ac87..2a3ceef80 100644 --- a/datagouv-components/src/functions/resources.ts +++ b/datagouv-components/src/functions/resources.ts @@ -129,3 +129,42 @@ export const detectOgcService = (resource: Resource) => { } return false } + +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 +} + +export const isResourceCorsEnabled = (resource: Resource): boolean => { + const extras = resource.extras + if (!extras) return false + + const status = extras['check:status'] as number | undefined + const allowOrigin = extras['check:cors:allow-origin'] as string | undefined + const rawMethods = extras['check:cors:allow-methods'] as string | undefined + + // Verify the last cors probe was successful (HTTP 200) + const isHealthy = status === 200 + if (!isHealthy) return false + + // Validate Origin (Wildcard OR specific domain) + const trustedDomains = ['data.gouv.fr', 'www.data.gouv.fr'] + + // Check if allow-origin is '*' or contains one of our trusted domains + 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 +} From 366a716449a5c7ea8aeb0b7e815be21c6d6b7d74 Mon Sep 17 00:00:00 2001 From: Adrien Carpentier Date: Mon, 23 Feb 2026 18:08:22 +0100 Subject: [PATCH 03/10] fix: remove double comment and add a TODO --- datagouv-components/src/functions/resources.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/datagouv-components/src/functions/resources.ts b/datagouv-components/src/functions/resources.ts index 2a3ceef80..342175311 100644 --- a/datagouv-components/src/functions/resources.ts +++ b/datagouv-components/src/functions/resources.ts @@ -149,10 +149,8 @@ export const isResourceCorsEnabled = (resource: Resource): boolean => { const isHealthy = status === 200 if (!isHealthy) return false - // Validate Origin (Wildcard OR specific domain) - const trustedDomains = ['data.gouv.fr', 'www.data.gouv.fr'] - // Check if allow-origin is '*' or contains one of our trusted domains + const trustedDomains = ['data.gouv.fr', 'www.data.gouv.fr'] // TODO: get from config const hasPublicCors = allowOrigin === '*' const hasSpecificCors = allowOrigin ? trustedDomains.some(domain => allowOrigin.includes(domain)) From 47ce98524dd6aadd9e7290b333aec751b00bc371 Mon Sep 17 00:00:00 2001 From: Adrien Carpentier Date: Tue, 24 Feb 2026 12:17:31 +0100 Subject: [PATCH 04/10] refactor: move resources functions to resources.ts instead of dataset.ts --- .../src/components/ResourceAccordion/Metadata.vue | 3 +-- .../ResourceAccordion/ResourceAccordion.vue | 3 +-- .../ResourceExplorer/ResourceExplorerViewer.vue | 3 +-- datagouv-components/src/functions/datasets.ts | 10 ---------- datagouv-components/src/functions/resources.ts | 11 ++++++++++- 5 files changed, 13 insertions(+), 17 deletions(-) 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/ResourceAccordion.vue b/datagouv-components/src/components/ResourceAccordion/ResourceAccordion.vue index b22148f7d..a9e4169e1 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 Metadata from './Metadata.vue' import SchemaBadge from './SchemaBadge.vue' 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 2e38a6d45..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,11 +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}` -} diff --git a/datagouv-components/src/functions/resources.ts b/datagouv-components/src/functions/resources.ts index 342175311..a307f54fe 100644 --- a/datagouv-components/src/functions/resources.ts +++ b/datagouv-components/src/functions/resources.ts @@ -3,11 +3,12 @@ import { readonly, type Component } from 'vue' import { RiEarthLine, RiMap2Line } from '@remixicon/vue' 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' export function getResourceFormatIcon(format: string): Component | null { @@ -130,6 +131,14 @@ 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 From 7b193006e0c1a6711871af3e3110646aecd0a760 Mon Sep 17 00:00:00 2001 From: Adrien Carpentier Date: Tue, 24 Feb 2026 12:31:32 +0100 Subject: [PATCH 05/10] docs: fix comment --- datagouv-components/src/functions/resources.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datagouv-components/src/functions/resources.ts b/datagouv-components/src/functions/resources.ts index a307f54fe..f134fef2d 100644 --- a/datagouv-components/src/functions/resources.ts +++ b/datagouv-components/src/functions/resources.ts @@ -154,7 +154,7 @@ export const isResourceCorsEnabled = (resource: Resource): boolean => { const allowOrigin = extras['check:cors:allow-origin'] as string | undefined const rawMethods = extras['check:cors:allow-methods'] as string | undefined - // Verify the last cors probe was successful (HTTP 200) + // Verify the last check was successful (HTTP 200) const isHealthy = status === 200 if (!isHealthy) return false From 21169360fa0722180dfe7de9a8964e044acda821 Mon Sep 17 00:00:00 2001 From: Adrien Carpentier Date: Tue, 24 Feb 2026 14:58:11 +0100 Subject: [PATCH 06/10] fix: when testing preview, first check the file size than the CORS, so that the first error message is more clear for the user --- .../ResourceAccordion/JsonPreview.client.vue | 12 ++++++------ .../ResourceAccordion/PdfPreview.client.vue | 12 ++++++------ .../ResourceAccordion/XmlPreview.client.vue | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue b/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue index 73aa5ea56..9c9a0e7b7 100644 --- a/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue +++ b/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue @@ -105,18 +105,18 @@ const fetchJsonData = async () => { error.value = null fileTooLarge.value = false - // If CORS is blocked, don't even try - if (!isCorsAllowed.value) { - error.value = 'cors' - return - } - // Check if file is too large or size is unknown if (!isSizeAllowed.value) { fileTooLarge.value = true return } + // Check if CORS is allowed + if (!isCorsAllowed.value) { + error.value = 'cors' + return + } + loading.value = true try { const response = await fetch(props.resource.url) diff --git a/datagouv-components/src/components/ResourceAccordion/PdfPreview.client.vue b/datagouv-components/src/components/ResourceAccordion/PdfPreview.client.vue index 449f6cd59..0935b4988 100644 --- a/datagouv-components/src/components/ResourceAccordion/PdfPreview.client.vue +++ b/datagouv-components/src/components/ResourceAccordion/PdfPreview.client.vue @@ -117,18 +117,18 @@ const loadPdf = async () => { error.value = null fileTooLarge.value = false - // If CORS is blocked, don't even try - if (!isCorsAllowed.value) { - error.value = 'cors' - return - } - // Check if file is too large or size is unknown if (!isSizeAllowed.value) { fileTooLarge.value = true return } + // Check if CORS is allowed + if (!isCorsAllowed.value) { + error.value = 'cors' + return + } + loading.value = true try { // Test if the PDF URL is accessible diff --git a/datagouv-components/src/components/ResourceAccordion/XmlPreview.client.vue b/datagouv-components/src/components/ResourceAccordion/XmlPreview.client.vue index a27e1712e..550a04fd4 100644 --- a/datagouv-components/src/components/ResourceAccordion/XmlPreview.client.vue +++ b/datagouv-components/src/components/ResourceAccordion/XmlPreview.client.vue @@ -96,18 +96,18 @@ const fetchXmlData = async () => { error.value = null fileTooLarge.value = false - // If CORS is blocked, don't even try - if (!isCorsAllowed.value) { - error.value = 'cors' - return - } - // Check if file is too large or size is unknown if (!isSizeAllowed.value) { fileTooLarge.value = true return } + // Check if CORS is allowed + if (!isCorsAllowed.value) { + error.value = 'cors' + return + } + loading.value = true try { const response = await fetch(props.resource.url) From d60e24bf861c7dd3c5fb9a4856bee97f37f19422 Mon Sep 17 00:00:00 2001 From: Adrien Carpentier Date: Wed, 25 Feb 2026 12:15:40 +0100 Subject: [PATCH 07/10] fix: fix css class typo --- .../src/components/ResourceAccordion/JsonPreview.client.vue | 4 ++-- .../src/components/ResourceAccordion/Preview.vue | 2 +- .../src/components/ResourceAccordion/XmlPreview.client.vue | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue b/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue index 9c9a0e7b7..7a080172d 100644 --- a/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue +++ b/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue @@ -22,7 +22,7 @@ type="warning" class="flex items-center space-x-2" > - + {{ fileSizeBytes ? t("Fichier JSON trop volumineux pour l'aperçu. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.") : 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.") @@ -49,7 +49,7 @@ type="warning" class="flex items-center space-x-2" > - + {{ t("Erreur lors du chargement de l'aperçu JSON.") }}
diff --git a/datagouv-components/src/components/ResourceAccordion/Preview.vue b/datagouv-components/src/components/ResourceAccordion/Preview.vue index 54f7f2bf5..3a7ce4646 100644 --- a/datagouv-components/src/components/ResourceAccordion/Preview.vue +++ b/datagouv-components/src/components/ResourceAccordion/Preview.vue @@ -5,7 +5,7 @@ type="warning" class="flex items-center space-x-2" > - + {{ t("L'aperçu de ce fichier n'a pas pu être chargé.") }} diff --git a/datagouv-components/src/components/ResourceAccordion/XmlPreview.client.vue b/datagouv-components/src/components/ResourceAccordion/XmlPreview.client.vue index 550a04fd4..3bea5d8b9 100644 --- a/datagouv-components/src/components/ResourceAccordion/XmlPreview.client.vue +++ b/datagouv-components/src/components/ResourceAccordion/XmlPreview.client.vue @@ -14,7 +14,7 @@ type="warning" class="flex items-center space-x-2" > - + {{ fileSizeBytes ? t("Fichier XML trop volumineux pour l'aperçu. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.") : 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.") @@ -41,7 +41,7 @@ type="warning" class="flex items-center space-x-2" > - + {{ t("Erreur lors du chargement de l'aperçu XML.") }} From 0f8d9d7968fb54456d8558d25bf37a3895354bd5 Mon Sep 17 00:00:00 2001 From: Adrien Carpentier Date: Wed, 25 Feb 2026 12:19:35 +0100 Subject: [PATCH 08/10] fix: remove isHealthy check in isResourceCorsEnabled --- datagouv-components/src/functions/resources.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/datagouv-components/src/functions/resources.ts b/datagouv-components/src/functions/resources.ts index f134fef2d..204b02f30 100644 --- a/datagouv-components/src/functions/resources.ts +++ b/datagouv-components/src/functions/resources.ts @@ -150,14 +150,9 @@ export const isResourceCorsEnabled = (resource: Resource): boolean => { const extras = resource.extras if (!extras) return false - const status = extras['check:status'] as number | undefined const allowOrigin = extras['check:cors:allow-origin'] as string | undefined const rawMethods = extras['check:cors:allow-methods'] as string | undefined - // Verify the last check was successful (HTTP 200) - const isHealthy = status === 200 - if (!isHealthy) return false - // Check if allow-origin is '*' or contains one of our trusted domains const trustedDomains = ['data.gouv.fr', 'www.data.gouv.fr'] // TODO: get from config const hasPublicCors = allowOrigin === '*' From d3ed929f39f7f87a5a9b202f776fd1fe2416bb86 Mon Sep 17 00:00:00 2001 From: Adrien Carpentier Date: Wed, 25 Feb 2026 19:19:40 +0100 Subject: [PATCH 09/10] feat: use trustedDomains from config --- app.vue | 1 + datagouv-components/src/functions/resources.ts | 5 ++++- nuxt.config.ts | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) 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/functions/resources.ts b/datagouv-components/src/functions/resources.ts index 204b02f30..67c35986c 100644 --- a/datagouv-components/src/functions/resources.ts +++ b/datagouv-components/src/functions/resources.ts @@ -1,6 +1,7 @@ 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' @@ -11,6 +12,8 @@ import Table from '../components/Icons/Table.vue' 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': @@ -154,7 +157,7 @@ export const isResourceCorsEnabled = (resource: Resource): boolean => { const rawMethods = extras['check:cors:allow-methods'] as string | undefined // Check if allow-origin is '*' or contains one of our trusted domains - const trustedDomains = ['data.gouv.fr', 'www.data.gouv.fr'] // TODO: get from config + const trustedDomains = config.trustedDomains const hasPublicCors = allowOrigin === '*' const hasSpecificCors = allowOrigin ? trustedDomains.some(domain => allowOrigin.includes(domain)) 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', From efeef3b2d76ab4a453867b42697893dec12dc3cb Mon Sep 17 00:00:00 2001 From: Adrien Carpentier Date: Wed, 25 Feb 2026 19:32:12 +0100 Subject: [PATCH 10/10] fix: manage different cases for getResourceCorsStatus --- .../ResourceAccordion/JsonPreview.client.vue | 6 +++--- .../components/ResourceAccordion/PdfPreview.client.vue | 6 +++--- .../components/ResourceAccordion/XmlPreview.client.vue | 6 +++--- datagouv-components/src/functions/resources.ts | 10 +++++++--- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue b/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue index 7a080172d..a2b22a4d4 100644 --- a/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue +++ b/datagouv-components/src/components/ResourceAccordion/JsonPreview.client.vue @@ -62,7 +62,7 @@ import { RiErrorWarningLine } from '@remixicon/vue' import { useComponentsConfig } from '../../config' import SimpleBanner from '../SimpleBanner.vue' import type { Resource } from '../../types/resources' -import { getResourceFilesize, isResourceCorsEnabled } from '../../functions/resources' +import { getResourceFilesize, getResourceCorsStatus } from '../../functions/resources' import { useTranslation } from '../../composables/useTranslation' const JsonViewer = defineAsyncComponent(() => @@ -87,7 +87,7 @@ const fileTooLarge = ref(false) const fileSizeBytes = computed(() => getResourceFilesize(props.resource)) -const isCorsAllowed = computed(() => isResourceCorsEnabled(props.resource)) +const corsStatus = computed(() => getResourceCorsStatus(props.resource)) const isSizeAllowed = computed(() => { const size = fileSizeBytes.value @@ -112,7 +112,7 @@ const fetchJsonData = async () => { } // Check if CORS is allowed - if (!isCorsAllowed.value) { + if (corsStatus.value === 'blocked') { error.value = 'cors' return } diff --git a/datagouv-components/src/components/ResourceAccordion/PdfPreview.client.vue b/datagouv-components/src/components/ResourceAccordion/PdfPreview.client.vue index 0935b4988..adcd7106d 100644 --- a/datagouv-components/src/components/ResourceAccordion/PdfPreview.client.vue +++ b/datagouv-components/src/components/ResourceAccordion/PdfPreview.client.vue @@ -77,7 +77,7 @@ import { RiErrorWarningLine } from '@remixicon/vue' import SimpleBanner from '../SimpleBanner.vue' import { useComponentsConfig } from '../../config' import type { Resource } from '../../types/resources' -import { getResourceFilesize, isResourceCorsEnabled } from '../../functions/resources' +import { getResourceFilesize, getResourceCorsStatus } from '../../functions/resources' import { useTranslation } from '../../composables/useTranslation' const PDF = defineAsyncComponent(() => @@ -100,7 +100,7 @@ const fileTooLarge = ref(false) const fileSizeBytes = computed(() => getResourceFilesize(props.resource)) -const isCorsAllowed = computed(() => isResourceCorsEnabled(props.resource)) +const corsStatus = computed(() => getResourceCorsStatus(props.resource)) const isSizeAllowed = computed(() => { const size = fileSizeBytes.value @@ -124,7 +124,7 @@ const loadPdf = async () => { } // Check if CORS is allowed - if (!isCorsAllowed.value) { + if (corsStatus.value === 'blocked') { error.value = 'cors' return } diff --git a/datagouv-components/src/components/ResourceAccordion/XmlPreview.client.vue b/datagouv-components/src/components/ResourceAccordion/XmlPreview.client.vue index 3bea5d8b9..de6b8f2ba 100644 --- a/datagouv-components/src/components/ResourceAccordion/XmlPreview.client.vue +++ b/datagouv-components/src/components/ResourceAccordion/XmlPreview.client.vue @@ -54,7 +54,7 @@ import { RiErrorWarningLine } from '@remixicon/vue' import { useComponentsConfig } from '../../config' import SimpleBanner from '../SimpleBanner.vue' import type { Resource } from '../../types/resources' -import { getResourceFilesize, isResourceCorsEnabled } from '../../functions/resources' +import { getResourceFilesize, getResourceCorsStatus } from '../../functions/resources' import { useTranslation } from '../../composables/useTranslation' import '../../types/vue3-xml-viewer.d' @@ -78,7 +78,7 @@ const fileTooLarge = ref(false) const fileSizeBytes = computed(() => getResourceFilesize(props.resource)) -const isCorsAllowed = computed(() => isResourceCorsEnabled(props.resource)) +const corsStatus = computed(() => getResourceCorsStatus(props.resource)) const isSizeAllowed = computed(() => { const size = fileSizeBytes.value @@ -103,7 +103,7 @@ const fetchXmlData = async () => { } // Check if CORS is allowed - if (!isCorsAllowed.value) { + if (corsStatus.value === 'blocked') { error.value = 'cors' return } diff --git a/datagouv-components/src/functions/resources.ts b/datagouv-components/src/functions/resources.ts index 67c35986c..e028c96ca 100644 --- a/datagouv-components/src/functions/resources.ts +++ b/datagouv-components/src/functions/resources.ts @@ -149,9 +149,13 @@ export function getResourceFilesize(resource: Resource): null | number { return null } -export const isResourceCorsEnabled = (resource: Resource): boolean => { +type CorsStatus = 'allowed' | 'blocked' | 'unknown' + +export const getResourceCorsStatus = (resource: Resource): CorsStatus => { const extras = resource.extras - if (!extras) return false + 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 @@ -171,5 +175,5 @@ export const isResourceCorsEnabled = (resource: Resource): boolean => { : [] const supportsGet = allowedMethods.length === 0 || allowedMethods.includes('GET') - return isOriginAllowed && supportsGet + return isOriginAllowed && supportsGet ? 'allowed' : 'blocked' }