Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add iframe raw handling (e.g. Google Maps) #4660

Merged
merged 2 commits into from
Feb 8, 2018
Merged

Conversation

ellatrix
Copy link
Member

@ellatrix ellatrix commented Jan 24, 2018

Description

  1. This will allow the pasting of iframes when the user can publish unfiltered HTML. When frames are pasted, they are put in an HTML block. We can iterate on this later by creating a Google Maps embed block that would accept the embed code as well for discoverability. See Add google maps as an embed #3970.
  2. It will extract iframes and images from paragraphs so that they can be converted to blocks. Previously an aligned image in the middle of a paragraph would not be converted to an image block.

How Has This Been Tested?

  1. E.g. go to Google Maps, click the share icon, then the embed tab, and copy the embed code (an iframe). Pasting this in Gutenberg as an admin should now work. Ensure that it does not as an author or less.
  2. Put an iframe in a classic text block (through edit HTML). Then edit visually, and go back to edit HTML. The iframe will be wrapped in a paragraph because it is an inline element. Now convert into blocks. The iframe should be put in a separate block. Previously the iframe would be stripped from the paragraph tag.


export default withContext( 'editor' )(
( settings ) => ( {
unfilteredHTML: settings && settings.unfilteredHTML,
Copy link
Contributor

@jaswrks jaswrks Jan 24, 2018

Choose a reason for hiding this comment

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

@iseulde Do you think it would be better to use the withAPIData component and check user.data.capabilities.unfiltered_html? example here

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point, didn't think of that. Will try tomorrow.

@jaswrks
Copy link
Contributor

jaswrks commented Jan 24, 2018

E.g. go to Google Maps, click the share icon, then the embed tab, and copy the embed code (an iframe). Pasting this in Gutenberg as an admin should now work.

I tested this in latest Chrome on macOS and it works! :-)

Ensure that it does not as an editor or less.

I think you mean author, right? If so, this is working correctly. Editors can post unfiltered HTML.

@ellatrix
Copy link
Member Author

I think you mean author, right? If so, this is working correctly. Editors can post unfiltered HTML.

Yes, sorry. Didn't realise editors have the cap as well.

I updated the PR, but since the Editable doesn't need to be aware of the post type, it now loads duplicate user data... This is more of a REST API/caching concern though. Not sure how to best fix this.

@ellatrix ellatrix changed the title Allow pasting iframes (e.g. Google Maps) Allow pasting/convert from iframes (e.g. Google Maps) Jan 25, 2018
@ellatrix ellatrix changed the title Allow pasting/convert from iframes (e.g. Google Maps) Add iframe raw handling (e.g. Google Maps) Jan 25, 2018
@ellatrix ellatrix requested a review from mtias January 26, 2018 12:39
@ellatrix
Copy link
Member Author

If someone could review, that'd be great! :)


let wrapper = node;

while ( wrapper.nodeName !== 'P' ) {
Copy link
Contributor

@jaswrks jaswrks Jan 30, 2018

Choose a reason for hiding this comment

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

I think you might need while( wrapper && wrapper.nodeName ) here, because the final iteration might be a node with parentElement = null.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks! I changed it from while ( wrapper.nodeName !== 'P' && ( wrapper = wrapper.parentElement ) ) {} and forgot to add the check. 🙈 Nice catch.

@ellatrix
Copy link
Member Author

ellatrix commented Feb 1, 2018

Rebased after Editable rename.

@@ -909,6 +909,10 @@ function gutenberg_editor_scripts_and_styles( $hook ) {
'colors' => $color_palette,
'blockTypes' => $allowed_block_types,
'titlePlaceholder' => apply_filters( 'enter_title_here', __( 'Add title', 'gutenberg' ), $post ),
'alignWide' => $align_wide || ! empty( $gutenberg_theme_support[0]['wide-images'] ), // Backcompat. Use `align-wide` outside of `gutenberg` array.
Copy link
Member

Choose a reason for hiding this comment

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

PHPCS warnings from misaligned arrows here (causing the build failure)

Copy link
Member

Choose a reason for hiding this comment

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

Actually, I think these are duplicated and need to be removed. Maybe from a bad rebase?

onReplace( block.uid, rawHandler( {
HTML: serialize( block ),
mode: 'BLOCKS',
allowIframes: get( user, 'data.capabilities.unfiltered_html', false ),
Copy link
Member

Choose a reason for hiding this comment

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

Minor: String argument to get requires additional parsing and is slightly less performant than passing as an array, e.g.:

allowIframes: get( user, [ 'data', 'capabilities', 'unfiltered_html' ], false ),

Copy link
Member

Choose a reason for hiding this comment

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

Makes me wonder if we should have some equivalent of selectors for withAPIData too, rather than working with the REST properties directly, something like a currentUserCan

@@ -122,6 +123,12 @@ export class BlockListBlock extends Component {
};
}

getChildContext() {
return {
unfilteredHTML: get( this.props.user, 'data.capabilities.unfiltered_html', false ),
Copy link
Member

Choose a reason for hiding this comment

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

The name unfilteredHTML isn't entirely clear to me: At first, I thought this was passing some HTML string, not a true / false permissions thing. Maybe allowUnfilteredHTML ? isUnfilteredHTMLAllowed ?

Copy link
Contributor

Choose a reason for hiding this comment

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

canUserUseUnfilteredHTML? It's a mouthful, but a recognizable phrasing.

import inlineBlockCorrector from '../inline-block-corrector';
import { deepFilterHTML } from '../utils';

describe( 'inlineContentConverter', () => {
Copy link
Member

Choose a reason for hiding this comment

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

Name doesn't match the folder.

import { deepFilterHTML } from '../utils';

describe( 'inlineContentConverter', () => {
it( 'should move inline-block content from paragraph', () => {
Copy link
Member

Choose a reason for hiding this comment

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

It would be useful to have some of these behaviors documented (README.md and/or JSDoc on the functions themselves). By the name inlineBlockCorrector, I had no idea what to expect from its behavior.

I'm coming to this mostly unfamiliar with the raw handlers, so I expect it'd be a common sentiment.

@@ -110,6 +115,10 @@ export function isClassWhitelisted( tag, name ) {
);
}

export function isInlineBlock( node ) {
Copy link
Member

Choose a reason for hiding this comment

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

piece = deepFilterHTML( piece, [
listMerger,
imageCorrector,
// Add semantic formatting before attributes are stripped.
formattingTransformer,
stripAttributes,
commentRemover,
...iframeUnwrapper,
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we're already dealing with an array (the one provided to deepFilterHTML), we can simplify this:

deepFilterHTML( piece, compact( [
  
  allowIframes && unwrapper,
  inlineBlockCorrector,
  
] ) )

Alternatively, we could use .filter( Boolean ).

*/
const { ELEMENT_NODE } = window.Node;

export default function( node ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs a comment and a high-level explanation of what and why.

const hasImage = node.querySelector( 'img' );

return tag === 'img' || ( hasImage && ! hasText ) || ( hasImage && tag === 'figure' );
return tag === 'img' || ( hasImage && tag === 'figure' );
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we know this to be an improvement? For instance, pasting <p>Behold my picture: <img src="..."> Isn't it amazing?</p> yields Image<…> + Paragraph<Behold…amazing?>.

I ask because it change seems tangential to the iframe support. Should we try this separately?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, the same fix is needed for iframes as well as images. Otherwise the image just ends up inside a paragraph. Most of the time, this structure goes together with alignment, so it is not visually clear where the image was anchored.

@@ -31,6 +31,11 @@ const inlineWhitelist = {
br: {},
};

const inlineBlockWhiteList = {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm a bit concerned that all these lists and moving parts in raw-handling grow hard to keep track of, e.g. "inline" vs "inline wrapper" vs. "inline block". Docs in this file would help.

@@ -122,6 +123,12 @@ export class BlockListBlock extends Component {
};
}

getChildContext() {
return {
unfilteredHTML: get( this.props.user, 'data.capabilities.unfiltered_html', false ),
Copy link
Contributor

Choose a reason for hiding this comment

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

canUserUseUnfilteredHTML? It's a mouthful, but a recognizable phrasing.

@ellatrix ellatrix force-pushed the add/paste-iframe branch 2 times, most recently from 1c29b3e to 6d56c2d Compare February 5, 2018 13:47
@ellatrix
Copy link
Member Author

ellatrix commented Feb 5, 2018

I think I addressed all concerns.

@ellatrix ellatrix requested review from mcsf and aduth February 5, 2018 18:38
@ellatrix
Copy link
Member Author

ellatrix commented Feb 7, 2018

Rebased.

@ellatrix ellatrix requested a review from a team February 7, 2018 12:14
@ellatrix
Copy link
Member Author

ellatrix commented Feb 7, 2018

This PR also indirectly fixes a bug where multiple images in a paragraph are all removed except for the first.

/**
* External dependencies
*/
import { equal } from 'assert';
Copy link
Member

Choose a reason for hiding this comment

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

Curious why we're using the built-in assert lib here rather than Jest?

Copy link
Member Author

@ellatrix ellatrix Feb 8, 2018

Choose a reason for hiding this comment

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

It's been used all over this paste module since the beginning. I can change this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Not a fan of the globals :|

@@ -63,7 +68,7 @@ const whitelist = {
};

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

Choose a reason for hiding this comment

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

The type coercion is unnecessary (hasOwnProperty will certainly return a boolean).

@@ -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 );
Copy link
Member

Choose a reason for hiding this comment

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

The type coercion is unnecessary (hasOwnProperty will certainly return a boolean).

* @return {boolean} True if embedded content, false if not.
*/
export function isEmbedded( node ) {
return !! embeddedWhiteList.hasOwnProperty( node.nodeName.toLowerCase() );
Copy link
Member

Choose a reason for hiding this comment

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

The type coercion is unnecessary (hasOwnProperty will certainly return a boolean).

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

Choose a reason for hiding this comment

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

The type coercion is unnecessary (hasOwnProperty will certainly return a boolean).

@@ -371,6 +371,7 @@ export default class RichText extends Component {
plainText: this.pastedPlainText,
mode,
tagName: this.props.tagName,
allowIframes: this.context.canUserUseUnfilteredHTML,
Copy link
Member

Choose a reason for hiding this comment

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

Are we ever expecting allowScript, allowInput, allowStyle?

Maybe we just pass the canUserUseUnfilteredHTML verbatim as the property name?

@ellatrix
Copy link
Member Author

ellatrix commented Feb 8, 2018

Thanks @aduth, feedback addressed. :)

@ellatrix
Copy link
Member Author

ellatrix commented Feb 8, 2018

After reviews from 3 people in the last two weeks, I think this is ready for merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants