From 457fb9e898ecf4b1b690a217793d6d1178bd2cc5 Mon Sep 17 00:00:00 2001 From: Nicholas Chiang Date: Sat, 5 Aug 2023 18:01:48 -0600 Subject: [PATCH] feat(show): use show description as meta description This patch updates the show page description to use the show description if it exists, and revert back to the Vogue inspired description if it does not. This patch also adds some XSS support that may be somewhat premature, but was easy enough to add that it felt it wouldn't hurt. Closes: NC-666 --- .../route.tsx | 21 ++++++++++- app/sanitize.server.ts | 8 +++++ package.json | 2 ++ pnpm-lock.yaml | 36 ++++++++++++++----- remix.config.js | 2 +- 5 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 app/sanitize.server.ts diff --git a/app/routes/_header.shows.($location).$year.$season.$sex.$level.$brand/route.tsx b/app/routes/_header.shows.($location).$year.$season.$sex.$level.$brand/route.tsx index 8676b7de..a23b4413 100644 --- a/app/routes/_header.shows.($location).$year.$season.$sex.$level.$brand/route.tsx +++ b/app/routes/_header.shows.($location).$year.$season.$sex.$level.$brand/route.tsx @@ -19,6 +19,7 @@ import { getShowKeywords, getShowPath, getShowSchema } from 'utils/show' import { prisma } from 'db.server' import { log } from 'log.server' import { type Handle } from 'root' +import { sanitize } from 'sanitize.server' import { getUserId } from 'session.server' import { About } from './about' @@ -32,7 +33,9 @@ export const meta: V2_MetaFunction = ({ data }) => { { title: `${data.name} Collection | ${NAME}` }, { name: 'description', - content: `${data.name} collection, runway looks, beauty, models, and reviews.`, + content: data.description + ? sanitize(data.description, { allowedTags: [] }) + : `${data.name} collection, runway looks, beauty, models, and reviews.`, }, { name: 'keywords', content: keywords }, { name: 'news_keywords', content: keywords }, @@ -115,10 +118,26 @@ export async function loader({ request, params }: LoaderArgs) { }) if (show == null) throw miss log.debug('got show %o', show) + + // Sanitize HTML (perhaps this should be in a separate helper function). + /* eslint-disable no-param-reassign */ + show.articles.forEach((article) => { + article.content = sanitize(article.content) + }) + show.description = sanitize(show.description) + show.collections.forEach((collection) => { + collection.designers.forEach((designer) => { + designer.description = sanitize(designer.description) + }) + }) + /* eslint-enable no-param-reassign */ + + // Derive the show's scores and get the current user's review of it. const [scores, review] = await Promise.all([ getScores(show.id), getReview(show.id, request), ]) + return { ...show, scores, review } } diff --git a/app/sanitize.server.ts b/app/sanitize.server.ts new file mode 100644 index 00000000..01380600 --- /dev/null +++ b/app/sanitize.server.ts @@ -0,0 +1,8 @@ +import sanitizeHtml from 'sanitize-html' + +export function sanitize(html: string, options?: sanitizeHtml.IOptions): string +export function sanitize(html: null, options?: sanitizeHtml.IOptions): null +export function sanitize(html: string | null, options?: sanitizeHtml.IOptions): string | null +export function sanitize(html: string | null, options?: sanitizeHtml.IOptions) { + return html ? sanitizeHtml(html, options) : html +} diff --git a/package.json b/package.json index 9d163aee..ca71681a 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "react-simple-maps": "^3.0.0", "remix-sitemap": "^2.2.0", "rfdc": "^1.3.0", + "sanitize-html": "^2.11.0", "schema-dts": "^1.1.2", "sharp": "^0.32.4", "tailwind-merge": "^1.14.0", @@ -131,6 +132,7 @@ "@types/react": "^18.2.20", "@types/react-dom": "^18.2.7", "@types/react-simple-maps": "^3.0.0", + "@types/sanitize-html": "^2.9.0", "@types/sharp": "^0.31.1", "@types/user-agents": "^1.0.2", "@typescript-eslint/eslint-plugin": "^5.62.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 236987b0..6cf27087 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,6 +136,9 @@ dependencies: rfdc: specifier: ^1.3.0 version: 1.3.0 + sanitize-html: + specifier: ^2.11.0 + version: 2.11.0 schema-dts: specifier: ^1.1.2 version: 1.1.2(typescript@4.9.5) @@ -255,6 +258,9 @@ devDependencies: '@types/react-simple-maps': specifier: ^3.0.0 version: 3.0.0 + '@types/sanitize-html': + specifier: ^2.9.0 + version: 2.9.0 '@types/sharp': specifier: ^0.31.1 version: 0.31.1 @@ -4915,6 +4921,12 @@ packages: '@types/node': 18.17.4 dev: true + /@types/sanitize-html@2.9.0: + resolution: {integrity: sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg==} + dependencies: + htmlparser2: 8.0.2 + dev: true + /@types/scheduler@0.16.3: resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} @@ -7116,7 +7128,6 @@ packages: /deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - dev: true /defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} @@ -7259,18 +7270,15 @@ packages: domelementtype: 2.3.0 domhandler: 5.0.3 entities: 4.4.0 - dev: true /domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - dev: true /domhandler@5.0.3: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} dependencies: domelementtype: 2.3.0 - dev: true /domutils@3.0.1: resolution: {integrity: sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==} @@ -7278,7 +7286,6 @@ packages: dom-serializer: 2.0.0 domelementtype: 2.3.0 domhandler: 5.0.3 - dev: true /dot-json@1.3.0: resolution: {integrity: sha512-Pu11Prog/Yjf2lBICow82/DSV46n3a2XT1Rqt/CeuhkO1fuacF7xydYhI0SwQx2Ue0jCyLtQzgKPFEO6ewv+bQ==} @@ -7398,7 +7405,6 @@ packages: /entities@4.4.0: resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==} engines: {node: '>=0.12'} - dev: true /env-ci@9.1.0: resolution: {integrity: sha512-ZCEas2sDVFR3gpumwwzSU4OJZwWJ46yqJH3TqH3vSxEBzeAlC0uCJLGAnZC0vX1TIXzHzjcwpKmUn2xw5mC/qA==} @@ -7896,7 +7902,6 @@ packages: /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - dev: true /escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} @@ -9457,7 +9462,6 @@ packages: domhandler: 5.0.3 domutils: 3.0.1 entities: 4.4.0 - dev: true /http-cache-semantics@4.1.1: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} @@ -9924,7 +9928,6 @@ packages: /is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} - dev: true /is-reference@3.0.1: resolution: {integrity: sha512-baJJdQLiYaJdvFbJqXrcGv3WU3QCzBlUcI5QhbesIm6/xPsvmO+2CDoi/GMOFBQEQm+PXkwOPrp9KK5ozZsp2w==} @@ -12087,6 +12090,10 @@ packages: engines: {node: '>=6'} dev: true + /parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + dev: false + /parse5-htmlparser2-tree-adapter@7.0.0: resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} dependencies: @@ -13585,6 +13592,17 @@ packages: /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + /sanitize-html@2.11.0: + resolution: {integrity: sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==} + dependencies: + deepmerge: 4.3.1 + escape-string-regexp: 4.0.0 + htmlparser2: 8.0.2 + is-plain-object: 5.0.0 + parse-srcset: 1.0.2 + postcss: 8.4.27 + dev: false + /scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: diff --git a/remix.config.js b/remix.config.js index 2ba88d81..78a7a1b0 100644 --- a/remix.config.js +++ b/remix.config.js @@ -13,7 +13,7 @@ module.exports = { cacheDirectory: './node_modules/.cache/remix', ignoredRouteFiles: ['**/.*', '**/*.css', '**/*.test.{js,jsx,ts,tsx}'], serverModuleFormat: 'cjs', - serverDependenciesToBundle: ['nanoid/non-secure', /d3-.*/], + serverDependenciesToBundle: ['nanoid/non-secure', 'sanitize-html', /d3-.*/], images: { sizes: [200, 300, 400, 500, 600, 700, 800, 900, 1000], domains: ['aritzia.scene7.com'],