From e734e41c412cede1be5efbe27aab617d59faee9d Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Sat, 5 Sep 2026 06:31:47 +0200 Subject: [PATCH] fix(gallery): remove the inert image-protection prop surface from AuthenticatedImage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../components/common/AuthenticatedImage.tsx | 80 +++++++------------ .../components/gallery/GalleryFolderTiles.tsx | 8 -- .../src/components/gallery/HeroHeader.tsx | 6 -- .../src/components/gallery/PeopleSheet.tsx | 3 - .../src/components/gallery/PeopleStrip.tsx | 3 - frontend/src/components/gallery/PhotoGrid.tsx | 12 --- .../src/components/gallery/PhotoLightbox.tsx | 15 ---- .../gallery/layouts/CarouselGalleryLayout.tsx | 2 - .../gallery/layouts/GalleryPremiumLayout.tsx | 8 -- .../gallery/layouts/GridGalleryLayout.tsx | 12 --- .../layouts/JustifiedGalleryLayout.tsx | 16 ---- .../gallery/layouts/MasonryGalleryLayout.tsx | 4 - .../gallery/layouts/MosaicGalleryLayout.tsx | 1 - .../gallery/layouts/TimelineGalleryLayout.tsx | 1 - .../gallery/layouts/story/StoryHero.tsx | 8 -- .../gallery/layouts/story/StoryPhotoCard.tsx | 8 -- 16 files changed, 30 insertions(+), 157 deletions(-) diff --git a/frontend/src/components/common/AuthenticatedImage.tsx b/frontend/src/components/common/AuthenticatedImage.tsx index bd3bc8fe..08215896 100644 --- a/frontend/src/components/common/AuthenticatedImage.tsx +++ b/frontend/src/components/common/AuthenticatedImage.tsx @@ -11,25 +11,12 @@ import { interface AuthenticatedImageProps extends Omit, '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 + * 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 = ({ 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(''); const [error, setError] = useState(false); diff --git a/frontend/src/components/gallery/GalleryFolderTiles.tsx b/frontend/src/components/gallery/GalleryFolderTiles.tsx index 0f05efc7..3ac15763 100644 --- a/frontend/src/components/gallery/GalleryFolderTiles.tsx +++ b/frontend/src/components/gallery/GalleryFolderTiles.tsx @@ -41,9 +41,7 @@ export const GalleryFolderTiles: React.FC = ({ compact = false, slug, protectionLevel, - useEnhancedProtection, useCanvasRendering, - allowDownloads = true, }) => { const { t } = useTranslation(); @@ -96,12 +94,6 @@ export const GalleryFolderTiles: React.FC = ({ 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 diff --git a/frontend/src/components/gallery/HeroHeader.tsx b/frontend/src/components/gallery/HeroHeader.tsx index 891ff638..6f183799 100644 --- a/frontend/src/components/gallery/HeroHeader.tsx +++ b/frontend/src/components/gallery/HeroHeader.tsx @@ -43,9 +43,7 @@ export const HeroHeader: React.FC = ({ 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 = ({ style={{ objectPosition: heroImageAnchor }} isGallery={true} slug={slug} - photoId={heroPhoto.id} - protectFromDownload={!allowDownloads || useEnhancedProtection} - protectionLevel={protectionLevel} - useEnhancedProtection={useEnhancedProtection} useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'} /> diff --git a/frontend/src/components/gallery/PeopleSheet.tsx b/frontend/src/components/gallery/PeopleSheet.tsx index af339142..c0ab98b0 100644 --- a/frontend/src/components/gallery/PeopleSheet.tsx +++ b/frontend/src/components/gallery/PeopleSheet.tsx @@ -144,9 +144,6 @@ export const PeopleSheet: React.FC = ({ 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 diff --git a/frontend/src/components/gallery/PeopleStrip.tsx b/frontend/src/components/gallery/PeopleStrip.tsx index a85e05fd..d2bacdc7 100644 --- a/frontend/src/components/gallery/PeopleStrip.tsx +++ b/frontend/src/components/gallery/PeopleStrip.tsx @@ -86,9 +86,6 @@ const PersonAvatar: React.FC = ({ alt="" isGallery slug={slug} - photoId={photo.id} - requiresToken={photo.requires_token} - secureUrlTemplate={photo.secure_url_template} style={cropStyle || { width: '100%', height: '100%', objectFit: 'cover' }} /> ) : ( diff --git a/frontend/src/components/gallery/PhotoGrid.tsx b/frontend/src/components/gallery/PhotoGrid.tsx index 2379cb94..154e8858 100644 --- a/frontend/src/components/gallery/PhotoGrid.tsx +++ b/frontend/src/components/gallery/PhotoGrid.tsx @@ -244,7 +244,6 @@ const PhotoThumbnail: React.FC = ({ onDownload, allowDownloads = true, protectionLevel = 'standard', - useEnhancedProtection = false, useCanvasRendering = false, slug, feedbackEnabled = false @@ -269,18 +268,7 @@ const PhotoThumbnail: React.FC = ({ 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) { diff --git a/frontend/src/components/gallery/PhotoLightbox.tsx b/frontend/src/components/gallery/PhotoLightbox.tsx index d5c475c0..6daf8686 100644 --- a/frontend/src/components/gallery/PhotoLightbox.tsx +++ b/frontend/src/components/gallery/PhotoLightbox.tsx @@ -1159,9 +1159,6 @@ export const PhotoLightbox: React.FC = ({ draggable={false} isGallery={true} slug={slug} - photoId={photo.id} - requiresToken={photo.requires_token} - secureUrlTemplate={photo.secure_url_template} /> )} @@ -1189,21 +1186,9 @@ export const PhotoLightbox: React.FC = ({ 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}`); diff --git a/frontend/src/components/gallery/layouts/CarouselGalleryLayout.tsx b/frontend/src/components/gallery/layouts/CarouselGalleryLayout.tsx index f341101d..d0da929e 100644 --- a/frontend/src/components/gallery/layouts/CarouselGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/CarouselGalleryLayout.tsx @@ -89,7 +89,6 @@ export const CarouselGalleryLayout: React.FC = ({ 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 = ({ 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 diff --git a/frontend/src/components/gallery/layouts/GalleryPremiumLayout.tsx b/frontend/src/components/gallery/layouts/GalleryPremiumLayout.tsx index e2dedbb9..e6c99b34 100644 --- a/frontend/src/components/gallery/layouts/GalleryPremiumLayout.tsx +++ b/frontend/src/components/gallery/layouts/GalleryPremiumLayout.tsx @@ -68,9 +68,7 @@ const PhotoCard: React.FC = ({ isSelectionMode, isLiked, slug, - allowDownloads = true, protectionLevel = 'standard', - useEnhancedProtection = false, useCanvasRendering = false, feedbackEnabled = false, allowLikes = false, @@ -124,12 +122,6 @@ const PhotoCard: React.FC = ({ 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'} /> diff --git a/frontend/src/components/gallery/layouts/GridGalleryLayout.tsx b/frontend/src/components/gallery/layouts/GridGalleryLayout.tsx index 544154ca..9fe14320 100644 --- a/frontend/src/components/gallery/layouts/GridGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/GridGalleryLayout.tsx @@ -48,7 +48,6 @@ const GridPhoto: React.FC = ({ allowDownloads = true, slug, protectionLevel = 'standard', - useEnhancedProtection = false, useCanvasRendering = false, feedbackEnabled = false, feedbackOptions, @@ -93,18 +92,7 @@ const GridPhoto: React.FC = ({ 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}`); }, diff --git a/frontend/src/components/gallery/layouts/JustifiedGalleryLayout.tsx b/frontend/src/components/gallery/layouts/JustifiedGalleryLayout.tsx index a99193fe..8e0d5113 100644 --- a/frontend/src/components/gallery/layouts/JustifiedGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/JustifiedGalleryLayout.tsx @@ -70,7 +70,6 @@ const JustifiedPhoto: React.FC = ({ allowDownloads = true, slug, protectionLevel = 'standard', - useEnhancedProtection = false, useCanvasRendering = false, feedbackEnabled = false, feedbackOptions, @@ -134,18 +133,7 @@ const JustifiedPhoto: React.FC = ({ 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 = ({ 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'} /> diff --git a/frontend/src/components/gallery/layouts/MasonryGalleryLayout.tsx b/frontend/src/components/gallery/layouts/MasonryGalleryLayout.tsx index 8c9ac998..4328d9b0 100644 --- a/frontend/src/components/gallery/layouts/MasonryGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/MasonryGalleryLayout.tsx @@ -120,7 +120,6 @@ const MasonryPhoto: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ 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" diff --git a/frontend/src/components/gallery/layouts/MosaicGalleryLayout.tsx b/frontend/src/components/gallery/layouts/MosaicGalleryLayout.tsx index 6a7b2915..05d14d01 100644 --- a/frontend/src/components/gallery/layouts/MosaicGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/MosaicGalleryLayout.tsx @@ -83,7 +83,6 @@ const MosaicPhoto: React.FC = ({ 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} diff --git a/frontend/src/components/gallery/layouts/TimelineGalleryLayout.tsx b/frontend/src/components/gallery/layouts/TimelineGalleryLayout.tsx index cac34ec8..b7bccf79 100644 --- a/frontend/src/components/gallery/layouts/TimelineGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/TimelineGalleryLayout.tsx @@ -130,7 +130,6 @@ export const TimelineGalleryLayout: React.FC = ({ 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} diff --git a/frontend/src/components/gallery/layouts/story/StoryHero.tsx b/frontend/src/components/gallery/layouts/story/StoryHero.tsx index 3c6e23b3..66293623 100644 --- a/frontend/src/components/gallery/layouts/story/StoryHero.tsx +++ b/frontend/src/components/gallery/layouts/story/StoryHero.tsx @@ -22,9 +22,7 @@ export const StoryHero: React.FC = ({ stats, photo, slug, - allowDownloads = true, protectionLevel = 'standard', - useEnhancedProtection = false, useCanvasRendering = false }) => { const formattedDate = date @@ -51,12 +49,6 @@ export const StoryHero: React.FC = ({ 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'} /> ) : ( diff --git a/frontend/src/components/gallery/layouts/story/StoryPhotoCard.tsx b/frontend/src/components/gallery/layouts/story/StoryPhotoCard.tsx index b9fa9c76..a5eb5643 100644 --- a/frontend/src/components/gallery/layouts/story/StoryPhotoCard.tsx +++ b/frontend/src/components/gallery/layouts/story/StoryPhotoCard.tsx @@ -28,9 +28,7 @@ export const StoryPhotoCard: React.FC = ({ onToggleFavorite, onClick, slug, - allowDownloads = true, protectionLevel = 'standard', - useEnhancedProtection = false, useCanvasRendering = false, featured = false, galleryId: _galleryId @@ -105,12 +103,6 @@ export const StoryPhotoCard: React.FC = ({ }`} 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'} /> )}