diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php index 539e3d69f9b..b070f213a4a 100644 --- a/includes/utils/class-amp-validation-utils.php +++ b/includes/utils/class-amp-validation-utils.php @@ -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. * @@ -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' ) ); } @@ -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']; + } } } @@ -715,6 +732,7 @@ 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'] ); @@ -722,16 +740,21 @@ public static function wrap_hook_callbacks( $hook ) { 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']; @@ -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. * @@ -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] ); } diff --git a/tests/test-class-amp-validation-utils.php b/tests/test-class-amp-validation-utils.php index fd0ec9e091b..4e71c1a3b6e 100644 --- a/tests/test-class-amp-validation-utils.php +++ b/tests/test-class-amp-validation-utils.php @@ -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' ) ) ); } @@ -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 'test'; } ); - $this->assertSame( - 'beforetestafter', - 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( + "testafter

' . "\n", + "