From 15f10c615b23bb7019cb1eba860e7c45e11411c1 Mon Sep 17 00:00:00 2001 From: Matchu Date: Wed, 28 Oct 2020 00:00:14 -0700 Subject: [PATCH] add descriptions to closet lists (formatted! :3) --- package.json | 2 + src/app/UserItemsPage.js | 80 ++++++++++++++++++++++++++++++++++++++++ src/server/types/User.js | 6 +++ yarn.lock | 20 ++++++++++ 4 files changed, 108 insertions(+) diff --git a/package.json b/package.json index 236123b..c663c1c 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "apollo-server-env": "^2.4.3", "aws-sdk": "^2.726.0", "dataloader": "^2.0.0", + "dompurify": "^2.2.0", "emotion": "^10.0.27", "graphql": "^15.0.0", "honeycomb-beeline": "^2.2.0", @@ -36,6 +37,7 @@ "react-router-dom": "^5.1.2", "react-scripts": "3.4.1", "react-transition-group": "^4.3.0", + "simple-markdown": "^0.7.2", "xmlrpc": "^1.3.2" }, "scripts": { diff --git a/src/app/UserItemsPage.js b/src/app/UserItemsPage.js index 0e02447..a9c112c 100644 --- a/src/app/UserItemsPage.js +++ b/src/app/UserItemsPage.js @@ -1,9 +1,12 @@ import React from "react"; +import { css } from "emotion"; import { Badge, Box, Center, Wrap, VStack } from "@chakra-ui/core"; import { CheckIcon, EmailIcon, StarIcon } from "@chakra-ui/icons"; import gql from "graphql-tag"; import { useParams } from "react-router-dom"; import { useQuery } from "@apollo/client"; +import SimpleMarkdown from "simple-markdown"; +import DOMPurify from "dompurify"; import HangerSpinner from "./components/HangerSpinner"; import { Heading1, Heading2, Heading3 } from "./util"; @@ -35,6 +38,7 @@ function UserItemsPage() { closetLists { id name + description ownsOrWantsItems isDefaultList items { @@ -237,6 +241,11 @@ function ClosetList({ closetList, isCurrentUser, showHeading }) { {closetList.name} )} + {closetList.description && ( + + {closetList.description} + + )} {sortedItems.length > 0 ? ( {sortedItems.map((item) => ( @@ -264,6 +273,77 @@ function ClosetList({ closetList, isCurrentUser, showHeading }) { ); } +const unsafeMarkdownRules = { + autolink: SimpleMarkdown.defaultRules.autolink, + br: SimpleMarkdown.defaultRules.br, + em: SimpleMarkdown.defaultRules.em, + escape: SimpleMarkdown.defaultRules.escape, + link: SimpleMarkdown.defaultRules.link, + list: SimpleMarkdown.defaultRules.list, + newline: SimpleMarkdown.defaultRules.newline, + paragraph: SimpleMarkdown.defaultRules.paragraph, + strong: SimpleMarkdown.defaultRules.strong, + u: SimpleMarkdown.defaultRules.u, + + // DANGER: We override Markdown's `text` rule to _not_ escape HTML. This is + // intentional, to allow users to embed some limited HTML. DOMPurify is + // responsible for sanitizing the HTML afterward. Do not use these rules + // without sanitizing!! + text: { + ...SimpleMarkdown.defaultRules.text, + html: (node) => node.content, + }, +}; +const markdownParser = SimpleMarkdown.parserFor(unsafeMarkdownRules); +const unsafeMarkdownOutput = SimpleMarkdown.htmlFor( + SimpleMarkdown.ruleOutput(unsafeMarkdownRules, "html") +); + +function MarkdownAndSafeHTML({ children }) { + const htmlAndMarkdown = children; + + const unsafeHtml = unsafeMarkdownOutput(markdownParser(htmlAndMarkdown)); + + const sanitizedHtml = DOMPurify.sanitize(unsafeHtml, { + ALLOWED_TAGS: [ + "b", + "i", + "u", + "strong", + "em", + "a", + "p", + "div", + "br", + "ol", + "ul", + "li", + ], + ALLOWED_ATTR: ["href", "class"], + // URL must either start with an approved host (external link), or with a + // slash or hash (internal link). + ALLOWED_URI_REGEXP: /^https?:\/\/(impress\.openneo\.net|impress-2020\.openneo\.net|www\.neopets\.com|neopets\.com)\/|^[\/#]/, + }); + + return ( + + ); +} + function NeopetsStarIcon(props) { // Converted from the Neopets favicon with https://www.vectorizer.io/. return ( diff --git a/src/server/types/User.js b/src/server/types/User.js index bdcfbd6..67072f0 100644 --- a/src/server/types/User.js +++ b/src/server/types/User.js @@ -21,6 +21,9 @@ const typeDefs = gql` id: ID! name: String + # A user-customized description. May contain Markdown and limited HTML. + description: String + # Whether this is a list of items they own, or items they want. ownsOrWantsItems: OwnsOrWants! @@ -158,6 +161,7 @@ const resolvers = { .map((closetList) => ({ id: closetList.id, name: closetList.name, + description: closetList.description, ownsOrWantsItems: closetList.hangersOwned ? "OWNS" : "WANTS", isDefaultList: false, items: allClosetHangers @@ -169,6 +173,7 @@ const resolvers = { closetListNodes.push({ id: `user-${id}-default-list-OWNS`, name: "Not in a list", + description: null, ownsOrWantsItems: "OWNS", isDefaultList: true, items: allClosetHangers @@ -181,6 +186,7 @@ const resolvers = { closetListNodes.push({ id: `user-${id}-default-list-WANTS`, name: "Not in a list", + description: null, ownsOrWantsItems: "WANTS", isDefaultList: true, items: allClosetHangers diff --git a/yarn.lock b/yarn.lock index 2604300..e897f9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4694,6 +4694,14 @@ "@types/prop-types" "*" csstype "^3.0.2" +"@types/react@>=16.0.0": + version "16.9.54" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.54.tgz#140280c31825287ee74e9da95285b91c5a2e471d" + integrity sha512-GhawhYraQZpGFO2hVMArjPrYbnA/6+DS8SubK8IPhhVClmKqANihsRenOm5E0mvqK0m/BKoqVktA1O1+Xvlz9w== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + "@types/reactcss@*": version "1.2.3" resolved "https://registry.yarnpkg.com/@types/reactcss/-/reactcss-1.2.3.tgz#af28ae11bbb277978b99d04d1eedfd068ca71834" @@ -8327,6 +8335,11 @@ domhandler@^2.3.0: dependencies: domelementtype "1" +dompurify@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.2.0.tgz#51d34e76faa38b5d6b4e83a0678530f27fe3965c" + integrity sha512-bqFOQ7XRmmozp0VsKdIEe8UwZYxj0yttz7l80GBtBqdVRY48cOpXH2J/CVO7AEkV51qY0EBVXfilec18mdmQ/w== + domutils@1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" @@ -16302,6 +16315,13 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= +simple-markdown@^0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/simple-markdown/-/simple-markdown-0.7.2.tgz#896cc3e3dd9acd068d30e696bce70b0b97655665" + integrity sha512-XfCvqqzMyzRj4L7eIxJgGaQ2Gaxr20GhTFMB+1yuY8q3xffjzmOg4Q5tC0kcaJPV42NNUHCQDaRK6jzi3/RhrA== + dependencies: + "@types/react" ">=16.0.0" + simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"