diff --git a/.changeset/mighty-stingrays-pay.md b/.changeset/mighty-stingrays-pay.md new file mode 100644 index 000000000..f1b222387 --- /dev/null +++ b/.changeset/mighty-stingrays-pay.md @@ -0,0 +1,5 @@ +--- +"@headstartwp/core": minor +--- + +Introduces `SafeHtml` and `HtmlDecoder` components. diff --git a/docs/documentation/03- Utilities/sanitization.md b/docs/documentation/03- Utilities/sanitization.md new file mode 100644 index 000000000..53391e990 --- /dev/null +++ b/docs/documentation/03- Utilities/sanitization.md @@ -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('

some raw html

') }; +return
; +``` + +## 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

{stripTags('this is a title without a span')}

; +``` + +## 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'; + +

+ +

+``` + +## 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'; + + +``` + diff --git a/packages/core/src/dom/__tests__/stripTags.ts b/packages/core/src/dom/__tests__/stripTags.ts new file mode 100644 index 000000000..87e9d7052 --- /dev/null +++ b/packages/core/src/dom/__tests__/stripTags.ts @@ -0,0 +1,11 @@ +import { stripTags } from '../stripTags'; + +describe('stripTags', () => { + test('it strips tags', () => { + expect(stripTags('
test
')).toBe('test'); + expect(stripTags('
test

test

')).toBe('test test'); + expect(stripTags('

hello world

')).toBe( + 'alert()hello world', + ); + }); +}); diff --git a/packages/core/src/dom/index.ts b/packages/core/src/dom/index.ts index 8ba02a4c6..987e65661 100644 --- a/packages/core/src/dom/index.ts +++ b/packages/core/src/dom/index.ts @@ -342,3 +342,4 @@ export function isBlockByName(node: DOMNode, name: string) { } export * from './wpKsesPost'; +export * from './stripTags'; diff --git a/packages/core/src/dom/stripTags.ts b/packages/core/src/dom/stripTags.ts new file mode 100644 index 000000000..67e3dbb42 --- /dev/null +++ b/packages/core/src/dom/stripTags.ts @@ -0,0 +1,10 @@ +/** + * Utility functions to strip any tags + * + * @param html The html string + * + * @returns + */ +export function stripTags(html) { + return html.replace(/(<([^>]+)>)/gi, ''); +} diff --git a/packages/core/src/react/components/HtmlDecoder.tsx b/packages/core/src/react/components/HtmlDecoder.tsx new file mode 100644 index 000000000..c9f0996d8 --- /dev/null +++ b/packages/core/src/react/components/HtmlDecoder.tsx @@ -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 + * + * ``` + */ + 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 + * + * ``` + * + * @param props Component properties + * + * @category React Components + */ +export const HtmlDecoder: FC = ({ html }) => { + return <>{parse(stripTags(html))}; +}; diff --git a/packages/core/src/react/components/SafeHtml.tsx b/packages/core/src/react/components/SafeHtml.tsx new file mode 100644 index 000000000..76770fa7f --- /dev/null +++ b/packages/core/src/react/components/SafeHtml.tsx @@ -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 + * + * ``` + */ + html: string; + + /** + * The allow list for the parser + * + * ```jsx + * + * ``` + */ + 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 + * + * ``` + * + * @param props Component properties + * + * @category React Components + */ +export const SafeHtml: FC = ({ html, ksesAllowList }) => { + return <>{parse(wpKsesPost(html, ksesAllowList))}; +}; diff --git a/packages/core/src/react/components/__tests__/HtmlDecoder.tsx b/packages/core/src/react/components/__tests__/HtmlDecoder.tsx new file mode 100644 index 000000000..02d5bfae8 --- /dev/null +++ b/packages/core/src/react/components/__tests__/HtmlDecoder.tsx @@ -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( + , + ); + + expect(container.firstChild).toMatchInlineSnapshot( + `Hello world! – foo bar – ‘\\@£#?,’\\[]`, + ); + }); + + it('does not render arbitrary markup', () => { + const { container } = render(); + expect(container).toMatchInlineSnapshot(` +
+ This is a title +
+ `); + }); +}); diff --git a/packages/core/src/react/components/__tests__/SafeHtml.tsx b/packages/core/src/react/components/__tests__/SafeHtml.tsx new file mode 100644 index 000000000..c5b4b2972 --- /dev/null +++ b/packages/core/src/react/components/__tests__/SafeHtml.tsx @@ -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( + , + ); + + expect(container.firstChild).toMatchInlineSnapshot( + `Hello world! – foo bar – ‘\\@£#?,’\\[]`, + ); + }); + + it('renders arbitrary markup', () => { + const { container } = render(); + expect(container).toMatchInlineSnapshot(` +
+ This is a + + title + +
+ `); + }); +}); diff --git a/packages/core/src/react/components/index.ts b/packages/core/src/react/components/index.ts index b1e7b1f15..7e1f33709 100644 --- a/packages/core/src/react/components/index.ts +++ b/packages/core/src/react/components/index.ts @@ -1,2 +1,4 @@ export * from './BlocksRenderer'; export * from './Menu'; +export * from './SafeHtml'; +export * from './HtmlDecoder'; diff --git a/projects/wp-nextjs/src/components/PageContent.js b/projects/wp-nextjs/src/components/PageContent.js index e73308a20..3a35cea6c 100644 --- a/projects/wp-nextjs/src/components/PageContent.js +++ b/projects/wp-nextjs/src/components/PageContent.js @@ -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')); @@ -19,7 +20,9 @@ export const PageContent = ({ params }) => { return ( <> -

{data.post.title.rendered}

+

+ +

);