Skip to content

Commit

Permalink
HTML API: Add context to Unsupported_Exception class for improved deb…
Browse files Browse the repository at this point in the history
…ugging.

The HTML Processor internally throws an exception when it reaches HTML
that it knows it cannot process, but this exception is not made
available to calling code. It can be useful to extract more knowledge
about why it gave up, especially for debugging purposes.

In this patch, more context is added to the WP_HTML_Unsupported_Exception
and the last exception is made available to calling code through a new
method, `get_unsupported_exception()`.

Developed in WordPress/wordpress-develop#6985
Discussed in https://core.trac.wordpress.org/ticket/61646

Props bernhard-reiter, dmsnell, jonsurrell.
See #61646.

Built from https://develop.svn.wordpress.org/trunk@58714


git-svn-id: https://core.svn.wordpress.org/trunk@58116 1a063a9b-81f0-0310-95a4-ce76da25c4cd
  • Loading branch information
dmsnell committed Jul 12, 2024
1 parent 0151e40 commit 09ce30b
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 57 deletions.
151 changes: 95 additions & 56 deletions wp-includes/html-api/class-wp-html-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,17 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
*/
private $last_error = null;

/**
* Stores context for why the parser bailed on unsupported HTML, if it did.
*
* @see self::get_unsupported_exception
*
* @since 6.7.0
*
* @var WP_HTML_Unsupported_Exception|null
*/
private $unsupported_exception = null;

/**
* Releases a bookmark when PHP garbage-collects its wrapping WP_HTML_Token instance.
*
Expand Down Expand Up @@ -384,6 +395,45 @@ function ( WP_HTML_Token $token ) {
};
}

/**
* Stops the parser and terminates its execution when encountering unsupported markup.
*
* @throws WP_HTML_Unsupported_Exception Halts execution of the parser.
*
* @since 6.7.0
*
* @param string $message Explains support is missing in order to parse the current node.
*
* @return mixed
*/
private function bail( string $message ) {
$here = $this->bookmarks[ $this->state->current_token->bookmark_name ];
$token = substr( $this->html, $here->start, $here->length );

$open_elements = array();
foreach ( $this->state->stack_of_open_elements->stack as $item ) {
$open_elements[] = $item->node_name;
}

$active_formats = array();
foreach ( $this->state->active_formatting_elements->walk_down() as $item ) {
$active_formats[] = $item->node_name;
}

$this->last_error = self::ERROR_UNSUPPORTED;

$this->unsupported_exception = new WP_HTML_Unsupported_Exception(
$message,
$this->state->current_token->node_name,
$here->start,
$token,
$open_elements,
$active_formats
);

throw $this->unsupported_exception;
}

/**
* Returns the last error, if any.
*
Expand Down Expand Up @@ -411,6 +461,21 @@ public function get_last_error() {
return $this->last_error;
}

/**
* Returns context for why the parser aborted due to unsupported HTML, if it did.
*
* This is meant for debugging purposes, not for production use.
*
* @since 6.7.0
*
* @see self::$unsupported_exception
*
* @return WP_HTML_Unsupported_Exception|null
*/
public function get_unsupported_exception() {
return $this->unsupported_exception;
}

/**
* Finds the next tag matching the $query.
*
Expand Down Expand Up @@ -841,8 +906,7 @@ public function step( $node_to_process = self::PROCESS_NEXT_NODE ) {

// This should be unreachable but PHP doesn't have total type checking on switch.
default:
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "Found unrecognized insertion mode '{$this->state->insertion_mode}'." );
$this->bail( "Unaware of the requested parsing mode: '{$this->state->insertion_mode}'." );
}
} catch ( WP_HTML_Unsupported_Exception $e ) {
/*
Expand Down Expand Up @@ -922,8 +986,7 @@ public function get_current_depth() {
* @return bool Whether an element was found.
*/
private function step_initial() {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
$this->bail( "No support for parsing in the '{$this->state->insertion_mode}' state." );
}

/**
Expand All @@ -942,8 +1005,7 @@ private function step_initial() {
* @return bool Whether an element was found.
*/
private function step_before_html() {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
$this->bail( "No support for parsing in the '{$this->state->insertion_mode}' state." );
}

/**
Expand All @@ -962,8 +1024,7 @@ private function step_before_html() {
* @return bool Whether an element was found.
*/
private function step_before_head() {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
$this->bail( "No support for parsing in the '{$this->state->insertion_mode}' state." );
}

/**
Expand All @@ -982,8 +1043,7 @@ private function step_before_head() {
* @return bool Whether an element was found.
*/
private function step_in_head() {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
$this->bail( "No support for parsing in the '{$this->state->insertion_mode}' state." );
}

/**
Expand All @@ -1002,8 +1062,7 @@ private function step_in_head() {
* @return bool Whether an element was found.
*/
private function step_in_head_noscript() {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
$this->bail( "No support for parsing in the '{$this->state->insertion_mode}' state." );
}

/**
Expand All @@ -1022,8 +1081,7 @@ private function step_in_head_noscript() {
* @return bool Whether an element was found.
*/
private function step_after_head() {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
$this->bail( "No support for parsing in the '{$this->state->insertion_mode}' state." );
}

/**
Expand Down Expand Up @@ -1445,8 +1503,9 @@ private function step_in_body() {
* > than the end tag token that it actually is.
*/
case '-BR':
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( 'Closing BR tags require unimplemented special handling.' );
$this->bail( 'Closing BR tags require unimplemented special handling.' );
// This return required because PHPCS can't determine that the call to bail() throws.
return false;

