Skip to content

Commit

Permalink
Introducing SafeHtml and HtmlDecoder component (#548)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicholasio authored Jul 7, 2023
1 parent 43e6dc5 commit df3e65c
Show file tree
Hide file tree
Showing 11 changed files with 220 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/mighty-stingrays-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@headstartwp/core": minor
---

Introduces `SafeHtml` and `HtmlDecoder` components.
57 changes: 57 additions & 0 deletions docs/documentation/03- Utilities/sanitization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
slug: /utilities/sanitization
sidebar_label: Escaping & Sanitization
---

# Escaping & Sanitization
As you're probably aware, React won't render raw HTML by default. If you want to do so you must use [dangerouslySetInnerHTML](https://react.dev/reference/react-dom/components/common#dangerously-setting-the-inner-html).

This page describes some of the utility functions and components provided by the framework to help with escaping & sanitization when rendering raw markup.

## wpKsesPost

This function sanitizes HTML content with requirements similar to [wp_kses_post](https://developer.wordpress.org/reference/functions/wp_kses_post/). If you are rendering arbitrary HTML markup you should probably run the markup through this function first.

```jsx
import { wpKsesPost } from '@headstartwp/core';

const markup = { __html: wpKsesPost('<p>some raw html</p>') };
return <div dangerouslySetInnerHTML={markup} />;
```

## stripTags

This function simply strips any html tags from a string. This can be useful in contexts where you don't want any HTML to be rendered.

```jsx
import { stripTags } from '@headstartwp/core';

return <h1>{stripTags('this is a title <span>without a span</span>')}</h1>;
```

## BlocksRenderer

When using [BlocksRenderer](/learn/gutenberg/rendering-blocks) your markup already goes through `wpKsesPost` so there's nothing else you need to worry about.

## HtmlDecoder

Sometimes you might just want to decode some HTML entities without actually rendering any HTML tags. For this purpose you can use the `HtmlDecoder` component.

```jsx
import { HtmlDecoder } from '@headstartwp/core/react';

<h1>
<HtmlDecoder html="Hello world! &#8211; foo bar &#8211;"/>
</h1>
```

## SafeHtml

The `SafeHtml` component provides an easy way to safely render HTML markup. It runs the markup through `wpKsesPost` just like `BlocksRenderer`.

```jsx
import { SafeHtml } from '@headstartwp/core/react';

<SafeHtml html="<div><p>hello world</p> div content</div>">
```

11 changes: 11 additions & 0 deletions packages/core/src/dom/__tests__/stripTags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { stripTags } from '../stripTags';

describe('stripTags', () => {
test('it strips tags', () => {
expect(stripTags('<div>test</div>')).toBe('test');
expect(stripTags('<div>test <p>test</p></div>')).toBe('test test');
expect(stripTags('<div><p><script>alert()</script>hello world</p></div>')).toBe(
'alert()hello world',
);
});
});
1 change: 1 addition & 0 deletions packages/core/src/dom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,3 +342,4 @@ export function isBlockByName(node: DOMNode, name: string) {
}

export * from './wpKsesPost';
export * from './stripTags';
10 changes: 10 additions & 0 deletions packages/core/src/dom/stripTags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Utility functions to strip any tags
*
* @param html The html string
*
* @returns
*/
export function stripTags(html) {
return html.replace(/(<([^>]+)>)/gi, '');
}
33 changes: 33 additions & 0 deletions packages/core/src/react/components/HtmlDecoder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { FC } from 'react';
import parse from 'html-react-parser';
import { stripTags } from '../../dom';

export interface HtmlDecodeProps {
/**
* The string with html entities to decode
*
* ```jsx
* <HtmlDecoder value="Hello world! &#8211; foo bar &#8211;" />
* ```
*/
html: string;
}

/**
* The `HtmlDecoder` simply decodes html entities
*
* Any actual html markup gets stripped before decoding html entities. If you need to render HTML use {@link SafeHtml}
*
* ## Usage
*
* ```jsx
* <HtmlDecoder html="Hello world! &#8211; foo bar &#8211;" />
* ```
*
* @param props Component properties
*
* @category React Components
*/
export const HtmlDecoder: FC<HtmlDecodeProps> = ({ html }) => {
return <>{parse(stripTags(html))}</>;
};
46 changes: 46 additions & 0 deletions packages/core/src/react/components/SafeHtml.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { FC } from 'react';
import parse from 'html-react-parser';
import type { IWhiteList } from 'xss';
import { wpKsesPost } from '../../dom';

export interface SafeHtmlProps {
/**
* The HTML string to be rendered.
*
* ```jsx
* <SafeHtml html="<div><p>hello world</p> div content</div>" />
* ```
*/
html: string;

/**
* The allow list for the parser
*
* ```jsx
* <SafeHtml
* html="<div><p>hello world</p> div content</div>"
* ksesAllowList={{ div: [] }}
* />
* ```
*/
ksesAllowList?: IWhiteList;
}

/**
* The `SafeHtml` component provides an easy way to safely render HTML
*
* The html prop is sanitized through {@link wpKsesPost} so it's safe for rendering arbitrary html markup.
*
* ## Usage
*
* ```jsx
* <SafeHtml html="<div><p>hello world</p> div content</div>" />
* ```
*
* @param props Component properties
*
* @category React Components
*/
export const SafeHtml: FC<SafeHtmlProps> = ({ html, ksesAllowList }) => {
return <>{parse(wpKsesPost(html, ksesAllowList))}</>;
};
24 changes: 24 additions & 0 deletions packages/core/src/react/components/__tests__/HtmlDecoder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from 'react';
import { render } from '@testing-library/react';
import { HtmlDecoder } from '../HtmlDecoder';

describe('HtmlDecoder', () => {
it('decodes entities', () => {
const { container } = render(
<HtmlDecoder html="Hello world! &#8211; foo bar &#8211; &#8216;\@£#?,&#8217;\[]" />,
);

expect(container.firstChild).toMatchInlineSnapshot(
`Hello world! – foo bar – ‘\\@£#?,’\\[]`,
);
});

it('does not render arbitrary markup', () => {
const { container } = render(<HtmlDecoder html="This is a <span>title</span>" />);
expect(container).toMatchInlineSnapshot(`
<div>
This is a title
</div>
`);
});
});
27 changes: 27 additions & 0 deletions packages/core/src/react/components/__tests__/SafeHtml.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from 'react';
import { render } from '@testing-library/react';
import { SafeHtml } from '../SafeHtml';

describe('SafeHtml', () => {
it('renders entities', () => {
const { container } = render(
<SafeHtml html="Hello world! &#8211; foo bar &#8211; &#8216;\@£#?,&#8217;\[]" />,
);

expect(container.firstChild).toMatchInlineSnapshot(
`Hello world! – foo bar – ‘\\@£#?,’\\[]`,
);
});

it('renders arbitrary markup', () => {
const { container } = render(<SafeHtml html="This is a <span>title</span>" />);
expect(container).toMatchInlineSnapshot(`
<div>
This is a
<span>
title
</span>
</div>
`);
});
});
2 changes: 2 additions & 0 deletions packages/core/src/react/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './BlocksRenderer';
export * from './Menu';
export * from './SafeHtml';
export * from './HtmlDecoder';
5 changes: 4 additions & 1 deletion projects/wp-nextjs/src/components/PageContent.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import { usePost } from '@headstartwp/next';
import dynamic from 'next/dynamic';
import { HtmlDecoder } from '@headstartwp/core/react';

const Blocks = dynamic(() => import('./Blocks'));

Expand All @@ -19,7 +20,9 @@ export const PageContent = ({ params }) => {

return (
<>
<h1>{data.post.title.rendered}</h1>
<h1>
<HtmlDecoder html={data.post.title.rendered} />
</h1>
<Blocks html={data.post.content.rendered} />
</>
);
Expand Down

1 comment on commit df3e65c

@vercel
Copy link

@vercel vercel bot commented on df3e65c Jul 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.