feat(media): support external https image urls for entities
Allow entities to use full https:// URLs as their image path Validate external URLs to prevent http://, data:, or credentials Update API responses and frontend components to handle external sources
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { parseIdList, parseMatchMode, sqlForRelationFilter } from './filter.ts';
|
||||
import { pool, query, queryOne } from './db.ts';
|
||||
import {
|
||||
isExternalImageUrl,
|
||||
isUploadImagePath,
|
||||
linkEntityImageUpload,
|
||||
listEntityImageUploads,
|
||||
@@ -192,7 +193,7 @@ type PokemonImage = {
|
||||
version: string;
|
||||
variant: string;
|
||||
description: string;
|
||||
source?: 'sprite' | 'upload';
|
||||
source?: 'sprite' | 'upload' | 'external';
|
||||
};
|
||||
|
||||
type EntityImageValue = {
|
||||
@@ -846,6 +847,10 @@ function sqlLiteral(value: string): string {
|
||||
function uploadedImageJson(pathExpression: string): string {
|
||||
return `
|
||||
CASE
|
||||
WHEN lower(${pathExpression}) LIKE 'https://%' THEN json_build_object(
|
||||
'path', ${pathExpression},
|
||||
'url', ${pathExpression}
|
||||
)
|
||||
WHEN ${pathExpression} LIKE ${sqlLiteral(`${itemStaticImagePathPrefix}%`)}
|
||||
OR ${pathExpression} LIKE ${sqlLiteral(`${habitatStaticImagePathPrefix}%`)} THEN json_build_object(
|
||||
'path', ${pathExpression},
|
||||
@@ -863,6 +868,15 @@ function uploadedImageJson(pathExpression: string): string {
|
||||
function pokemonImageJson(alias: string): string {
|
||||
return `
|
||||
CASE
|
||||
WHEN lower(${alias}.image_path) LIKE 'https://%' THEN json_build_object(
|
||||
'path', ${alias}.image_path,
|
||||
'url', ${alias}.image_path,
|
||||
'style', '',
|
||||
'version', '',
|
||||
'variant', ${alias}.name,
|
||||
'description', '',
|
||||
'source', 'external'
|
||||
)
|
||||
WHEN ${alias}.image_path LIKE '/sprites/%' THEN json_build_object(
|
||||
'path', ${alias}.image_path,
|
||||
'url', ${sqlLiteral(pokemonSpriteBaseUrl)} || ${alias}.image_path,
|
||||
@@ -891,6 +905,9 @@ function imagePathLabel(path: string | null | undefined): string {
|
||||
if (cleanPath === '') {
|
||||
return '';
|
||||
}
|
||||
if (isExternalImageUrl(cleanPath)) {
|
||||
return cleanPath;
|
||||
}
|
||||
|
||||
const parts = cleanPath.split('/');
|
||||
return parts.length >= 3 ? `${parts[1]} / ${parts[2]}` : cleanPath;
|
||||
@@ -1247,6 +1264,9 @@ function cleanUploadImagePath(value: unknown, entityType: 'items' | 'habitats' |
|
||||
if (imagePath === '') {
|
||||
return '';
|
||||
}
|
||||
if (isExternalImageUrl(imagePath)) {
|
||||
return imagePath;
|
||||
}
|
||||
if (entityType === 'habitats' && isHabitatStaticImagePath(imagePath)) {
|
||||
return imagePath;
|
||||
}
|
||||
@@ -1261,6 +1281,9 @@ function cleanItemOrArtifactImagePath(value: unknown): string {
|
||||
if (imagePath === '') {
|
||||
return '';
|
||||
}
|
||||
if (isExternalImageUrl(imagePath)) {
|
||||
return imagePath;
|
||||
}
|
||||
if (isItemStaticImagePath(imagePath)) {
|
||||
return imagePath;
|
||||
}
|
||||
@@ -1983,7 +2006,12 @@ function pokemonImageLabel(image: PokemonImage | null | undefined): string {
|
||||
if (!image) {
|
||||
return '';
|
||||
}
|
||||
return image.source === 'upload' || isUploadImagePath(image.path) ? imagePathLabel(image.path) : `${image.style} - ${image.version} - ${image.variant}`;
|
||||
return image.source === 'upload'
|
||||
|| image.source === 'external'
|
||||
|| isUploadImagePath(image.path)
|
||||
|| isExternalImageUrl(image.path)
|
||||
? imagePathLabel(image.path)
|
||||
: `${image.style} - ${image.version} - ${image.variant}`;
|
||||
}
|
||||
|
||||
function pokemonImageDataIdFromPath(path: string): number | null {
|
||||
@@ -2013,6 +2041,18 @@ function cleanPokemonImage(value: unknown, displayId: number): PokemonImage | nu
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isExternalImageUrl(path)) {
|
||||
return {
|
||||
path,
|
||||
url: path,
|
||||
style: '',
|
||||
version: '',
|
||||
variant: `#${displayId}`,
|
||||
description: '',
|
||||
source: 'external'
|
||||
};
|
||||
}
|
||||
|
||||
if (isUploadImagePath(path)) {
|
||||
if (!path.startsWith('pokemon/')) {
|
||||
throw validationError('server.validation.imagePathInvalid');
|
||||
|
||||
@@ -53,6 +53,20 @@ export function isUploadImagePath(value: string | null | undefined): boolean {
|
||||
return isUploadEntityType(entityType);
|
||||
}
|
||||
|
||||
export function isExternalImageUrl(value: string | null | undefined): boolean {
|
||||
const cleanUrl = value?.trim() ?? '';
|
||||
if (cleanUrl === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(cleanUrl);
|
||||
return url.protocol === 'https:' && url.username === '' && url.password === '';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function uploadImageUrl(relativePath: string): string {
|
||||
return `${uploadPublicBaseUrl}${relativePath.split('/').map(encodeURIComponent).join('/')}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user