/*
* > A start tag whose tag name is one of: "area", "br", "embed", "img", "keygen", "wbr"
Expand Down Expand Up @@ -1602,8 +1661,7 @@ private function step_in_body() {
case 'TITLE':
case 'TR':
case 'XMP':
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "Cannot process {$token_name} element." );
$this->bail( "Cannot process {$token_name} element." );
}

if ( ! parent::is_tag_closer() ) {
Expand Down Expand Up @@ -1665,8 +1723,7 @@ private function step_in_body() {
* @return bool Whether an element was found.
*/
private function step_in_table() {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
$this->bail( "No support for parsing in the '{$this->state->insertion_mode}' state." );
}

/**
Expand All @@ -1685,8 +1742,7 @@ private function step_in_table() {
* @return bool Whether an element was found.
*/
private function step_in_table_text() {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
$this->bail( "No support for parsing in the '{$this->state->insertion_mode}' state." );
}

/**
Expand All @@ -1705,8 +1761,7 @@ private function step_in_table_text() {
* @return bool Whether an element was found.
*/
private function step_in_caption() {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
$this->bail( "No support for parsing in the '{$this->state->insertion_mode}' state." );
}

/**
Expand All @@ -1725,8 +1780,7 @@ private function step_in_caption() {
* @return bool Whether an element was found.
*/
private function step_in_column_group() {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
$this->bail( "No support for parsing in the '{$this->state->insertion_mode}' state." );
}

/**
Expand All @@ -1745,8 +1799,7 @@ private function step_in_column_group() {
* @return bool Whether an element was found.
*/
private function step_in_table_body() {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
$this->bail( "No support for parsing in the '{$this->state->insertion_mode}' state." );
}

/**
Expand All @@ -1765,8 +1818,7 @@ private function step_in_table_body() {
* @return bool Whether an element was found.
*/
private function step_in_row() {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
$this->bail( "No support for parsing in the '{$this->state->insertion_mode}' state." );
}

/**
Expand All @@ -1785,8 +1837,7 @@ private function step_in_row() {
* @return bool Whether an element was found.
*/
private function step_in_cell() {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
$this->bail( "No support for parsing in the '{$this->state->insertion_mode}' state." );
}

/**
Expand Down Expand Up @@ -1986,8 +2037,7 @@ private function step_in_select() {
* @return bool Whether an element was found.
*/
private function step_in_select_in_table() {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
$this->bail( "No support for parsing in the '{$this->state->insertion_mode}' state." );
}

/**
Expand All @@ -2006,8 +2056,7 @@ private function step_in_select_in_table() {
* @return bool Whether an element was found.
*/
private function step_in_template() {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
$this->bail( "No support for parsing in the '{$this->state->insertion_mode}' state." );
}

/**
Expand All @@ -2026,8 +2075,7 @@ private function step_in_template() {
* @return bool Whether an element was found.
*/
private function step_after_body() {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
$this->bail( "No support for parsing in the '{$this->state->insertion_mode}' state." );
}

/**
Expand All @@ -2046,8 +2094,7 @@ private function step_after_body() {
* @return bool Whether an element was found.
*/
private function step_in_frameset() {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
$this->bail( "No support for parsing in the '{$this->state->insertion_mode}' state." );
}

/**
Expand All @@ -2066,8 +2113,7 @@ private function step_in_frameset() {
* @return bool Whether an element was found.
*/
private function step_after_frameset() {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
$this->bail( "No support for parsing in the '{$this->state->insertion_mode}' state." );
}

/**
Expand All @@ -2086,8 +2132,7 @@ private function step_after_frameset() {
* @return bool Whether an element was found.
*/
private function step_after_after_body() {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
$this->bail( "No support for parsing in the '{$this->state->insertion_mode}' state." );
}

/**
Expand All @@ -2106,8 +2151,7 @@ private function step_after_after_body() {
* @return bool Whether an element was found.
*/
private function step_after_after_frameset() {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
$this->bail( "No support for parsing in the '{$this->state->insertion_mode}' state." );
}

/**
Expand All @@ -2126,8 +2170,7 @@ private function step_after_after_frameset() {
* @return bool Whether an element was found.
*/
private function step_in_foreign_content() {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." );
$this->bail( "No support for parsing in the '{$this->state->insertion_mode}' state." );
}

/*
Expand Down Expand Up @@ -2835,8 +2878,7 @@ private function reconstruct_active_formatting_elements() {
return false;
}

$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( 'Cannot reconstruct active formatting elements when advancing and rewinding is required.' );
$this->bail( 'Cannot reconstruct active formatting elements when advancing and rewinding is required.' );
}

/**
Expand Down Expand Up @@ -3072,8 +3114,7 @@ private function run_adoption_agency_algorithm() {

// > If there is no such element, then return and instead act as described in the "any other end tag" entry above.
if ( null === $formatting_element ) {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( 'Cannot run adoption agency when "any other end tag" is required.' );
$this->bail( 'Cannot run adoption agency when "any other end tag" is required.' );
}

// > If formatting element is not in the stack of open elements, then this is a parse error; remove the element from the list, and return.
Expand Down Expand Up @@ -3125,12 +3166,10 @@ private function run_adoption_agency_algorithm() {
}
}

$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( 'Cannot extract common ancestor in adoption agency algorithm.' );
$this->bail( 'Cannot extract common ancestor in adoption agency algorithm.' );
}

$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( 'Cannot run adoption agency when looping required.' );
$this->bail( 'Cannot run adoption agency when looping required.' );
}

/**
Expand Down
Loading

0 comments on commit 09ce30b

Please sign in to comment.