Skip to content

Commit

Permalink
Add iframe raw handling (e.g. Google Maps) (#4660)
Browse files Browse the repository at this point in the history
* Allow pasting iframes (squashed 7 commits)

* Polish
  • Loading branch information
ellatrix authored Feb 8, 2018
1 parent abd2ab8 commit 13b181b
Show file tree
Hide file tree
Showing 12 changed files with 139 additions and 37 deletions.
36 changes: 36 additions & 0 deletions blocks/api/raw-handling/embedded-content-reducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Internal dependencies
*/
import { isEmbedded } from './utils';

/**
* Browser dependencies
*/
const { ELEMENT_NODE } = window.Node;

/**
* This filter takes embedded content out of paragraphs.
*
* @param {Node} node The node to filter.
*
* @return {void}
*/
export default function( node ) {
if ( node.nodeType !== ELEMENT_NODE ) {
return;
}

if ( ! isEmbedded( node ) ) {
return;
}

let wrapper = node;

while ( wrapper && wrapper.nodeName !== 'P' ) {
wrapper = wrapper.parentElement;
}

if ( wrapper ) {
wrapper.parentNode.insertBefore( node, wrapper );
}
}
27 changes: 15 additions & 12 deletions blocks/api/raw-handling/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { find, get } from 'lodash';
import { find, get, compact } from 'lodash';
import showdown from 'showdown';

/**
Expand All @@ -22,24 +22,25 @@ import imageCorrector from './image-corrector';
import blockquoteNormaliser from './blockquote-normaliser';
import tableNormaliser from './table-normaliser';
import inlineContentConverter from './inline-content-converter';
import embeddedContentReducer from './embedded-content-reducer';
import { deepFilterHTML, isInvalidInline, isNotWhitelisted, isPlain, isInline } from './utils';
import shortcodeConverter from './shortcode-converter';

/**
* Converts an HTML string to known blocks. Strips everything else.
*
* @param {string} [options.HTML] The HTML to convert.
* @param {string} [options.plainText] Plain text version.
* @param {string} [options.mode] Handle content as blocks or inline content.
* * 'AUTO': Decide based on the content passed.
* * 'INLINE': Always handle as inline content, and return string.
* * 'BLOCKS': Always handle as blocks, and return array of blocks.
* @param {Array} [options.tagName] The tag into which content will be
* inserted.
* @param {string} [options.HTML] The HTML to convert.
* @param {string} [options.plainText] Plain text version.
* @param {string} [options.mode] Handle content as blocks or inline content.
* * 'AUTO': Decide based on the content passed.
* * 'INLINE': Always handle as inline content, and return string.
* * 'BLOCKS': Always handle as blocks, and return array of blocks.
* @param {Array} [options.tagName] The tag into which content will be inserted.
* @param {boolean} [options.canUserUseUnfilteredHTML] Whether or not to user can use unfiltered HTML.
*
* @return {Array|string} A list of blocks or a string, depending on `handlerMode`.
*/
export default function rawHandler( { HTML, plainText = '', mode = 'AUTO', tagName } ) {
export default function rawHandler( { HTML, plainText = '', mode = 'AUTO', tagName, canUserUseUnfilteredHTML = false } ) {
// First of all, strip any meta tags.
HTML = HTML.replace( /<meta[^>]+>/, '' );

Expand Down Expand Up @@ -112,18 +113,20 @@ export default function rawHandler( { HTML, plainText = '', mode = 'AUTO', tagNa
msListConverter,
] );

piece = deepFilterHTML( piece, [
piece = deepFilterHTML( piece, compact( [
listReducer,
imageCorrector,
// Add semantic formatting before attributes are stripped.
formattingTransformer,
stripAttributes,
commentRemover,
! canUserUseUnfilteredHTML && createUnwrapper( ( element ) => element.nodeName === 'IFRAME' ),
embeddedContentReducer,
createUnwrapper( isNotWhitelisted ),
blockquoteNormaliser,
tableNormaliser,
inlineContentConverter,
] );
] ) );

piece = deepFilterHTML( piece, [
createUnwrapper( isInvalidInline ),
Expand Down
12 changes: 12 additions & 0 deletions blocks/api/raw-handling/test/embedded-content-corrector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Internal dependencies
*/
import embeddedContentReducer from '../embedded-content-reducer';
import { deepFilterHTML } from '../utils';

describe( 'embeddedContentReducer', () => {
it( 'should move embedded content from paragraph', () => {
expect( deepFilterHTML( '<p><strong>test<img class="one"></strong><img class="two"></p>', [ embeddedContentReducer ] ) )
.toEqual( '<img class="one"><img class="two"><p><strong>test</strong></p>' );
} );
} );
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<iframe src="https://www.google.com/maps/embed" width="600" height="450" frameborder="0" style="border:0" allowfullscreen></iframe>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<!-- wp:html -->
<iframe src="https://www.google.com/maps/embed" width="600" height="450" allowfullscreen=""></iframe>
<!-- /wp:html -->
3 changes: 2 additions & 1 deletion blocks/api/raw-handling/test/integration/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const types = [
'ms-word',
'ms-word-online',
'evernote',
'iframe-embed',
];

describe( 'raw handling: integration', () => {
Expand All @@ -32,7 +33,7 @@ describe( 'raw handling: integration', () => {
it( type, () => {
const input = fs.readFileSync( path.join( __dirname, `${ type }-in.html` ), 'utf8' ).trim();
const output = fs.readFileSync( path.join( __dirname, `${ type }-out.html` ), 'utf8' ).trim();
const converted = rawHandler( { HTML: input } );
const converted = rawHandler( { HTML: input, canUserUseUnfilteredHTML: true } );
const serialized = typeof converted === 'string' ? converted : serialize( converted );

equal( output, serialized );
Expand Down
28 changes: 23 additions & 5 deletions blocks/api/raw-handling/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ const inlineWhitelist = {
br: {},
};

const embeddedWhiteList = {
img: { attributes: [ 'src', 'alt' ], classes: [ 'alignleft', 'aligncenter', 'alignright', 'alignnone' ] },
iframe: { attributes: [ 'src', 'allowfullscreen', 'height', 'width' ] },
};

const inlineWrapperWhiteList = {
figcaption: {},
h1: {},
Expand All @@ -39,7 +44,7 @@ const inlineWrapperWhiteList = {
h4: {},
h5: {},
h6: {},
p: { children: [ 'img' ] },
p: {},
li: { children: [ 'ul', 'ol', 'li' ] },
pre: {},
td: {},
Expand All @@ -49,7 +54,7 @@ const inlineWrapperWhiteList = {
const whitelist = {
...inlineWhitelist,
...inlineWrapperWhiteList,
img: { attributes: [ 'src', 'alt' ], classes: [ 'alignleft', 'aligncenter', 'alignright', 'alignnone' ] },
...embeddedWhiteList,
figure: {},
blockquote: {},
hr: {},
Expand All @@ -63,7 +68,7 @@ const whitelist = {
};

export function isWhitelisted( element ) {
return !! whitelist[ element.nodeName.toLowerCase() ];
return whitelist.hasOwnProperty( element.nodeName.toLowerCase() );
}

export function isNotWhitelisted( element ) {
Expand Down Expand Up @@ -99,7 +104,7 @@ function isInlineForTag( nodeName, tagName ) {

export function isInline( node, tagName ) {
const nodeName = node.nodeName.toLowerCase();
return !! inlineWhitelist[ nodeName ] || isInlineForTag( nodeName, tagName );
return inlineWhitelist.hasOwnProperty( nodeName ) || isInlineForTag( nodeName, tagName );
}

export function isClassWhitelisted( tag, name ) {
Expand All @@ -110,8 +115,21 @@ export function isClassWhitelisted( tag, name ) {
);
}

/**
* Whether or not the given node is embedded content.
*
* @see https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories#Embedded_content
*
* @param {Node} node The node to check.
*
* @return {boolean} True if embedded content, false if not.
*/
export function isEmbedded( node ) {
return embeddedWhiteList.hasOwnProperty( node.nodeName.toLowerCase() );
}

export function isInlineWrapper( node ) {
return !! inlineWrapperWhiteList[ node.nodeName.toLowerCase() ];
return inlineWrapperWhiteList.hasOwnProperty( node.nodeName.toLowerCase() );
}

export function isAllowedBlock( parentNode, node ) {
Expand Down
9 changes: 9 additions & 0 deletions blocks/library/html/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ export const settings = {
},
},

transforms: {
from: [
{
type: 'raw',
isMatch: ( node ) => node.nodeName === 'IFRAME',
},
],
},

edit: withState( {
preview: false,
} )( ( { attributes, setAttributes, setState, isSelected, preview } ) => [
Expand Down
3 changes: 1 addition & 2 deletions blocks/library/image/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,9 @@ export const settings = {
type: 'raw',
isMatch( node ) {
const tag = node.nodeName.toLowerCase();
const hasText = !! node.textContent.trim();
const hasImage = node.querySelector( 'img' );

return tag === 'img' || ( hasImage && ! hasText ) || ( hasImage && tag === 'figure' );
return tag === 'img' || ( hasImage && tag === 'figure' );
},
transform( node ) {
const targetNode = node.parentNode.querySelector( 'figure,img' );
Expand Down
2 changes: 2 additions & 0 deletions blocks/rich-text/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ export default class RichText extends Component {
plainText: this.pastedPlainText,
mode,
tagName: this.props.tagName,
canUserUseUnfilteredHTML: this.context.canUserUseUnfilteredHTML,
} );

if ( typeof content === 'string' ) {
Expand Down Expand Up @@ -843,6 +844,7 @@ export default class RichText extends Component {

RichText.contextTypes = {
onUndo: noop,
canUserUseUnfilteredHTML: noop,
};

RichText.defaultProps = {
Expand Down
9 changes: 8 additions & 1 deletion editor/components/block-list/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
getSaveElement,
isReusableBlock,
} from '@wordpress/blocks';
import { withFilters, withContext } from '@wordpress/components';
import { withFilters, withContext, withAPIData } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';

/**
Expand Down Expand Up @@ -65,6 +65,7 @@ import {
isSelectionEnabled,
isTyping,
getBlockMode,
getCurrentPostType,
} from '../../store/selectors';

const { BACKSPACE, ESCAPE, DELETE, ENTER, UP, RIGHT, DOWN, LEFT } = keycodes;
Expand Down Expand Up @@ -138,6 +139,7 @@ export class BlockListBlock extends Component {
renderBlockMenu,
showContextualToolbar
),
canUserUseUnfilteredHTML: get( this.props.user, [ 'data', 'capabilities', 'unfiltered_html' ], false ),
};
}

Expand Down Expand Up @@ -610,6 +612,7 @@ const mapStateToProps = ( state, { uid, rootUID } ) => ( {
meta: getEditedPostAttribute( state, 'meta' ),
mode: getBlockMode( state, uid ),
isSelectionEnabled: isSelectionEnabled( state ),
postType: getCurrentPostType( state ),
} );

const mapDispatchToProps = ( dispatch, ownProps ) => ( {
Expand Down Expand Up @@ -687,6 +690,7 @@ BlockListBlock.className = 'editor-block-list__block-edit';

BlockListBlock.childContextTypes = {
BlockList: noop,
canUserUseUnfilteredHTML: noop,
};

export default compose(
Expand All @@ -699,4 +703,7 @@ export default compose(
};
} ),
withFilters( 'editor.BlockListBlock' ),
withAPIData( ( { postType } ) => ( {
user: `/wp/v2/users/me?post_type=${ postType }&context=edit`,
} ) ),
)( BlockListBlock );
43 changes: 27 additions & 16 deletions editor/components/block-settings-menu/unknown-converter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,41 @@
* External dependencies
*/
import { connect } from 'react-redux';
import { get } from 'lodash';

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { IconButton } from '@wordpress/components';
import { IconButton, withAPIData } from '@wordpress/components';
import { getUnknownTypeHandlerName, rawHandler, serialize } from '@wordpress/blocks';
import { compose } from '@wordpress/element';

/**
* Internal dependencies
*/
import { getBlock } from '../../store/selectors';
import { getBlock, getCurrentPostType } from '../../store/selectors';
import { replaceBlocks } from '../../store/actions';

export function UnknownConverter( { block, convertToBlocks, small } ) {
export function UnknownConverter( { block, onReplace, small, user } ) {
if ( ! block || getUnknownTypeHandlerName() !== block.name ) {
return null;
}

const label = __( 'Convert to blocks' );

const convertToBlocks = () => {
onReplace( block.uid, rawHandler( {
HTML: serialize( block ),
mode: 'BLOCKS',
canUserUseUnfilteredHTML: get( user, [ 'data', 'capabilities', 'unfiltered_html' ], false ),
} ) );
};

return (
<IconButton
className="editor-block-settings-menu__control"
onClick={ () => convertToBlocks( block ) }
onClick={ convertToBlocks }
icon="screenoptions"
label={ small ? label : undefined }
>
Expand All @@ -35,16 +45,17 @@ export function UnknownConverter( { block, convertToBlocks, small } ) {
);
}

export default connect(
( state, { uid } ) => ( {
block: getBlock( state, uid ),
} ),
( dispatch, { uid } ) => ( {
convertToBlocks( block ) {
dispatch( replaceBlocks( uid, rawHandler( {
HTML: serialize( block ),
mode: 'BLOCKS',
} ) ) );
},
} )
export default compose(
connect(
( state, { uid } ) => ( {
block: getBlock( state, uid ),
postType: getCurrentPostType( state ),
} ),
{
onReplace: replaceBlocks,
}
),
withAPIData( ( { postType } ) => ( {
user: `/wp/v2/users/me?post_type=${ postType }&context=edit`,
} ) ),
)( UnknownConverter );

0 comments on commit 13b181b

Please sign in to comment.