cabrerahector / wordpress-popular-posts

WordPress Popular Posts - A highly customizable WordPress widget that displays your most popular posts.
https://wordpress.org/plugins/wordpress-popular-posts/
GNU General Public License v2.0
280 stars 82 forks source link

`<picture>` tags get stripped when used within `wpp_custom_html` filter (since >= `v5.3.5`) #349

Closed E-VANCE closed 1 year ago

E-VANCE commented 1 year ago

Describe the bug I am using the wpp_custom_html()-filter in order to output custom HTML for our most viewed posts widget. We are using an image generation helper class called Responsive.pics which renders a <picture>-tag like this:

<picture>
  <source media="(min-width: 1024px)" srcset="https://my.site/app/uploads/2012/12/test_img-100x100-crop-50-50.jpg 1x, https://my.site/app/uploads/2012/12/test_img-100x100-crop-50-50@2x.jpg 2x">
  <source media="(min-width: 768px)" srcset="https://my.site/app/uploads/2012/12/test_img-240x180-crop-50-50.jpg 1x, https://my.site/app/uploads/2012/12/test_img-240x180-crop-50-50@2x.jpg 2x">
  <source media="(min-width: 360px)" srcset="https://my.site/app/uploads/2012/12/test_img-640x360-crop-50-50.jpg">
  <source media="(min-width: 0px)" srcset="https://my.site/app/uploads/2012/12/test_img.jpg">
  <img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Test Img">
</picture>

After upgrading from v5.3.5 to both v5.5.1 as well as the latest v6.1.1 our custom markup breaks in the sense that there is some filtering happening and the <picture>-tag gets stripped completely whilst only the <img>-tag is being used.

This in turn renders the whole output unusable and I would expect to not have the custom markup be filtered at all...

Could the image filtering be excluded from touching custom markup via the wpp_custom_html()-filter? Or is there a setting I could use? Tried altering the backend image settings, checked the wiki and the source code but couldn't find anything that would apply here.

Screenshots CleanShot 2022-12-16 at 11 24 38

Let me know if you need more details / system specs – omitted those here since I thought you'd know what causes this and it's unrelated to our specific environment...

Thanks so much for your effort!

cabrerahector commented 1 year ago

Hi @E-VANCE,

Could you please explain in more detail how exactly you're using this class to generate this <picture> tag and how you're adding it to wpp_get_mostpopular() so I can reproduce the issue you're describing?

E-VANCE commented 1 year ago

Hi @cabrerahector,

sure thing – this is the relevant part in the filter markup:

if(has_post_thumbnail($post->id)) {
  if($i > 2) {
    $output .= '<div class="thumb lg:flex-shrink-0 lg:mr-4">' . ResponsivePics::get_picture(get_post_thumbnail_id($post->id), "xs:640 360|f, md:240 180|f, lg:100 100|f", "swiper-lazy", true);
    $output .= '<div class="swiper-lazy-preloader"></div></div>';
  } else {
    $output .= '<div class="thumb lg:flex-shrink-0 lg:mr-4">' . ResponsivePics::get_picture(get_post_thumbnail_id($post->id), "xs:640 360|f, md:240 180|f, lg:100 100|f") . '</div>';
  }
}

You can see the whole widget in action here: https://profashionals.de/ (= the "Meistgelesen"-widget in the sidebar).

Just wondering what has changed since v5.3.5 to this regard and how I can "protect" the <picture>-tags from being re-written... Thought about obfuscating / encoding the relevant parts but that seems like the wrong approach 😄

cabrerahector commented 1 year ago

I was hoping to see more of a "copy & paste" thing so I don't need to write code to reproduce the issue 😅 (and I'm pretty sure the issue template includes a "Steps to reproduce" section precisely for this.)

Anyways, I'll try using your code and get back to you as soon as I can.

E-VANCE commented 1 year ago

@cabrerahector

OK, no worries – didn't know what you were aiming for... Steps for reproducing this:

/**
 * Alter 'WordPress Popular Posts' HTML output
 *
 * @param  array $popular_posts
 * @param  array $instance
 * @return string
 */
function custom_popular_posts_html($popular_posts, $instance) {
  $output = '<div class="wrapper">';

  // Loop over the array of popular posts objects
  foreach($popular_posts as $post) {
    $output = '<div class="item">';

    // Image
    if(has_post_thumbnail($post->id)) {
      $image = ResponsivePics::get_picture(get_post_thumbnail_id($post->id), '0:150 150|f');
      var_dump($image); // Debug
      $output .= '<div class="thumb">';
      $output .= $image;
      $output .= '</div>';
    }

    $output .= '<div class="title">' . $post->title . '</div>';
    $output .= '</div>';
  }

  $output .= '</div>';

  return $output;
}
add_filter('wpp_custom_html', 'custom_popular_posts_html', 10, 2);

Haven't tested this but should be fairly straight forward.

cabrerahector commented 1 year ago

Alright, so the reason why <picture> tags are being stripped out of the HTML output is due to the usage of wp_kses_allowed_html(). This is a core WordPress function that basically provides a list of "allowed" HTML tags, that is HTML tags -and attributes- that are considered to be safe to use (as in that are unlikely to be exploited by malicious users).

To improve security when using the [wpp] shortcode (the wpp_get_mostpopular() function is actually a wrapper that uses this shortcode internally to render posts outside post/page content), and as you may have already guessed by now, WordPress Popular Posts started using this function since version 5.3.6. And so it happens that wp_kses_allowed_html() doesn't include <picture> nor <source> in the list of "allowed" tags. Now you know why this is happening :P

Fortunately, wp_kses_allowed_html() does include filter hooks so you can modify the list of allowed HTML tags. To save you some time, this is what you want:

/**
 * Modifies wp_kses_allowed_html() to allow the usage of picture tags.
 *
 * @see     https://developer.wordpress.org/reference/functions/wp_kses_allowed_html/
 *
 * @author  Hector Cabrera
 * @param   array   $allowedposttags
 * @param   string  $context
 * @return  array   $allowedposttags
 */
function wp5560_kses_allow_picture_tags($allowedposttags, $context) {
    $allowedposttags['picture'] = [];
    $allowedposttags['source'] = [
        'media' => true,
        'srcset' => true
    ];

    return $allowedposttags;
}
add_filter('wp_kses_allowed_html', 'wp5560_kses_allow_picture_tags', 10, 2);
E-VANCE commented 1 year ago

Hey @cabrerahector,

many many thanks for looking into this and helping out here – really appreciate it. And all the effort you are putting into providing WPP as a great tool for so many developers...!

And so it happens that wp_kses_allowed_html() doesn't include <picture> nor <source> in the list of "allowed" tags. Now you know why this is happening :P

Completely makes sense and above all an easily applied fix 🚀

Have a great holiday time and thanks again.