|
| 1 | +import { isRemoteAllowed } from '@astrojs/internal-helpers/remote'; |
1 | 2 | import { AstroError, AstroErrorData } from '../../core/errors/index.js'; |
| 3 | +import type { AstroConfig } from '../../types/public/config.js'; |
2 | 4 | import type { ImageMetadata } from '../types.js'; |
3 | 5 | import { imageMetadata } from './metadata.js'; |
4 | 6 |
|
| 7 | +type RemoteImageConfig = Pick<AstroConfig['image'], 'domains' | 'remotePatterns'>; |
| 8 | + |
5 | 9 | /** |
6 | 10 | * Infers the dimensions of a remote image by streaming its data and analyzing it progressively until sufficient metadata is available. |
7 | 11 | * |
8 | 12 | * @param {string} url - The URL of the remote image from which to infer size metadata. |
| 13 | + * @param {RemoteImageConfig} [imageConfig] - Optional image config used to validate remote allowlists. |
9 | 14 | * @return {Promise<Omit<ImageMetadata, 'src' | 'fsPath'>>} Returns a promise that resolves to an object containing the image dimensions metadata excluding `src` and `fsPath`. |
10 | 15 | * @throws {AstroError} Thrown when the fetching fails or metadata cannot be extracted. |
11 | 16 | */ |
12 | | -export async function inferRemoteSize(url: string): Promise<Omit<ImageMetadata, 'src' | 'fsPath'>> { |
| 17 | +export async function inferRemoteSize( |
| 18 | + url: string, |
| 19 | + imageConfig?: RemoteImageConfig, |
| 20 | +): Promise<Omit<ImageMetadata, 'src' | 'fsPath'>> { |
| 21 | + if (!URL.canParse(url)) { |
| 22 | + throw new AstroError({ |
| 23 | + ...AstroErrorData.FailedToFetchRemoteImageDimensions, |
| 24 | + message: AstroErrorData.FailedToFetchRemoteImageDimensions.message(url), |
| 25 | + }); |
| 26 | + } |
| 27 | + |
| 28 | + const allowlistConfig = imageConfig |
| 29 | + ? { |
| 30 | + domains: imageConfig.domains ?? [], |
| 31 | + remotePatterns: imageConfig.remotePatterns ?? [], |
| 32 | + } |
| 33 | + : undefined; |
| 34 | + |
| 35 | + if (!allowlistConfig) { |
| 36 | + const parsedUrl = new URL(url); |
| 37 | + if (!['http:', 'https:'].includes(parsedUrl.protocol)) { |
| 38 | + throw new AstroError({ |
| 39 | + ...AstroErrorData.FailedToFetchRemoteImageDimensions, |
| 40 | + message: AstroErrorData.FailedToFetchRemoteImageDimensions.message(url), |
| 41 | + }); |
| 42 | + } |
| 43 | + } |
| 44 | + |
| 45 | + if (allowlistConfig && !isRemoteAllowed(url, allowlistConfig)) { |
| 46 | + throw new AstroError({ |
| 47 | + ...AstroErrorData.RemoteImageNotAllowed, |
| 48 | + message: AstroErrorData.RemoteImageNotAllowed.message(url), |
| 49 | + }); |
| 50 | + } |
| 51 | + |
13 | 52 | // Start fetching the image |
14 | | - const response = await fetch(url); |
| 53 | + const response = await fetch(url, { redirect: 'manual' }); |
| 54 | + |
| 55 | + if (response.status >= 300 && response.status < 400) { |
| 56 | + throw new AstroError({ |
| 57 | + ...AstroErrorData.FailedToFetchRemoteImageDimensions, |
| 58 | + message: AstroErrorData.FailedToFetchRemoteImageDimensions.message(url), |
| 59 | + }); |
| 60 | + } |
| 61 | + |
15 | 62 | if (!response.body || !response.ok) { |
16 | 63 | throw new AstroError({ |
17 | 64 | ...AstroErrorData.FailedToFetchRemoteImageDimensions, |
|
0 commit comments