@datagouv/components-next 1.0.2-dev.180 → 1.0.2-dev.182

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { g as Ke } from "./main-DD4ieUAo.js";
1
+ import { g as Ke } from "./main-8mcMUsT7.js";
2
2
  import We from "vue";
3
3
  function Fe($, K) {
4
4
  for (var V = 0; V < K.length; V++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datagouv/components-next",
3
- "version": "1.0.2-dev.180",
3
+ "version": "1.0.2-dev.182",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,14 +1,13 @@
1
1
  import { computed, type MaybeRefOrGetter, toValue } from 'vue'
2
2
  import { useComponentsConfig } from '../config'
3
3
  import { isOrganizationCertified } from '../functions/organizations'
4
- import { getWfsExportFormats } from '../functions/resourceCapabilities'
4
+ import { getGeneratedFormats, getWfsExportFormats } from '../functions/resourceCapabilities'
5
5
  import { detectOgcService, getParsingErrorMessage, getParsingErrorStep, isImagePreviewFormat } from '../functions/resources'
6
6
  import type { Dataset, DatasetV2 } from '../types/datasets'
7
7
  import type { Resource, WfsMetadata } from '../types/resources'
8
8
  import { useHasTabularData } from './useHasTabularData'
9
9
  import { useTranslation } from './useTranslation'
10
10
 
11
- const GENERATED_FORMATS = ['parquet', 'pmtiles', 'geojson']
12
11
  const URL_FORMATS = ['url', 'doi', 'www:link', 'www:link-1.0-http--link', 'www:link-1.0-http--partners', 'www:link-1.0-http--related', 'www:link-1.0-http--samples']
13
12
 
14
13
  // Which preview the data tab shows. Single source of truth: the viewer renders it and
@@ -54,24 +53,7 @@ export function useResourceCapabilities(
54
53
  const hasPmtilesError = computed(() => getParsingErrorStep(toValue(resource)) === 'pmtiles_export')
55
54
  const pmtilesError = computed(() => hasPmtilesError.value ? getParsingErrorMessage(toValue(resource)) : null)
56
55
 
57
- const generatedFormats = computed(() => {
58
- const r = toValue(resource)
59
- const formats = GENERATED_FORMATS
60
- .filter(format => `analysis:parsing:${format}_url` in r.extras)
61
- .map(format => ({
62
- url: r.extras[`analysis:parsing:${format}_url`] as string,
63
- size: r.extras[`analysis:parsing:${format}_size`] as number | undefined,
64
- format,
65
- }))
66
- if ('analysis:parsing:parsing_table' in r.extras) {
67
- formats.push({
68
- url: `${config.tabularApiUrl}/api/resources/${r.id}/data/json/`,
69
- size: undefined,
70
- format: 'json',
71
- })
72
- }
73
- return formats
74
- })
56
+ const generatedFormats = computed(() => getGeneratedFormats(toValue(resource), config.tabularApiUrl))
75
57
 
76
58
  const wfsFormats = computed(() => {
77
59
  return getWfsExportFormats(toValue(resource))
@@ -38,6 +38,21 @@ export const summarize = (val: number, fractionDigits = 0) => {
38
38
  return `${toFixedIfNotZero(val)}Y`
39
39
  }
40
40
 
41
+ // Guards any value that ends up in an `href`: only absolute http(s) URLs are allowed.
42
+ // Some of these URLs come from resource extras, which are freely writable by the
43
+ // resource owner, so an unguarded value could carry a `javascript:` or `data:` scheme
44
+ // and execute in the platform origin when a visitor clicks the link.
45
+ export const isSafeHttpUrl = (value: unknown): value is string => {
46
+ if (typeof value !== 'string') return false
47
+ try {
48
+ const { protocol } = new URL(value)
49
+ return protocol === 'http:' || protocol === 'https:'
50
+ }
51
+ catch {
52
+ return false
53
+ }
54
+ }
55
+
41
56
  export const escapeCsvValue = (value: string | number | null | undefined): string => {
42
57
  if (value === null || value === undefined || value === '') {
43
58
  return ''
@@ -1,4 +1,35 @@
1
1
  import type { Resource, WfsMetadata, OgcLayerInfo } from '../types/resources'
2
+ import { isSafeHttpUrl } from './helpers'
3
+
4
+ const GENERATED_FORMATS = ['parquet', 'pmtiles', 'geojson']
5
+
6
+ export type GeneratedFormat = {
7
+ url: string
8
+ size: number | undefined
9
+ format: string
10
+ }
11
+
12
+ export function getGeneratedFormats(resource: Pick<Resource, 'extras' | 'id'>, tabularApiUrl: string | undefined): GeneratedFormat[] {
13
+ const formats: GeneratedFormat[] = GENERATED_FORMATS.flatMap((format) => {
14
+ const url = resource.extras[`analysis:parsing:${format}_url`]
15
+ // The URL is read verbatim from owner-writable extras: drop any entry that is not
16
+ // a plain http(s) URL so a `javascript:`/`data:` value never reaches an href.
17
+ if (!isSafeHttpUrl(url)) return []
18
+ return [{
19
+ url,
20
+ size: resource.extras[`analysis:parsing:${format}_size`] as number | undefined,
21
+ format,
22
+ }]
23
+ })
24
+ if ('analysis:parsing:parsing_table' in resource.extras) {
25
+ formats.push({
26
+ url: `${tabularApiUrl}/api/resources/${resource.id}/data/json/`,
27
+ size: undefined,
28
+ format: 'json',
29
+ })
30
+ }
31
+ return formats
32
+ }
2
33
 
3
34
  const WFS_EXPORT_FORMATS = [
4
35
  {
@@ -46,6 +77,9 @@ export function getWfsExportFormats(resource: Pick<Resource, 'extras' | 'url'>)
46
77
  const outputFormats = wfsMetadata.output_formats.map((format: string) => format.toLowerCase())
47
78
  const layer = wfsMetadata.detected_layer
48
79
  if (!layer) return []
80
+ // Every download URL below is built on top of `resource.url`, so its scheme is the one
81
+ // of `resource.url`: guard it once here rather than on each of the built URLs.
82
+ if (!isSafeHttpUrl(resource.url)) return []
49
83
  const formats = WFS_EXPORT_FORMATS.filter(format => outputFormats.includes(format.mimetype.toLowerCase()))
50
84
  .map(format => ({
51
85
  url: buildWfsDownloadUrl(resource.url, wfsMetadata, format, layer),