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"