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

HTML API: Introduce safe HTML templating. #5949

Open
wants to merge 4 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 59 additions & 26 deletions src/wp-includes/class-wp-editor.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,8 @@
*/
public static function editor( $content, $editor_id, $settings = array() ) {
$set = self::parse_settings( $editor_id, $settings );
$editor_class = ' class="' . trim( esc_attr( $set['editor_class'] ) . ' wp-editor-area' ) . '"';
$tabindex = $set['tabindex'] ? ' tabindex="' . (int) $set['tabindex'] . '"' : '';
$default_editor = 'html';
$buttons = '';
$autocomplete = '';
$editor_id_attr = esc_attr( $editor_id );

if ( $set['drag_drop_upload'] ) {
self::$drag_drop_upload = true;
Expand All @@ -180,19 +176,23 @@
}

if ( self::$this_tinymce ) {
$autocomplete = ' autocomplete="off"';

if ( self::$this_quicktags ) {
$default_editor = $set['default_editor'] ? $set['default_editor'] : wp_default_editor();
// 'html' is used for the "Text" editor tab.
if ( 'html' !== $default_editor ) {
$default_editor = 'tinymce';
}

$buttons .= '<button type="button" id="' . $editor_id_attr . '-tmce" class="wp-switch-editor switch-tmce"' .
' data-wp-editor-id="' . $editor_id_attr . '">' . _x( 'Visual', 'Name for the Visual editor tab' ) . "</button>\n";
$buttons .= '<button type="button" id="' . $editor_id_attr . '-html" class="wp-switch-editor switch-html"' .
' data-wp-editor-id="' . $editor_id_attr . '">' . _x( 'Text', 'Name for the Text editor tab (formerly HTML)' ) . "</button>\n";
$buttons .= WP_HTML::render( <<<HTML

Check failure on line 186 in src/wp-includes/class-wp-editor.php

View workflow job for this annotation

GitHub Actions / PHP coding standards

Opening parenthesis of a multi-line function call must be the last content on the line
<button type="button" id="</%id>-tmce" class="wp-switch-editor switch-tmce" data-wp-editor-id="</%id>"></%visual_label></button>
<button type="button" id="</%id>-html" class="wp-switch-editor switch-html" data-wp-editor-id="</%id>"></%text_label></button>
HTML,

Check failure on line 189 in src/wp-includes/class-wp-editor.php

View workflow job for this annotation

GitHub Actions / Check PHP compatibility

Having code - other than a semi-colon or new line - after the closing marker of a heredoc/nowdoc is not supported in PHP 7.2 or earlier.
array(
'id' => $editor_id,
'text_label' => _x( 'Text', 'Name for the Text editor tab (formerly HTML)' ),
'visual_label' => _x( 'Visual', 'Name for the Visual editor tab' ),
)
);
} else {
$default_editor = 'tinymce';
}
Expand All @@ -201,11 +201,13 @@
$switch_class = 'html' === $default_editor ? 'html-active' : 'tmce-active';
$wrap_class = 'wp-core-ui wp-editor-wrap ' . $switch_class;

if ( $set['_content_editor_dfw'] ) {
$wrap_class .= ' has-dfw';
}

echo '<div id="wp-' . $editor_id_attr . '-wrap" class="' . $wrap_class . '">';
echo WP_HTML::render(
'<div id="wp-</%id>-wrap" class="wp-core-jui wp-editor-wrap </%has_dfw>">',
array(
'id' => $editor_id,
'has_dfw' => $set['_content_editor_dfw'] ? 'has-dfw' : '',
)
);

if ( self::$editor_buttons_css ) {
wp_print_styles( 'editor-buttons' );
Expand All @@ -217,7 +219,10 @@
}

if ( ! empty( $buttons ) || $set['media_buttons'] ) {
echo '<div id="wp-' . $editor_id_attr . '-editor-tools" class="wp-editor-tools hide-if-no-js">';
echo WP_HTML::render(
'<div id="wp-</%id>-editor-tools" class="wp-editor-tools hide-if-no-js">',
array( 'id' => $editor_id )
);

if ( $set['media_buttons'] ) {
self::$has_medialib = true;
Expand All @@ -226,7 +231,10 @@
require ABSPATH . 'wp-admin/includes/media.php';
}

echo '<div id="wp-' . $editor_id_attr . '-media-buttons" class="wp-media-buttons">';
echo WP_HTML::render(
'<div id="wp-</%id>-media-button" class="wp-media-buttons">',
array( 'id' => $editor_id )
);

/**
* Fires after the default media button(s) are displayed.
Expand All @@ -249,10 +257,13 @@
if ( 'content' === $editor_id && ! empty( $GLOBALS['current_screen'] ) && 'post' === $GLOBALS['current_screen']->base ) {
$toolbar_id = 'ed_toolbar';
} else {
$toolbar_id = 'qt_' . $editor_id_attr . '_toolbar';
$toolbar_id = 'qt_' . $editor_id . '_toolbar';
}

$quicktags_toolbar = '<div id="' . $toolbar_id . '" class="quicktags-toolbar hide-if-no-js"></div>';
$quicktags_toolbar = WP_HTML::render(
'<div id="</%id>" class="quicktags-toolbar hide-if-no-js"></div>',
array( 'id' => $toolbar_id )
);
}

/**
Expand All @@ -264,10 +275,28 @@
*/
$the_editor = apply_filters(
'the_editor',
'<div id="wp-' . $editor_id_attr . '-editor-container" class="wp-editor-container">' .
WP_HTML::render(
'<div id="wp-</%id>-editor-container" class="wp-editor-container">',
array( 'id' => $editor_id )
) .
$quicktags_toolbar .
'<textarea' . $editor_class . $height . $tabindex . $autocomplete . ' cols="40" name="' . esc_attr( $set['textarea_name'] ) . '" ' .
'id="' . $editor_id_attr . '">%s</textarea></div>'
WP_HTML::render(
<<<'HTML'
<textarea class="</%editor_class>" ...height tabindex="</%tabindex>"
autocomplete="</%autocomplete>" cols="40" name="</%name>" id="</%id>">%s</textarea>
HTML,

Check failure on line 287 in src/wp-includes/class-wp-editor.php

View workflow job for this annotation

GitHub Actions / Check PHP compatibility

Having code - other than a semi-colon or new line - after the closing marker of a heredoc/nowdoc is not supported in PHP 7.2 or earlier.
array(
'autocomplete' => self::$this_tinymce ? 'off' : null,
'editor_class' => trim( "{$set['editor_class']} wp-editor-area" ),
'height' => ! empty( $set['editor_height'] )
? array( 'style' => "height: {$set['editor_height']}px;" )
: array( 'rows' => (string) $set['textarea_rows'] ),
'id' => $editor_id,
'name' => $set['textarea_name'],
'tabindex' => $set['tabindex'] ? (string) $set['tabindex'] : null,
)
) .
'</div>'
);

// Prepare the content for the Visual or Text editor, only when TinyMCE is used (back-compat).
Expand Down Expand Up @@ -300,12 +329,16 @@
$content = apply_filters_deprecated( 'richedit_pre', array( $content ), '4.3.0', 'format_for_editor' );
}

if ( false !== stripos( $content, 'textarea' ) ) {
$content = preg_replace( '%</textarea%i', '&lt;/textarea', $content );
$processor = new WP_HTML_Tag_Processor( $the_editor );
while ( $processor->next_tag( 'TEXTAREA' ) ) {
if ( $editor_id === $processor->get_attribute( 'id' ) ) {
$processor->set_modifiable_text( $content );
break;
}
}
$the_editor = $processor->get_updated_html();

printf( $the_editor, $content );
echo "\n</div>\n\n";
echo "{$the_editor}\n</div>\n\n";

self::editor_settings( $editor_id, $set );
}
Expand Down
75 changes: 71 additions & 4 deletions src/wp-includes/html-api/class-wp-html-tag-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -2038,8 +2038,8 @@ private function after_tag() {
$this->token_length = null;
$this->tag_name_starts_at = null;
$this->tag_name_length = null;
$this->text_starts_at = 0;
$this->text_length = 0;
$this->text_starts_at = null;
$this->text_length = null;
$this->is_closing_tag = null;
$this->attributes = array();
$this->comment_type = null;
Expand Down Expand Up @@ -2826,6 +2826,50 @@ public function get_modifiable_text() {
return $decoded;
}

/**
* Sets the modifiable text for the matched token, if possible.
*
* @param string $text Replace the modifiable text with this string.
* @return bool Whether the modifiable text was updated.
*/
public function set_modifiable_text( $text ) {
if ( null === $this->text_starts_at || ! is_string( $text ) ) {
return false;
}

switch ( $this->get_token_name() ) {
case '#text':
$this->lexical_updates[] = new WP_HTML_Text_Replacement(
$this->text_starts_at,
$this->text_length,
esc_html( $text )
);
break;

case 'TEXTAREA':
$this->lexical_updates[] = new WP_HTML_Text_Replacement(
$this->text_starts_at,
$this->text_length,
preg_replace( '~</textarea~i', '&lt;/textarea', $text )
);
break;

case 'TITLE':
$this->lexical_updates[] = new WP_HTML_Text_Replacement(
$this->text_starts_at,
$this->text_length,
preg_replace( '~</title~i', '&lt;/title', $text )
);
break;

default:
return false;
}

$this->get_updated_html();
return true;
}

/**
* Updates or creates a new attribute on the currently matched tag with the passed value.
*
Expand Down Expand Up @@ -2899,14 +2943,37 @@ public function set_attribute( $name, $value ) {
* > To represent a false value, the attribute has to be omitted altogether.
* - HTML5 spec, https://html.spec.whatwg.org/#boolean-attributes
*/
if ( false === $value ) {
if ( false === $value || null === $value ) {
return $this->remove_attribute( $name );
}

if ( true === $value ) {
$updated_attribute = $name;
} else {
$escaped_new_value = esc_attr( $value );
$tag_name = $this->get_tag();
$comparable_name = strtolower( $name );

/*
* Escape URL attributes.
*
* @see https://html.spec.whatwg.org/#attributes-3
*/
if (
! str_starts_with( $value, 'data:' ) && (
'cite' === $comparable_name ||
'formaction' === $comparable_name ||
'href' === $comparable_name ||
'ping' === $comparable_name ||
'src' === $comparable_name ||
( 'FORM' === $tag_name && 'action' === $comparable_name ) ||
( 'OBJECT' === $tag_name && 'data' === $comparable_name ) ||
( 'VIDEO' === $tag_name && 'poster' === $comparable_name )
)
) {
$escaped_new_value = esc_url( $value );
} else {
$escaped_new_value = esc_attr( $value );
}
$updated_attribute = "{$name}=\"{$escaped_new_value}\"";
}

Expand Down
Loading
Loading