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

Keep track of the callbacks run (and their sources) when a filter is applied #1016

Merged
merged 1 commit into from
Mar 13, 2018
Merged
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
65 changes: 61 additions & 4 deletions includes/utils/class-amp-validation-utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,15 @@ class AMP_Validation_Utils {
*/
public static $posts_pending_frontend_validation = array();

/**
* Current sources gathered for a given hook currently being run.
*
* @see AMP_Validation_Utils::wrap_hook_callbacks()
* @see AMP_Validation_Utils::decorate_filter_source()
* @var array[]
*/
protected static $current_hook_source_stack = array();

/**
* Add the actions.
*
Expand Down Expand Up @@ -285,7 +294,13 @@ public static function print_dashboard_glance_styles() {
*/
public static function add_validation_hooks() {
self::wrap_widget_callbacks();

add_action( 'all', array( __CLASS__, 'wrap_hook_callbacks' ) );
$wrapped_filters = array( 'the_content', 'the_excerpt' );
foreach ( $wrapped_filters as $wrapped_filter ) {
add_filter( $wrapped_filter, array( __CLASS__, 'decorate_filter_source' ), PHP_INT_MAX );
}

add_filter( 'do_shortcode_tag', array( __CLASS__, 'decorate_shortcode_source' ), -1, 2 );
add_filter( 'amp_content_sanitizers', array( __CLASS__, 'add_validation_callback' ) );
}
Expand Down Expand Up @@ -470,7 +485,9 @@ public static function summarize_validation_errors( $validation_errors ) {
if ( ! empty( $validation_error['sources'] ) ) {
$source = array_pop( $validation_error['sources'] );

$invalid_sources[ $source['type'] ][] = $source['name'];
if ( isset( $source['type'], $source['name'] ) ) {
$invalid_sources[ $source['type'] ][] = $source['name'];
}
}
}

Expand Down Expand Up @@ -715,23 +732,29 @@ public static function wrap_hook_callbacks( $hook ) {
return;
}

self::$current_hook_source_stack[ $hook ] = array();
foreach ( $wp_filter[ $hook ]->callbacks as $priority => &$callbacks ) {
foreach ( $callbacks as &$callback ) {
$source = self::get_source( $callback['function'] );
if ( ! $source ) {
continue;
}

$reflection = $source['reflection'];
unset( $source['reflection'] ); // Omit from stored source.

// Add hook to stack for decorate_filter_source to read from.
self::$current_hook_source_stack[ $hook ][] = $source;

/*
* A current limitation with wrapping callbacks is that the wrapped function cannot have
* any parameters passed by reference. Without this the result is:
*
* > PHP Warning: Parameter 1 to wp_default_styles() expected to be a reference, value given.
*/
if ( self::has_parameters_passed_by_reference( $source['reflection'] ) ) {
if ( self::has_parameters_passed_by_reference( $reflection ) ) {
continue;
}
unset( $source['reflection'] ); // No longer needed.

$source['hook'] = $hook;
$original_function = $callback['function'];
Expand Down Expand Up @@ -793,6 +816,40 @@ public static function decorate_shortcode_source( $output, $tag ) {
return $output;
}

/**
* Wraps output of a filter to add source stack comments.
*
* @param string $value Value.
* @return string Value wrapped in source comments.
*/
public static function decorate_filter_source( $value ) {

// Abort if the output is not a string and it doesn't contain any HTML tags.
if ( ! is_string( $value ) || ! preg_match( '/<.+?>/s', $value ) ) {
return $value;
}

$post = get_post();
$source = array(
'hook' => current_filter(),
'filter' => true,
);
if ( $post ) {
$source['post_id'] = $post->ID;
$source['post_type'] = $post->post_type;
}
if ( isset( self::$current_hook_source_stack[ current_filter() ] ) ) {
$sources = self::$current_hook_source_stack[ current_filter() ];
array_pop( $sources ); // Remove self.
$source['sources'] = $sources;
}
return implode( '', array(
self::get_source_comment( $source, true ),
$value,
self::get_source_comment( $source, false ),
) );
}

/**
* Gets the plugin or theme of the callback, if one exists.
*
Expand Down Expand Up @@ -822,7 +879,7 @@ public static function get_source( $callback ) {
} elseif ( is_array( $callback ) && isset( $callback[0], $callback[1] ) && method_exists( $callback[0], $callback[1] ) ) {
// The $callback is a method.
if ( is_string( $callback[0] ) ) {
$class_name = '\'' . $callback[0];
$class_name = $callback[0];
} elseif ( is_object( $callback[0] ) ) {
$class_name = get_class( $callback[0] );
}
Expand Down
16 changes: 11 additions & 5 deletions tests/test-class-amp-validation-utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ public function test_init() {
*/
public function test_add_validation_hooks() {
AMP_Validation_Utils::add_validation_hooks();
$this->assertEquals( PHP_INT_MAX, has_filter( 'the_content', array( self::TESTED_CLASS, 'decorate_filter_source' ) ) );
$this->assertEquals( PHP_INT_MAX, has_filter( 'the_excerpt', array( self::TESTED_CLASS, 'decorate_filter_source' ) ) );
$this->assertEquals( 10, has_action( 'amp_content_sanitizers', array( self::TESTED_CLASS, 'add_validation_callback' ) ) );
$this->assertEquals( -1, has_action( 'do_shortcode_tag', array( self::TESTED_CLASS, 'decorate_shortcode_source' ) ) );
}
Expand Down Expand Up @@ -526,17 +528,21 @@ public function test_callback_wrappers() {
* Test decorate_shortcode_source.
*
* @covers AMP_Validation_Utils::decorate_shortcode_source()
* @covers AMP_Validation_Utils::decorate_filter_source()
*/
public function test_decorate_shortcode_source() {
public function test_decorate_shortcode_and_filter_source() {
AMP_Validation_Utils::add_validation_hooks();
add_shortcode( 'test', function() {
return '<b>test</b>';
} );

$this->assertSame(
'before<!--amp-source-stack {"type":"plugin","name":"amp","function":"{closure}","shortcode":"test"}--><b>test</b><!--/amp-source-stack {"type":"plugin","name":"amp","function":"{closure}","shortcode":"test"}-->after',
do_shortcode( 'before[test]after' )
);
$filtered_content = apply_filters( 'the_content', 'before[test]after' );
$source_json = '{"hook":"the_content","filter":true,"sources":[{"type":"core","name":"wp-includes","function":"WP_Embed::run_shortcode"},{"type":"core","name":"wp-includes","function":"WP_Embed::autoembed"},{"type":"core","name":"wp-includes","function":"wptexturize"},{"type":"core","name":"wp-includes","function":"wpautop"},{"type":"core","name":"wp-includes","function":"shortcode_unautop"},{"type":"core","name":"wp-includes","function":"prepend_attachment"},{"type":"core","name":"wp-includes","function":"wp_make_content_images_responsive"},{"type":"core","name":"wp-includes","function":"capital_P_dangit"},{"type":"core","name":"wp-includes","function":"do_shortcode"},{"type":"core","name":"wp-includes","function":"convert_smilies"}]}';
$expected_content = implode( '', array(
"<!--amp-source-stack $source_json>",
'<p>before<!--amp-source-stack {"type":"plugin","name":"amp","function":"{closure}","shortcode":"test"}--><b>test</b><!--/amp-source-stack {"type":"plugin","name":"amp","function":"{closure}","shortcode":"test"}-->after</p>' . "\n",
"<!--/amp-source-stack $source_json>",
) );
}

/**
Expand Down