Skip to content
Snippets Groups Projects
Commit 48670c46 authored by xaralis's avatar xaralis
Browse files

feat: markdown XSS protection, better dropdown UX

parent 454ce8a2
No related branches found
No related tags found
No related merge requests found
...@@ -4458,6 +4458,11 @@ ...@@ -4458,6 +4458,11 @@
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="
}, },
"cssfilter": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz",
"integrity": "sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4="
},
"cssnano": { "cssnano": {
"version": "4.1.10", "version": "4.1.10",
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz", "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz",
...@@ -14977,6 +14982,15 @@ ...@@ -14977,6 +14982,15 @@
"@babel/runtime-corejs3": "^7.8.3" "@babel/runtime-corejs3": "^7.8.3"
} }
}, },
"xss": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/xss/-/xss-1.0.8.tgz",
"integrity": "sha512-3MgPdaXV8rfQ/pNn16Eio6VXYPTkqwa0vc7GkiymmY/DqR1SE/7VPAAVZz1GJsJFrllMYO3RHfEaiUGjab6TNw==",
"requires": {
"commander": "^2.20.3",
"cssfilter": "0.0.10"
}
},
"xtend": { "xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
......
...@@ -21,7 +21,8 @@ ...@@ -21,7 +21,8 @@
"react-scripts": "3.4.3", "react-scripts": "3.4.3",
"showdown": "^1.9.1", "showdown": "^1.9.1",
"unfetch": "^4.2.0", "unfetch": "^4.2.0",
"wait-queue": "^1.1.4" "wait-queue": "^1.1.4",
"xss": "^1.0.8"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
...@@ -58,7 +59,7 @@ ...@@ -58,7 +59,7 @@
"^@?\\w" "^@?\\w"
], ],
[ [
"^(api|actions|config|hooks|components|containers|pages|utils|stores|keycloak|ws)(/.*|$)" "^(api|actions|config|hooks|components|containers|pages|utils|stores|keycloak|markdown|ws)(/.*|$)"
], ],
[ [
"^(test-utils)(/.*|$)" "^(test-utils)(/.*|$)"
......
...@@ -95,7 +95,11 @@ const Announcement = ({ ...@@ -95,7 +95,11 @@ const Announcement = ({
)} )}
</div> </div>
{canRunActions && ( {canRunActions && (
<DropdownMenu right triggerIconClass="ico--dots-three-horizontal"> <DropdownMenu
right
className="pl-4"
triggerIconClass="ico--dots-three-horizontal"
>
{showEdit && ( {showEdit && (
<DropdownMenuItem <DropdownMenuItem
onClick={onEdit} onClick={onEdit}
......
import React, { useState } from "react"; import React, { useState } from "react";
import ReactMde from "react-mde"; import ReactMde from "react-mde";
import classNames from "classnames"; import classNames from "classnames";
import Showdown from "showdown";
import { markdownConverter } from "markdown";
import "react-mde/lib/styles/css/react-mde-toolbar.css"; import "react-mde/lib/styles/css/react-mde-toolbar.css";
import "./MarkdownEditor.css"; import "./MarkdownEditor.css";
const converter = new Showdown.Converter({
tables: true,
simplifiedAutoLink: true,
strikethrough: true,
tasklists: true,
});
const MarkdownEditor = ({ const MarkdownEditor = ({
value, value,
onChange, onChange,
...@@ -47,7 +41,7 @@ const MarkdownEditor = ({ ...@@ -47,7 +41,7 @@ const MarkdownEditor = ({
selectedTab={selectedTab} selectedTab={selectedTab}
onTabChange={setSelectedTab} onTabChange={setSelectedTab}
generateMarkdownPreview={(markdown) => generateMarkdownPreview={(markdown) =>
Promise.resolve(converter.makeHtml(markdown)) Promise.resolve(markdownConverter.makeHtml(markdown))
} }
classes={classes} classes={classes}
l18n={l18n} l18n={l18n}
......
...@@ -135,6 +135,10 @@ const Post = ({ ...@@ -135,6 +135,10 @@ const Post = ({
const showHideAction = !archived; const showHideAction = !archived;
const showArchiveAction = !archived; const showArchiveAction = !archived;
const htmlContent = {
__html: content,
};
return ( return (
<div className={wrapperClassName} ref={ref}> <div className={wrapperClassName} ref={ref}>
<img <img
...@@ -164,7 +168,7 @@ const Post = ({ ...@@ -164,7 +168,7 @@ const Post = ({
{labels} {labels}
</div> </div>
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center">
<Thumbs <Thumbs
likes={ranking.likes} likes={ranking.likes}
dislikes={ranking.dislikes} dislikes={ranking.dislikes}
...@@ -174,7 +178,7 @@ const Post = ({ ...@@ -174,7 +178,7 @@ const Post = ({
myVote={ranking.myVote} myVote={ranking.myVote}
/> />
{canRunActions && ( {canRunActions && (
<DropdownMenu right> <DropdownMenu right className="pl-4">
{showAnnounceAction && ( {showAnnounceAction && (
<DropdownMenuItem <DropdownMenuItem
onClick={onAnnounceProcedureProposal} onClick={onAnnounceProcedureProposal}
...@@ -239,9 +243,10 @@ const Post = ({ ...@@ -239,9 +243,10 @@ const Post = ({
<div className="flex lg:hidden flex-row flex-wrap my-2 space-x-2"> <div className="flex lg:hidden flex-row flex-wrap my-2 space-x-2">
{labels} {labels}
</div> </div>
<p className="text-sm lg:text-base text-black leading-normal"> <div
{content} className="text-sm lg:text-base text-black leading-normal content-block"
</p> dangerouslySetInnerHTML={htmlContent}
></div>
</div> </div>
</div> </div>
); );
......
...@@ -56,7 +56,7 @@ const PostList = ({ ...@@ -56,7 +56,7 @@ const PostList = ({
author={item.author} author={item.author}
type={item.type} type={item.type}
state={item.state} state={item.state}
content={item.content} content={item.contentHtml}
ranking={item.ranking} ranking={item.ranking}
historyLog={item.historyLog} historyLog={item.historyLog}
modified={item.modified} modified={item.modified}
......
import Showdown from "showdown";
import xss from "xss";
const xssFilter = (converter) => [
{
type: "output",
filter: (text) => xss(text),
},
];
export const markdownConverter = new Showdown.Converter({
tables: true,
simplifiedAutoLink: true,
strikethrough: true,
tasklists: true,
omitExtraWLInCodeBlocks: true,
noHeaderId: true,
headerLevelStart: 2,
openLinksInNewWindow: true,
extensions: [xssFilter],
});
...@@ -151,7 +151,7 @@ const Home = () => { ...@@ -151,7 +151,7 @@ const Home = () => {
<h1 className="head-alt-md lg:head-alt-lg mb-0"> <h1 className="head-alt-md lg:head-alt-lg mb-0">
Bod č. {programEntry.number}: {programEntry.title} Bod č. {programEntry.number}: {programEntry.title}
</h1> </h1>
<DropdownMenu right triggerSize="lg"> <DropdownMenu right triggerSize="lg" className="pl-4">
<DropdownMenuItem <DropdownMenuItem
onClick={() => setShowProgramEditModal(true)} onClick={() => setShowProgramEditModal(true)}
icon="ico--edit-pencil" icon="ico--edit-pencil"
......
...@@ -3,6 +3,8 @@ import pick from "lodash/pick"; ...@@ -3,6 +3,8 @@ import pick from "lodash/pick";
import property from "lodash/property"; import property from "lodash/property";
import values from "lodash/values"; import values from "lodash/values";
import { markdownConverter } from "markdown";
/** /**
* Filter & sort collection of posts. * Filter & sort collection of posts.
* @param {CF2021.PostStoreFilters} filters * @param {CF2021.PostStoreFilters} filters
...@@ -120,6 +122,7 @@ export const announcementTypeMappingRev = { ...@@ -120,6 +122,7 @@ export const announcementTypeMappingRev = {
export const parseRawPost = (rawPost) => { export const parseRawPost = (rawPost) => {
const post = { const post = {
...pick(rawPost, ["id", "content", "author"]), ...pick(rawPost, ["id", "content", "author"]),
contentHtml: markdownConverter.makeHtml(rawPost.content),
datetime: new Date(rawPost.datetime), datetime: new Date(rawPost.datetime),
historyLog: rawPost.history_log, historyLog: rawPost.history_log,
ranking: { ranking: {
......
import has from "lodash/has"; import has from "lodash/has";
import { markdownConverter } from "markdown";
import { PostStore } from "stores"; import { PostStore } from "stores";
import { parseRawPost, postsStateMapping, updateWindowPosts } from "utils"; import { parseRawPost, postsStateMapping, updateWindowPosts } from "utils";
...@@ -24,6 +25,9 @@ export const handlePostChanged = (payload) => { ...@@ -24,6 +25,9 @@ export const handlePostChanged = (payload) => {
if (state.items[payload.id]) { if (state.items[payload.id]) {
if (has(payload, "content")) { if (has(payload, "content")) {
state.items[payload.id].content = payload.content; state.items[payload.id].content = payload.content;
state.items[payload.id].contentHtml = markdownConverter.makeHtml(
payload.content
);
state.items[payload.id].modified = true; state.items[payload.id].modified = true;
} }
......
...@@ -80,6 +80,7 @@ declare namespace CF2021 { ...@@ -80,6 +80,7 @@ declare namespace CF2021 {
}; };
type: PostType; type: PostType;
content: string; content: string;
contentHtml: string;
ranking: { ranking: {
score: number; score: number;
likes: number; likes: number;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment