fix(gallery): remove the inert image-protection prop surface from AuthenticatedImage

AuthenticatedImage accepted the whole image-protection prop surface and
discarded it in a `void unusedProps` block. Callers computed those props
from the event's protection level and passed them in good faith, so
raising the level produced canvas rendering (via the layouts' own OR on
`protectionLevel === 'maximum'`) and nothing else the level implies.

Removes them from the interface and from every call site, so the props
state what the component actually does. Two survive because they are
real: `useCanvasRendering`, and `onProtectionViolation` — which #1297
listed as inert but which does fire, from the canvas context-menu
handler. `useWatermark` is removed as well; #1297 did not list it (it sat
outside the `unusedProps` block) but it was equally dead.

Removal rather than implementation is deliberate. The implementation
these props describe already exists in `ProtectedImage`, which is
exported from the barrel and rendered nowhere. Wiring it in is a product
decision about what protection level should mean, not a side effect of a
cleanup.

Analytics payloads inside the surviving onProtectionViolation handlers
keep their photoId/protectionLevel fields.

Refs #1297
This commit is contained in:
Paul Nothaft
2026-09-05 06:31:47 +02:00
parent c71ffae912
commit e734e41c41
16 changed files with 30 additions and 157 deletions
@@ -11,25 +11,12 @@ import {
interface AuthenticatedImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'onLoad'> {
src: string;
fallbackSrc?: string;
useWatermark?: boolean;
isGallery?: boolean;
protectFromDownload?: boolean;
slug?: string;
photoId?: number;
requiresToken?: boolean;
secureUrlTemplate?: string;
downloadUrlTemplate?: string;
onProtectionViolation?: (violationType: string) => void;
watermarkText?: string;
overlayProtection?: boolean;
fragmentGrid?: boolean;
scrambleFragments?: boolean;
useCanvasRendering?: boolean;
blockKeyboardShortcuts?: boolean;
detectPrintScreen?: boolean;
detectDevTools?: boolean;
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
useEnhancedProtection?: boolean;
/** Fired when the canvas branch blocks a context-menu attempt. The only
* protection callback this component actually implements (#1297). */
onProtectionViolation?: (violationType: string) => void;
onLoad?: () => void;
/**
* Priority in the shared fetch queue (#1287). NOT the native `fetchPriority`
@@ -43,51 +30,44 @@ interface AuthenticatedImageProps extends Omit<React.ImgHTMLAttributes<HTMLImage
queuePriority?: 'high' | 'prefetch' | 'normal';
}
/**
* Fetches an image with the gallery's bearer token and renders it.
*
* IMAGE PROTECTION IS NOT IMPLEMENTED HERE (#1297). This component used to
* accept the whole protection prop surface — protectFromDownload,
* watermarkText, fragmentGrid, blockKeyboardShortcuts, detectPrintScreen,
* detectDevTools, protectionLevel and the rest — and discard every one of
* them in a `void unusedProps` block. Callers computed them from the event's
* protection level and passed them in good faith, so raising that level
* produced canvas rendering (via the layouts' own OR on
* `protectionLevel === 'maximum'`) and nothing else it implies.
*
* They are removed rather than implemented, so the interface states what the
* component actually does. The implementation those props describe already
* exists in `ProtectedImage` — which is exported and currently rendered
* nowhere. Wiring that in is a deliberate product decision about what
* protection level should mean, not a silent side effect of a cleanup.
*
* Two props survive because they are real:
* useCanvasRendering draws to a canvas instead of an <img>
* onProtectionViolation fires from the canvas context-menu handler below
*
* `useWatermark` was removed too. #1297 did not list it — it sat outside the
* `unusedProps` block — but it was equally inert: declared, defaulted, never
* read.
*/
export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
src,
fallbackSrc,
alt,
useWatermark = false,
isGallery = false,
protectFromDownload,
slug,
photoId,
requiresToken,
secureUrlTemplate,
downloadUrlTemplate,
onProtectionViolation,
watermarkText,
overlayProtection,
fragmentGrid,
scrambleFragments,
useCanvasRendering,
blockKeyboardShortcuts,
detectPrintScreen,
detectDevTools,
protectionLevel,
useEnhancedProtection,
onProtectionViolation,
onLoad,
queuePriority = 'normal',
...props
}) => {
const unusedProps = {
protectFromDownload,
photoId,
requiresToken,
secureUrlTemplate,
downloadUrlTemplate,
onProtectionViolation,
watermarkText,
overlayProtection,
fragmentGrid,
scrambleFragments,
blockKeyboardShortcuts,
detectPrintScreen,
detectDevTools,
protectionLevel,
useEnhancedProtection
};
void unusedProps;
const [imageSrc, setImageSrc] = useState<string>('');
const [error, setError] = useState(false);
@@ -41,9 +41,7 @@ export const GalleryFolderTiles: React.FC<GalleryFolderTilesProps> = ({
compact = false,
slug,
protectionLevel,
useEnhancedProtection,
useCanvasRendering,
allowDownloads = true,
}) => {
const { t } = useTranslation();
@@ -96,12 +94,6 @@ export const GalleryFolderTiles: React.FC<GalleryFolderTilesProps> = ({
className="w-full h-full object-cover group-hover:scale-[1.02] transition-transform"
isGallery
slug={slug}
photoId={coverPhoto.id}
requiresToken={coverPhoto.requires_token}
secureUrlTemplate={coverPhoto.secure_url_template}
protectFromDownload={!allowDownloads || useEnhancedProtection}
protectionLevel={protectionLevel}
useEnhancedProtection={useEnhancedProtection}
// Same rule as every other gallery image path: maximum
// protection implies canvas rendering even when the separate
// toggle is off (its default), otherwise a cover silently
@@ -43,9 +43,7 @@ export const HeroHeader: React.FC<HeroHeaderProps> = ({
heroLogoSize = 'medium',
heroLogoPosition = 'top',
dividerStyle = 'wave',
allowDownloads = true,
protectionLevel = 'standard',
useEnhancedProtection = false,
useCanvasRendering = false,
onScrollToContent,
heroImageAnchor = 'center'
@@ -144,10 +142,6 @@ export const HeroHeader: React.FC<HeroHeaderProps> = ({
style={{ objectPosition: heroImageAnchor }}
isGallery={true}
slug={slug}
photoId={heroPhoto.id}
protectFromDownload={!allowDownloads || useEnhancedProtection}
protectionLevel={protectionLevel}
useEnhancedProtection={useEnhancedProtection}
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
/>
@@ -144,9 +144,6 @@ export const PeopleSheet: React.FC<PeopleSheetProps> = ({
alt=""
isGallery
slug={slug}
photoId={photo.id}
requiresToken={photo.requires_token}
secureUrlTemplate={photo.secure_url_template}
// Crop to the face, exactly as the strip does. Without
// this a group photo shows whoever is centred — often
// not the person being labelled, and identical for two
@@ -86,9 +86,6 @@ const PersonAvatar: React.FC<PersonAvatarProps> = ({
alt=""
isGallery
slug={slug}
photoId={photo.id}
requiresToken={photo.requires_token}
secureUrlTemplate={photo.secure_url_template}
style={cropStyle || { width: '100%', height: '100%', objectFit: 'cover' }}
/>
) : (
@@ -244,7 +244,6 @@ const PhotoThumbnail: React.FC<PhotoThumbnailProps> = ({
onDownload,
allowDownloads = true,
protectionLevel = 'standard',
useEnhancedProtection = false,
useCanvasRendering = false,
slug,
feedbackEnabled = false
@@ -269,18 +268,7 @@ const PhotoThumbnail: React.FC<PhotoThumbnailProps> = ({
loading="lazy"
isGallery={true}
slug={slug}
photoId={photo.id}
requiresToken={photo.requires_token}
secureUrlTemplate={photo.secure_url_template}
protectFromDownload={!allowDownloads || useEnhancedProtection}
protectionLevel={protectionLevel}
useEnhancedProtection={useEnhancedProtection}
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
fragmentGrid={protectionLevel === 'enhanced' || protectionLevel === 'maximum'}
blockKeyboardShortcuts={useEnhancedProtection}
detectPrintScreen={useEnhancedProtection}
detectDevTools={protectionLevel === 'maximum'}
watermarkText={useEnhancedProtection ? 'Protected' : undefined}
onProtectionViolation={(violationType) => {
// Track analytics
if (typeof window !== 'undefined' && (window as any).umami) {
@@ -1159,9 +1159,6 @@ export const PhotoLightbox: React.FC<PhotoLightboxProps> = ({
draggable={false}
isGallery={true}
slug={slug}
photoId={photo.id}
requiresToken={photo.requires_token}
secureUrlTemplate={photo.secure_url_template}
/>
)}
</div>
@@ -1189,21 +1186,9 @@ export const PhotoLightbox: React.FC<PhotoLightboxProps> = ({
transition: isDragging ? 'none' : 'transform 0.2s',
}}
draggable={false}
useWatermark={useEnhancedProtection}
watermarkText={useEnhancedProtection ? `${photo.filename} - Protected` : undefined}
isGallery={true}
slug={slug}
photoId={photo.id}
requiresToken={photo.requires_token}
secureUrlTemplate={photo.secure_url_template}
protectFromDownload={!allowDownloads || useEnhancedProtection}
protectionLevel={protectionLevel}
useEnhancedProtection={useEnhancedProtection}
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
fragmentGrid={protectionLevel === 'enhanced' || protectionLevel === 'maximum'}
blockKeyboardShortcuts={useEnhancedProtection}
detectPrintScreen={useEnhancedProtection}
detectDevTools={protectionLevel === 'enhanced' || protectionLevel === 'maximum'}
onProtectionViolation={(violationType) => {
console.warn(`Protection violation in lightbox for photo ${photo.id}: ${violationType}`);
@@ -89,7 +89,6 @@ export const CarouselGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
alt={currentPhoto.filename}
className="w-full h-full object-contain"
isGallery={true}
protectFromDownload={!allowDownloads}
/>
{/* Colour labels for the photo in view (#1189). Bottom-left because it
@@ -267,7 +266,6 @@ export const CarouselGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
className="w-full h-full object-cover"
loading="lazy"
isGallery={true}
protectFromDownload={!allowDownloads}
/>
{/* The strip is the only place this layout shows more than one
photo at a time, so it is the only place a label can
@@ -68,9 +68,7 @@ const PhotoCard: React.FC<PhotoCardProps> = ({
isSelectionMode,
isLiked,
slug,
allowDownloads = true,
protectionLevel = 'standard',
useEnhancedProtection = false,
useCanvasRendering = false,
feedbackEnabled = false,
allowLikes = false,
@@ -124,12 +122,6 @@ const PhotoCard: React.FC<PhotoCardProps> = ({
loading="lazy"
isGallery={true}
slug={slug}
photoId={photo.id}
requiresToken={photo.requires_token}
secureUrlTemplate={photo.secure_url_template}
protectFromDownload={!allowDownloads || useEnhancedProtection}
protectionLevel={protectionLevel}
useEnhancedProtection={useEnhancedProtection}
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
/>
@@ -48,7 +48,6 @@ const GridPhoto: React.FC<GridPhotoProps> = ({
allowDownloads = true,
slug,
protectionLevel = 'standard',
useEnhancedProtection = false,
useCanvasRendering = false,
feedbackEnabled = false,
feedbackOptions,
@@ -93,18 +92,7 @@ const GridPhoto: React.FC<GridPhotoProps> = ({
loading: 'lazy',
isGallery: true,
slug,
photoId: photo.id,
requiresToken: photo.requires_token,
secureUrlTemplate: photo.secure_url_template,
protectFromDownload: !allowDownloads || useEnhancedProtection,
protectionLevel,
useEnhancedProtection,
useCanvasRendering: useCanvasRendering || protectionLevel === 'maximum',
fragmentGrid: protectionLevel === 'enhanced' || protectionLevel === 'maximum',
blockKeyboardShortcuts: useEnhancedProtection,
detectPrintScreen: useEnhancedProtection,
detectDevTools: protectionLevel === 'maximum',
watermarkText: useEnhancedProtection ? 'Protected' : undefined,
onProtectionViolation: (violationType: string) => {
console.warn(`Protection violation on grid photo ${photo.id}: ${violationType}`);
},
@@ -70,7 +70,6 @@ const JustifiedPhoto: React.FC<JustifiedPhotoProps> = ({
allowDownloads = true,
slug,
protectionLevel = 'standard',
useEnhancedProtection = false,
useCanvasRendering = false,
feedbackEnabled = false,
feedbackOptions,
@@ -134,18 +133,7 @@ const JustifiedPhoto: React.FC<JustifiedPhotoProps> = ({
loading: 'lazy',
isGallery: true,
slug,
photoId: photo.id,
requiresToken: photo.requires_token,
secureUrlTemplate: photo.secure_url_template,
protectFromDownload: !allowDownloads || useEnhancedProtection,
protectionLevel,
useEnhancedProtection,
useCanvasRendering: useCanvasRendering || protectionLevel === 'maximum',
fragmentGrid: protectionLevel === 'enhanced' || protectionLevel === 'maximum',
blockKeyboardShortcuts: useEnhancedProtection,
detectPrintScreen: useEnhancedProtection,
detectDevTools: protectionLevel === 'maximum',
watermarkText: useEnhancedProtection ? 'Protected' : undefined,
onProtectionViolation: (violationType: string) => {
console.warn(`Protection violation on justified photo ${photo.id}: ${violationType}`);
},
@@ -411,10 +399,6 @@ export const JustifiedGalleryLayout: React.FC<JustifiedGalleryLayoutProps> = ({
className="w-full h-full object-cover"
isGallery={true}
slug={slug}
photoId={heroPhoto.id}
protectFromDownload={!allowDownloads || useEnhancedProtection}
protectionLevel={protectionLevel}
useEnhancedProtection={useEnhancedProtection}
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
/>
@@ -120,7 +120,6 @@ const MasonryPhoto: React.FC<MasonryPhotoProps> = ({
className: 'w-full h-full object-cover rounded-lg',
loading: 'lazy',
isGallery: true,
protectFromDownload: !allowDownloads,
}}
overlayBaseClassName="absolute inset-0 bg-black/40 transition-opacity duration-200 rounded-lg flex items-center justify-center gap-2"
allowDownloads={allowDownloads}
@@ -376,7 +375,6 @@ export const MasonryGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
className: 'w-full h-full object-cover rounded-lg transition-transform duration-300 group-hover:scale-[1.02]',
loading: 'lazy',
isGallery: true,
protectFromDownload: !allowDownloads,
}}
overlayBaseClassName="absolute inset-0 bg-black/40 transition-opacity duration-200 rounded-lg flex items-center justify-center gap-2"
actionVariant="dark"
@@ -434,7 +432,6 @@ export const MasonryGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
className: 'w-full h-full object-cover rounded-lg transition-transform duration-300 group-hover:scale-[1.02]',
loading: 'lazy',
isGallery: true,
protectFromDownload: !allowDownloads,
}}
overlayBaseClassName="absolute inset-0 bg-black/40 transition-opacity duration-200 rounded-lg flex items-center justify-center gap-2"
actionVariant="dark"
@@ -501,7 +498,6 @@ export const MasonryGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
className: 'w-full h-full object-cover transition-transform duration-300 group-hover:scale-105',
loading: 'lazy',
isGallery: true,
protectFromDownload: !allowDownloads,
}}
overlayBaseClassName="absolute inset-0 bg-black/40 transition-opacity duration-200 flex items-center justify-center gap-2"
actionVariant="dark"
@@ -83,7 +83,6 @@ const MosaicPhoto: React.FC<MosaicPhotoProps> = ({
className: 'w-full h-full object-cover transition-transform duration-300 group-hover:scale-105',
loading: 'lazy',
isGallery: true,
protectFromDownload: !allowDownloads,
}}
overlayBaseClassName="absolute inset-0 bg-black/40 transition-opacity duration-200 flex items-center justify-center gap-2"
allowDownloads={allowDownloads}
@@ -130,7 +130,6 @@ export const TimelineGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
className: 'w-full h-full object-cover rounded-lg',
loading: 'lazy',
isGallery: true,
protectFromDownload: !allowDownloads,
}}
overlayBaseClassName="absolute inset-0 bg-black/40 transition-opacity duration-200 rounded-lg flex items-center justify-center gap-2"
allowDownloads={allowDownloads}
@@ -22,9 +22,7 @@ export const StoryHero: React.FC<StoryHeroProps> = ({
stats,
photo,
slug,
allowDownloads = true,
protectionLevel = 'standard',
useEnhancedProtection = false,
useCanvasRendering = false
}) => {
const formattedDate = date
@@ -51,12 +49,6 @@ export const StoryHero: React.FC<StoryHeroProps> = ({
className="w-full h-full object-cover"
isGallery={true}
slug={slug}
photoId={photo.id}
requiresToken={photo.requires_token}
secureUrlTemplate={photo.secure_url_template}
protectFromDownload={!allowDownloads || useEnhancedProtection}
protectionLevel={protectionLevel}
useEnhancedProtection={useEnhancedProtection}
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
/>
) : (
@@ -28,9 +28,7 @@ export const StoryPhotoCard: React.FC<StoryPhotoCardProps> = ({
onToggleFavorite,
onClick,
slug,
allowDownloads = true,
protectionLevel = 'standard',
useEnhancedProtection = false,
useCanvasRendering = false,
featured = false,
galleryId: _galleryId
@@ -105,12 +103,6 @@ export const StoryPhotoCard: React.FC<StoryPhotoCardProps> = ({
}`}
isGallery={true}
slug={slug}
photoId={photo.id}
requiresToken={photo.requires_token}
secureUrlTemplate={photo.secure_url_template}
protectFromDownload={!allowDownloads || useEnhancedProtection}
protectionLevel={protectionLevel}
useEnhancedProtection={useEnhancedProtection}
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
/>
)}