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