Merge pull request #1299 from PicPeak/fix/1297-inert-protection-props

fix(gallery): remove the inert image-protection prop surface from AuthenticatedImage
This commit is contained in:
Paul Nothaft
2026-09-05 23:36:27 +02:00
committed by GitHub
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'}
/>
)}