Make photo tag text encoding more resilient

This commit is contained in:
Sam Becker 2024-02-27 23:32:51 -06:00
parent c0b041bf4f
commit ab8d088df5
8 changed files with 44 additions and 22 deletions

View File

@ -15,7 +15,11 @@ interface Props {
params: { tag: string } params: { tag: string }
} }
export default async function PhotoPageEdit({ params: { tag } }: Props) { export default async function PhotoPageEdit({
params: { tag: tagFromParams } }: Props
) {
const tag = decodeURIComponent(tagFromParams);
const [ const [
count, count,
photos, photos,

View File

@ -6,15 +6,17 @@ import {
getPhotosTagDataCached, getPhotosTagDataCached,
getPhotosTagDataCachedWithPagination, getPhotosTagDataCachedWithPagination,
} from '@/tag/data'; } from '@/tag/data';
import { Metadata } from 'next'; import type { Metadata } from 'next';
interface TagProps { interface TagProps {
params: { tag: string } params: { tag: string }
} }
export async function generateMetadata({ export async function generateMetadata({
params: { tag }, params: { tag: tagFromParams },
}: TagProps): Promise<Metadata> { }: TagProps): Promise<Metadata> {
const tag = decodeURIComponent(tagFromParams);
const [ const [
photos, photos,
count, count,
@ -49,9 +51,11 @@ export async function generateMetadata({
} }
export default async function TagPage({ export default async function TagPage({
params: { tag }, params: { tag: tagFromParams },
searchParams, searchParams,
}:TagProps & PaginationParams) { }:TagProps & PaginationParams) {
const tag = decodeURIComponent(tagFromParams);
const { const {
photos, photos,
count, count,

View File

@ -7,15 +7,17 @@ import {
getPhotosTagDataCached, getPhotosTagDataCached,
getPhotosTagDataCachedWithPagination, getPhotosTagDataCachedWithPagination,
} from '@/tag/data'; } from '@/tag/data';
import { Metadata } from 'next'; import type { Metadata } from 'next';
interface TagProps { interface TagProps {
params: { tag: string } params: { tag: string }
} }
export async function generateMetadata({ export async function generateMetadata({
params: { tag }, params: { tag: tagFromParams },
}: TagProps): Promise<Metadata> { }: TagProps): Promise<Metadata> {
const tag = decodeURIComponent(tagFromParams);
const [ const [
photos, photos,
count, count,
@ -50,9 +52,11 @@ export async function generateMetadata({
} }
export default async function Share({ export default async function Share({
params: { tag }, params: { tag: tagFromParams },
searchParams, searchParams,
}: TagProps & PaginationParams) { }: TagProps & PaginationParams) {
const tag = decodeURIComponent(tagFromParams);
const { const {
photos, photos,
count, count,

View File

@ -17,7 +17,7 @@ export type CameraWithCount = {
export type Cameras = CameraWithCount[]; export type Cameras = CameraWithCount[];
export const createCameraKey = ({ make, model }: Camera) => export const createCameraKey = ({ make, model }: Camera) =>
parameterize(`${make}-${model}`); parameterize(`${make}-${model}`, true);
// Assumes no makes ('Fujifilm,' 'Apple,' 'Canon', etc.) have dashes // Assumes no makes ('Fujifilm,' 'Apple,' 'Canon', etc.) have dashes
export const getCameraFromKey = (cameraKey: string): Camera => { export const getCameraFromKey = (cameraKey: string): Camera => {

View File

@ -72,11 +72,11 @@ export default function TagInput({
onChange?.([ onChange?.([
...selectedOptions, ...selectedOptions,
option.startsWith(CREATE_LABEL) option.startsWith(CREATE_LABEL)
? option.slice(CREATE_LABEL.length, -1) ? option.match(new RegExp(`^${CREATE_LABEL} "(.+)"$`))?.[1] ?? option
: option, : option,
] ]
.filter(Boolean) .filter(Boolean)
.map(parameterize) .map(item => parameterize(item))
.join(',')); .join(','));
} }
setSelectedOptionIndex(undefined); setSelectedOptionIndex(undefined);

View File

@ -22,6 +22,7 @@ import {
revalidateAdminPaths, revalidateAdminPaths,
revalidateAllKeysAndPaths, revalidateAllKeysAndPaths,
revalidatePhotosKey, revalidatePhotosKey,
revalidateTagsKey,
} from '@/photo/cache'; } from '@/photo/cache';
import { PATH_ADMIN_PHOTOS, PATH_ADMIN_TAGS, PATH_ROOT } from '@/site/paths'; import { PATH_ADMIN_PHOTOS, PATH_ADMIN_TAGS, PATH_ROOT } from '@/site/paths';
import { extractExifDataFromBlobPath } from './server'; import { extractExifDataFromBlobPath } from './server';
@ -105,6 +106,7 @@ export async function renamePhotoTagGloballyAction(formData: FormData) {
if (tag && updatedTag && tag !== updatedTag) { if (tag && updatedTag && tag !== updatedTag) {
await sqlRenamePhotoTagGlobally(tag, updatedTag); await sqlRenamePhotoTagGlobally(tag, updatedTag);
revalidatePhotosKey(); revalidatePhotosKey();
revalidateTagsKey();
redirect(PATH_ADMIN_TAGS); redirect(PATH_ADMIN_TAGS);
} }
} }

View File

@ -173,8 +173,8 @@ const sqlGetPhotosTagCount = async (tag: string) => sql`
const sqlGetPhotosCameraCount = async (camera: Camera) => sql` const sqlGetPhotosCameraCount = async (camera: Camera) => sql`
SELECT COUNT(*) FROM photos SELECT COUNT(*) FROM photos
WHERE WHERE
LOWER(make)=${parameterize(camera.make)} AND LOWER(make)=${parameterize(camera.make, true)} AND
LOWER(REPLACE(model, ' ', '-'))=${parameterize(camera.model)} AND LOWER(REPLACE(model, ' ', '-'))=${parameterize(camera.model, true)} AND
hidden IS NOT TRUE hidden IS NOT TRUE
`.then(({ rows }) => parseInt(rows[0].count, 10)); `.then(({ rows }) => parseInt(rows[0].count, 10));
@ -203,8 +203,8 @@ const sqlGetPhotosCameraDateRange = async (camera: Camera) => sql`
SELECT MIN(taken_at_naive) as start, MAX(taken_at_naive) as end SELECT MIN(taken_at_naive) as start, MAX(taken_at_naive) as end
FROM photos FROM photos
WHERE WHERE
LOWER(make)=${parameterize(camera.make)} AND LOWER(make)=${parameterize(camera.make, true)} AND
LOWER(REPLACE(model, ' ', '-'))=${parameterize(camera.model)} AND LOWER(REPLACE(model, ' ', '-'))=${parameterize(camera.model, true)} AND
hidden IS NOT TRUE hidden IS NOT TRUE
`.then(({ rows }) => rows[0] as PhotoDateRange); `.then(({ rows }) => rows[0] as PhotoDateRange);
@ -347,8 +347,8 @@ export const getPhotos = async (options: GetPhotosOptions = {}) => {
if (camera) { if (camera) {
wheres.push(`LOWER(make)=$${valueIndex++}`); wheres.push(`LOWER(make)=$${valueIndex++}`);
wheres.push(`LOWER(REPLACE(model, ' ', '-'))=$${valueIndex++}`); wheres.push(`LOWER(REPLACE(model, ' ', '-'))=$${valueIndex++}`);
values.push(parameterize(camera.make)); values.push(parameterize(camera.make, true));
values.push(parameterize(camera.model)); values.push(parameterize(camera.model, true));
} }
if (simulation) { if (simulation) {
wheres.push(`film_simulation=$${valueIndex++}`); wheres.push(`film_simulation=$${valueIndex++}`);

View File

@ -2,9 +2,9 @@ export const convertStringToArray = (
string?: string, string?: string,
shouldParameterize = true, shouldParameterize = true,
) => string ) => string
? string.split(',').map(tag => shouldParameterize ? string.split(',').map(item => shouldParameterize
? parameterize(tag) ? parameterize(item)
: tag.trim()) : item.trim())
: undefined; : undefined;
export const capitalize = (string: string) => export const capitalize = (string: string) =>
@ -16,14 +16,22 @@ export const capitalizeWords = (string = '') =>
.map(capitalize) .map(capitalize)
.join(' '); .join(' ');
export const parameterize = (string: string) => export const parameterize = (
string: string,
shouldRemoveNonAlphanumeric?: boolean,
) =>
string string
.trim() .trim()
// Replaces spaces, underscores, and dashes with dashes // Replaces spaces, underscores, and dashes with dashes
.replaceAll(/[\s_—]/gi, '-') .replaceAll(/[\s_—]/gi, '-')
// Removes all non-alphanumeric characters // Removes all non-alphanumeric characters
.replaceAll(/([^a-z0-9-])/gi, '') .replaceAll(
.toLowerCase(); shouldRemoveNonAlphanumeric
? /([^a-z0-9-])/gi
: /''/gi,
'',
)
.toLocaleLowerCase();
export const formatCount = (count: number) => `× ${count}`; export const formatCount = (count: number) => `× ${count}`;