Skip to content

Commit

Permalink
Wrap registered widgets in callbacks to track widget validation errors
Browse files Browse the repository at this point in the history
* Include core as a source in addition to plugins and themes.
* Refactor source comments to only consist of JSON-encoded source data.
* Include the function name in the source.
  • Loading branch information
westonruter committed Mar 10, 2018
1 parent d0f4f01 commit 3d82d39
Show file tree
Hide file tree
Showing 2 changed files with 185 additions and 168 deletions.
199 changes: 102 additions & 97 deletions includes/utils/class-amp-validation-utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -597,47 +597,33 @@ public static function print_edit_form_validation_status( $post ) {
/**
* Get source start comment.
*
* @param string $type Extension type.
* @param string $name Extension name.
* @param array $args Args.
* @param array $source Source data.
* @param bool $is_start Whether the comment is the start or end.
* @return string HTML Comment.
*/
public static function get_source_comment_start( $type, $name, $args = array() ) {
$args_encoded = wp_json_encode( $args );
if ( '[]' === $args_encoded ) {
$args_encoded = '{}';
}
return sprintf( '<!--amp-source-stack:%s:%s %s-->', $type, $name, str_replace( '--', '', $args_encoded ) );
}

/**
* Get source end comment.
*
* @param string $type Extension type.
* @param string $name Extension name.
* @return string HTML Comment.
*/
public static function get_source_comment_end( $type, $name ) {
return sprintf( '<!--/amp-source-stack:%s:%s-->', $type, $name );
public static function get_source_comment( array $source, $is_start = true ) {
return sprintf(
'<!--%samp-source-stack %s-->',
$is_start ? '' : '/',
str_replace( '--', '', wp_json_encode( $source ) )
);
}

/**
* Parse source comment.
*
* @param DOMComment $comment Comment.
* @return array|null Source info or null if not a source comment.
* @return array|null Parsed source or null if not a source comment.
*/
public static function parse_source_comment( DOMComment $comment ) {
if ( ! preg_match( '#^\s*(?P<closing>/)?amp-source-stack:(?P<type>theme|plugin|mu-plugin):(?P<name>\S+)(?: (?P<args>{.+}))?\s*$#s', $comment->nodeValue, $matches ) ) {
if ( ! preg_match( '#^\s*(?P<closing>/)?amp-source-stack\s+(?P<args>{.+})\s*$#s', $comment->nodeValue, $matches ) ) {
return null;
}
$source = wp_array_slice_assoc( $matches, array( 'type', 'name' ) );

$source['closing'] = ! empty( $matches['closing'] );
if ( isset( $matches['args'] ) ) {
$source['args'] = json_decode( $matches['args'], true );
}
return $source;
$source = json_decode( $matches['args'], true );
$closing = ! empty( $matches['closing'] );

return compact( 'source', 'closing' );
}

/**
Expand All @@ -653,17 +639,17 @@ public static function parse_source_comment( DOMComment $comment ) {
*/
public static function locate_sources( DOMNode $node ) {
$xpath = new DOMXPath( $node->ownerDocument );
$comments = $xpath->query( 'preceding::comment()[ contains( ., "amp-source-stack:" ) ]', $node );
$comments = $xpath->query( 'preceding::comment()[ starts-with( ., "amp-source-stack" ) or starts-with( ., "/amp-source-stack" ) ]', $node );
$sources = array();
foreach ( $comments as $comment ) {
$source = self::parse_source_comment( $comment );
if ( $source ) {
if ( $source['closing'] ) {
array_pop( $sources );
} else {
unset( $source['closing'] );
$sources[] = $source;
}
$parsed_comment = self::parse_source_comment( $comment );
if ( ! $parsed_comment ) {
continue;
}
if ( $parsed_comment['closing'] ) {
array_pop( $sources );
} else {
$sources[] = $parsed_comment['source'];
}
}
return $sources;
Expand All @@ -677,7 +663,7 @@ public static function locate_sources( DOMNode $node ) {
public static function remove_source_comments( $dom ) {
$xpath = new DOMXPath( $dom );
$comments = array();
foreach ( $xpath->query( '//comment()[ contains( ., "amp-source-stack:" ) ]' ) as $comment ) {
foreach ( $xpath->query( '//comment()[ starts-with( ., "amp-source-stack" ) or starts-with( ., "/amp-source-stack" ) ]' ) as $comment ) {
if ( self::parse_source_comment( $comment ) ) {
$comments[] = $comment;
}
Expand All @@ -700,20 +686,22 @@ public static function remove_source_comments( $dom ) {
public static function callback_wrappers() {
global $wp_filter;
$pending_wrap_callbacks = array();
foreach ( $wp_filter as $filter_tag => $wp_hook ) {

// @todo Consider doing this at the 'all' action instead. This would be more reliable and we could reset $wp_filter at PHP_INT_MAX.
// Wrap all added filters and actions.
foreach ( $wp_filter as $hook => $wp_hook ) {
foreach ( $wp_hook->callbacks as $priority => $callbacks ) {
foreach ( $callbacks as $callback ) {
$source_data = self::get_source( $callback['function'] );
if ( isset( $source_data ) ) {
$pending_wrap_callbacks[ $filter_tag ][] = array_merge(
$callback,
$source_data,
array(
'hook' => $filter_tag,
),
compact( 'priority' )
);
$source = self::get_source( $callback['function'] );
if ( ! $source ) {
continue;
}
$source['hook'] = $hook;

$pending_wrap_callbacks[ $hook ][] = array_merge(
$callback,
compact( 'priority', 'source', 'hook' )
);
}
}
}
Expand All @@ -726,6 +714,23 @@ public static function callback_wrappers() {
add_action( $hook, $wrapped_callback, $callback['priority'], $callback['accepted_args'] );
}
}

// Wrap widgets callbacks.
global $wp_registered_widgets;
foreach ( $wp_registered_widgets as $widget_id => &$registered_widget ) {
$source = self::get_source( $registered_widget['callback'] );
if ( ! $source ) {
continue;
}
$source['widget_id'] = $widget_id;

$function = $registered_widget['callback'];
$accepted_args = 2; // For the $instance and $args arguments.
$callback = compact( 'function', 'accepted_args', 'source' );

$registered_widget['callback'] = self::wrapped_callback( $callback );
}

}

/**
Expand All @@ -747,10 +752,12 @@ public static function decorate_shortcode_source( $output, $tag ) {
if ( empty( $source ) ) {
return $output;
}
$source['shortcode'] = $tag;

$output = implode( '', array(
self::get_source_comment_start( $source['type'], $source['name'], array( 'shortcode' => $tag ) ),
self::get_source_comment( $source, true ),
$output,
self::get_source_comment_end( $source['type'], $source['name'] ),
self::get_source_comment( $source, false ),
) );
return $output;
}
Expand All @@ -767,47 +774,65 @@ public static function decorate_shortcode_source( $output, $tag ) {
* }
*/
public static function get_source( $callback ) {
$reflection = null;
$class_name = null; // Because ReflectionMethod::getDeclaringClass() can return a parent class.
try {
if ( is_string( $callback ) && is_callable( $callback ) ) {
// The $callback is a function or static method.
$exploded_callback = explode( '::', $callback );
if ( count( $exploded_callback ) > 1 ) {
$reflection = new ReflectionClass( $exploded_callback[0] );
$exploded_callback = explode( '::', $callback, 2 );
if ( 2 === count( $exploded_callback ) ) {
$class_name = $exploded_callback[0];
$reflection = new ReflectionMethod( $exploded_callback[0], $exploded_callback[1] );
} else {
$reflection = new ReflectionFunction( $callback );
}
} elseif ( is_array( $callback ) && isset( $callback[0], $callback[1] ) && method_exists( $callback[0], $callback[1] ) ) {
// The $callback is a method.
$reflection = new ReflectionClass( $callback[0] );
if ( is_string( $callback[0] ) ) {
$class_name = '\'' . $callback[0];
} elseif ( is_object( $callback[0] ) ) {
$class_name = get_class( $callback[0] );
}
$reflection = new ReflectionMethod( $callback[0], $callback[1] );
} elseif ( is_object( $callback ) && ( 'Closure' === get_class( $callback ) ) ) {
$reflection = new ReflectionFunction( $callback );
}
} catch ( Exception $e ) {
return null;
}

$file = isset( $reflection ) ? $reflection->getFileName() : null;
if ( ! isset( $file ) ) {
if ( ! $reflection ) {
return null;
}
$file = wp_normalize_path( $file );

$slug_pattern = '([^/]+)';
if ( preg_match( ':' . preg_quote( trailingslashit( wp_normalize_path( WP_PLUGIN_DIR ) ), ':' ) . $slug_pattern . ':s', $file, $matches ) ) {
$type = 'plugin';
$name = $matches[1];
} elseif ( preg_match( ':' . preg_quote( trailingslashit( wp_normalize_path( get_theme_root() ) ), ':' ) . $slug_pattern . ':s', $file, $matches ) ) {
$type = 'theme';
$name = $matches[1];
} elseif ( preg_match( ':' . preg_quote( trailingslashit( wp_normalize_path( WPMU_PLUGIN_DIR ) ), ':' ) . $slug_pattern . ':s', $file, $matches ) ) {
$type = 'mu-plugin';
$name = $matches[1];
$source = array();

$file = $reflection->getFileName();
if ( $file ) {
$file = wp_normalize_path( $file );
$slug_pattern = '([^/]+)';
if ( preg_match( ':' . preg_quote( trailingslashit( wp_normalize_path( WP_PLUGIN_DIR ) ), ':' ) . $slug_pattern . ':s', $file, $matches ) ) {
$source['type'] = 'plugin';
$source['name'] = $matches[1];
} elseif ( preg_match( ':' . preg_quote( trailingslashit( wp_normalize_path( get_theme_root() ) ), ':' ) . $slug_pattern . ':s', $file, $matches ) ) {
$source['type'] = 'theme';
$source['name'] = $matches[1];
} elseif ( preg_match( ':' . preg_quote( trailingslashit( wp_normalize_path( WPMU_PLUGIN_DIR ) ), ':' ) . $slug_pattern . ':s', $file, $matches ) ) {
$source['type'] = 'mu-plugin';
$source['name'] = $matches[1];
} elseif ( preg_match( ':' . preg_quote( trailingslashit( wp_normalize_path( ABSPATH ) ), ':' ) . '(wp-admin|wp-includes)/:s', $file, $matches ) ) {
$source['type'] = 'core';
$source['name'] = $matches[1];
}
}

if ( isset( $type, $name ) ) {
return compact( 'type', 'name' );
if ( $class_name ) {
$source['function'] = $class_name . '::' . $reflection->getName();
} else {
$source['function'] = $reflection->getName();
}
return null;

return $source;
}

/**
Expand All @@ -822,9 +847,7 @@ public static function get_source( $callback ) {
*
* @type callable $function
* @type int $accepted_args
* @type string $type
* @type string $source
* @type string $hook
* @type array $source
* }
* @return closure $wrapped_callback The callback, wrapped in comments.
*/
Expand Down Expand Up @@ -852,40 +875,22 @@ public static function wrapped_callback( $callback ) {
// Keep track of which source enqueued the styles.
if ( isset( $wp_styles ) && isset( $wp_styles->queue ) ) {
foreach ( array_diff( $wp_styles->queue, $before_styles_enqueued ) as $handle ) {
$source = array_merge(
wp_array_slice_assoc( $callback, array( 'type', 'name' ) ),
array(
'args' => array(
'hook' => $callback['hook'],
),
)
);

AMP_Validation_Utils::$enqueued_style_sources[ $handle ][] = $source;
AMP_Validation_Utils::$enqueued_style_sources[ $handle ][] = $callback['source'];
}
}

// Keep track of which source enqueued the scripts, and immediately report validity .
if ( isset( $wp_scripts ) && isset( $wp_scripts->queue ) ) {
foreach ( array_diff( $wp_scripts->queue, $before_scripts_enqueued ) as $handle ) {
$source = array_merge(
wp_array_slice_assoc( $callback, array( 'type', 'name' ) ),
array(
'args' => array(
'hook' => $callback['hook'],
),
)
);

AMP_Validation_Utils::$enqueued_script_sources[ $handle ][] = $source;
AMP_Validation_Utils::$enqueued_script_sources[ $handle ][] = $callback['source'];

if ( isset( $wp_scripts->registered[ $handle ] ) ) {
self::add_validation_error( array(
'code' => self::ENQUEUED_SCRIPT_CODE,
'handle' => $handle,
'dependency' => $wp_scripts->registered[ $handle ],
'sources' => array(
$source,
$callback['source'],
),
) );
}
Expand All @@ -894,9 +899,9 @@ public static function wrapped_callback( $callback ) {

// Wrap output that contains HTML tags (as opposed to actions that trigger in HTML attributes).
if ( ! empty( $output ) && preg_match( '/<.+?>/s', $output ) ) {
echo AMP_Validation_Utils::get_source_comment_start( $callback['type'], $callback['name'], array( 'hook' => $callback['hook'] ) ); // WPCS: XSS ok.
echo AMP_Validation_Utils::get_source_comment( $callback['source'], true ); // WPCS: XSS ok.
echo $output; // WPCS: XSS ok.
echo AMP_Validation_Utils::get_source_comment_end( $callback['type'], $callback['name'] ); // WPCS: XSS ok.
echo AMP_Validation_Utils::get_source_comment( $callback['source'], false ); // WPCS: XSS ok.
}
return $result;
};
Expand Down
Loading

0 comments on commit 3d82d39

Please sign in to comment.