Skip to content

Commit

Permalink
Escape Editable HTML (#17994)
Browse files Browse the repository at this point in the history
* Escape HTML: Always Escape Ampersand

* Revert "Escape HTML: Always Escape Ampersand"

This reverts commit 70dfa53.

* Escape HTML for Editable Text

* Revert PlainText changes

* Adjust code block escape util

* Update package lock

* Add test case for escapeEditableHTML

* Update docs
  • Loading branch information
ellatrix authored Nov 7, 2019
1 parent 03b1e80 commit ac9a5a6
Show file tree
Hide file tree
Showing 10 changed files with 57 additions and 113 deletions.
5 changes: 3 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/block-library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@wordpress/deprecated": "file:../deprecated",
"@wordpress/editor": "file:../editor",
"@wordpress/element": "file:../element",
"@wordpress/escape-html": "file:../escape-html",
"@wordpress/i18n": "file:../i18n",
"@wordpress/is-shallow-equal": "file:../is-shallow-equal",
"@wordpress/keycodes": "file:../keycodes",
Expand Down
5 changes: 2 additions & 3 deletions packages/block-library/src/code/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ import { __ } from '@wordpress/i18n';
* Internal dependencies
*/
import { PlainText } from '@wordpress/block-editor';
import { escape, unescape } from './utils';

export default function CodeEdit( { attributes, setAttributes, className } ) {
return (
<div className={ className }>
<PlainText
value={ unescape( attributes.content ) }
onChange={ ( content ) => setAttributes( { content: escape( content ) } ) }
value={ attributes.content }
onChange={ ( content ) => setAttributes( { content } ) }
placeholder={ __( 'Write code…' ) }
aria-label={ __( 'Code' ) }
/>
Expand Down
7 changes: 6 additions & 1 deletion packages/block-library/src/code/save.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* Internal dependencies
*/
import { escape } from './utils';

export default function save( { attributes } ) {
return <pre><code>{ attributes.content }</code></pre>;
return <pre><code>{ escape( attributes.content ) }</code></pre>;
}
41 changes: 1 addition & 40 deletions packages/block-library/src/code/test/utils.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
/**
* Internal dependencies
*/
import { escape, unescape } from '../utils';
import { escape } from '../utils';

describe( 'core/code', () => {
describe( 'escape()', () => {
it( 'should escape ampersands', () => {
const text = escape( '&' );
expect( text ).toBe( '&amp;' );
} );

it( 'should escape opening square brackets', () => {
const text = escape( '[shortcode][/shortcode]' );
expect( text ).toBe( '&#91;shortcode]&#91;/shortcode]' );
Expand All @@ -24,39 +19,5 @@ describe( 'core/code', () => {
const text = escape( 'Text https://example.com/test/' );
expect( text ).toBe( 'Text https://example.com/test/' );
} );

it( 'should escape ampersands last', () => {
const text = escape( '[shortcode][/shortcode]' );
expect( text ).toBe( '&#91;shortcode]&#91;/shortcode]' );
expect( text ).not.toBe( '&amp;#91;shortcode]&amp;#91;/shortcode]' );
} );
} );

describe( 'unescape()', () => {
it( 'should unescape escaped ampersands', () => {
const text = unescape( '&amp;' );
expect( text ).toBe( '&' );
} );

it( 'should unescape escaped opening square brackets', () => {
const text = unescape( '&#91;shortcode]&#91;/shortcode]' );
expect( text ).toBe( '[shortcode][/shortcode]' );
} );

it( 'should unescape the escaped protocol of an isolated url', () => {
const text = unescape( 'https:&#47;&#47;example.com/test/' );
expect( text ).toBe( 'https://example.com/test/' );
} );

it( 'should revert the result of escape()', () => {
const ampersand = unescape( escape( '&' ) );
expect( ampersand ).toBe( '&' );

const squareBracket = unescape( escape( '[shortcode][/shortcode]' ) );
expect( squareBracket ).toBe( '[shortcode][/shortcode]' );

const url = unescape( escape( 'https://example.com/test/' ) );
expect( url ).toBe( 'https://example.com/test/' );
} );
} );
} );
71 changes: 6 additions & 65 deletions packages/block-library/src/code/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
*/
import { flow } from 'lodash';

/**
* WordPress dependencies
*/
import { escapeEditableHTML } from '@wordpress/escape-html';

/**
* Escapes ampersands, shortcodes, and links.
*
Expand All @@ -11,49 +16,12 @@ import { flow } from 'lodash';
*/
export function escape( content ) {
return flow(
escapeAmpersands,
escapeEditableHTML,
escapeOpeningSquareBrackets,
escapeProtocolInIsolatedUrls
)( content || '' );
}

/**
* Unescapes escaped ampersands, shortcodes, and links.
*
* @param {string} content Content with (maybe) escaped ampersands, shortcodes, and links.
* @return {string} The given content with escaped characters unescaped.
*/
export function unescape( content ) {
return flow(
unescapeProtocolInIsolatedUrls,
unescapeOpeningSquareBrackets,
unescapeAmpersands
)( content || '' );
}

/**
* Returns the given content with all its ampersand characters converted
* into their HTML entity counterpart (i.e. & => &amp;)
*
* @param {string} content The content of a code block.
* @return {string} The given content with its ampersands converted into
* their HTML entity counterpart (i.e. & => &amp;)
*/
function escapeAmpersands( content ) {
return content.replace( /&/g, '&amp;' );
}

/**
* Returns the given content with all &amp; HTML entities converted into &.
*
* @param {string} content The content of a code block.
* @return {string} The given content with all &amp; HTML entities
* converted into &.
*/
function unescapeAmpersands( content ) {
return content.replace( /&amp;/g, '&' );
}

/**
* Returns the given content with all opening shortcode characters converted
* into their HTML entity counterpart (i.e. [ => &#91;). For instance, a
Expand All @@ -71,16 +39,6 @@ function escapeOpeningSquareBrackets( content ) {
return content.replace( /\[/g, '&#91;' );
}

/**
* Returns the given content translating all &#91; into [.
*
* @param {string} content The content of a code block.
* @return {string} The given content with all &#91; into [.
*/
function unescapeOpeningSquareBrackets( content ) {
return content.replace( /&#91;/g, '[' );
}

/**
* Converts the first two forward slashes of any isolated URL into their HTML
* counterparts (i.e. // => &#47;&#47;). For instance, https://youtube.com/watch?x
Expand All @@ -98,20 +56,3 @@ function unescapeOpeningSquareBrackets( content ) {
function escapeProtocolInIsolatedUrls( content ) {
return content.replace( /^(\s*https?:)\/\/([^\s<>"]+\s*)$/m, '$1&#47;&#47;$2' );
}

/**
* Converts the first two forward slashes of any isolated URL from the HTML entity
* &#73; into /.
*
* An isolated URL is a URL that sits in its own line, surrounded only by spacing
* characters.
*
* See https://github.com/WordPress/wordpress-develop/blob/5.1.1/src/wp-includes/class-wp-embed.php#L403
*
* @param {string} content The content of a code block.
* @return {string} The given content with the first two forward slashes of any
* isolated URL from the HTML entity &#73; into /.
*/
function unescapeProtocolInIsolatedUrls( content ) {
return content.replace( /^(\s*https?:)&#47;&#47;([^\s<>"]+\s*)$/m, '$1//$2' );
}
14 changes: 14 additions & 0 deletions packages/escape-html/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@ _Returns_

- `string`: Escaped attribute value.

<a name="escapeEditableHTML" href="#escapeEditableHTML">#</a> **escapeEditableHTML**

Returns an escaped Editable HTML element value. This is different from
`escapeHTML`, because for editable HTML, ALL ampersands must be escaped in
order to render the content correctly on the page.

_Parameters_

- _value_ `string`: Element value.

_Returns_

- `string`: Escaped HTML element value.

<a name="escapeHTML" href="#escapeHTML">#</a> **escapeHTML**

Returns an escaped HTML element value.
Expand Down
13 changes: 13 additions & 0 deletions packages/escape-html/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,19 @@ export function escapeHTML( value ) {
return escapeLessThan( escapeAmpersand( value ) );
}

/**
* Returns an escaped Editable HTML element value. This is different from
* `escapeHTML`, because for editable HTML, ALL ampersands must be escaped in
* order to render the content correctly on the page.
*
* @param {string} value Element value.
*
* @return {string} Escaped HTML element value.
*/
export function escapeEditableHTML( value ) {
return escapeLessThan( value.replace( /&/g, '&amp;' ) );
}

/**
* Returns true if the given attribute name is valid, or false otherwise.
*
Expand Down
9 changes: 9 additions & 0 deletions packages/escape-html/src/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
escapeAttribute,
escapeHTML,
isValidAttributeName,
escapeEditableHTML,
} from '../';
import __unstableEscapeGreaterThan from '../escape-greater';

Expand Down Expand Up @@ -94,3 +95,11 @@ describe( 'isValidAttributeName', () => {
expect( result ).toBe( true );
} );
} );

describe( 'escapeEditableHTML', () => {
it( 'should escape < and all ampersands', () => {
const result = escapeEditableHTML( '<a href="https://w.org">WP</a> & &lt;strong&gt;' );

expect( result ).toBe( '&lt;a href="https://w.org">WP&lt;/a> &amp; &amp;lt;strong&amp;gt;' );
} );
} );
4 changes: 2 additions & 2 deletions packages/rich-text/src/to-html-string.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

import {
escapeHTML,
escapeEditableHTML,
escapeAttribute,
isValidAttributeName,
} from '@wordpress/escape-html';
Expand Down Expand Up @@ -106,6 +106,6 @@ function createElementHTML( { type, attributes, object, children } ) {

function createChildrenHTML( children = [] ) {
return children.map( ( child ) => {
return child.text === undefined ? createElementHTML( child ) : escapeHTML( child.text );
return child.text === undefined ? createElementHTML( child ) : escapeEditableHTML( child.text );
} ).join( '' );
}

0 comments on commit ac9a5a6

Please sign in to comment.