From 56b7928ea2f22224296e8e35c4f7ca0ab87a5b1f Mon Sep 17 00:00:00 2001 From: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com> Date: Wed, 2 Sep 2026 10:58:06 +0200 Subject: [PATCH 1/4] Photo Directory: render the photo description as plain text The description submitted with a photo is its alternative text, and the submit form sanitizes it as plain text on the way in. On output it was still run through the regular post content filters, so text that looks like markup was interpreted instead of displayed. Escape it before those filters run, keeping paragraphs and the visible text as submitted. Co-Authored-By: Claude Fable 5.1 --- .../plugins/photo-directory/inc/posts.php | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/wordpress.org/public_html/wp-content/plugins/photo-directory/inc/posts.php b/wordpress.org/public_html/wp-content/plugins/photo-directory/inc/posts.php index 8e2e5b326f..8fc6b0d587 100644 --- a/wordpress.org/public_html/wp-content/plugins/photo-directory/inc/posts.php +++ b/wordpress.org/public_html/wp-content/plugins/photo-directory/inc/posts.php @@ -35,6 +35,9 @@ public static function init() { // Sync photo post content to photo media on update. add_action( 'post_updated', [ __CLASS__, 'sync_photo_post_to_photo_media_on_update' ], 5, 3 ); + // Photo content is plain text (the alternative text), never post markup. + add_filter( 'the_content', [ __CLASS__, 'render_content_as_plain_text' ], PHP_INT_MIN ); + // Offset subsequent paginations of front page by number of posts on front page. add_action( 'pre_get_posts', [ __CLASS__, 'offset_front_page_paginations' ], 11 ); // Fix pages count for front page paginations. @@ -256,6 +259,42 @@ public static function use_photo_url_instead_of_media_permalink_url( $url, $post return wp_get_attachment_url( $post_id ); } + /** + * Renders a photo's content as the plain text it is. + * + * A photo's content is the alternative text submitted with it. The submit + * form and its sanitization treat that as plain text, so the content must + * not be interpreted as post markup on output either. It is escaped here, + * ahead of every other 'the_content' callback, so that blocks, shortcodes, + * and embeds only ever see text. + * + * @param string $content Post content. + * @return string + */ + public static function render_content_as_plain_text( $content ) { + if ( Registrations::get_post_type() !== get_post_type() ) { + return $content; + } + + return self::plain_text_to_html( $content ); + } + + /** + * Converts plain text into paragraphs that later content filters leave as text. + * + * @param string $text Plain text. + * @return string + */ + public static function plain_text_to_html( $text ) { + $html = esc_html( $text ); + + // Shortcode syntax is part of the text; keep `do_shortcode()` from seeing its delimiter. + $html = str_replace( '[', '[', $html ); + + // Wrap paragraphs now, so a URL on a line of its own is not auto-embedded. + return wpautop( $html ); + } + /** * Syncs the photo post content to the caption for the associated photo media. * From f7fc8f43b535c6d3474a80a207e23012f360595c Mon Sep 17 00:00:00 2001 From: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:33:32 +0200 Subject: [PATCH 2/4] Photo Directory: unhook the embed callbacks while rendering a photo description WP_Embed::autoembed() also matches a URL that is alone inside a paragraph, so wrapping the escaped text first did not keep a URL-only description from being embedded, and the oEmbed lookup writes cache meta to the photo on a public request. Remove run_shortcode and autoembed for the photo and hook them back once its content has been filtered. Drop the pre-wrap and make the escaping helper private. Co-Authored-By: Claude Fable 5.1 --- .../plugins/photo-directory/inc/posts.php | 67 ++++++++++++++++--- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/wordpress.org/public_html/wp-content/plugins/photo-directory/inc/posts.php b/wordpress.org/public_html/wp-content/plugins/photo-directory/inc/posts.php index 8fc6b0d587..1194caf929 100644 --- a/wordpress.org/public_html/wp-content/plugins/photo-directory/inc/posts.php +++ b/wordpress.org/public_html/wp-content/plugins/photo-directory/inc/posts.php @@ -37,6 +37,7 @@ public static function init() { // Photo content is plain text (the alternative text), never post markup. add_filter( 'the_content', [ __CLASS__, 'render_content_as_plain_text' ], PHP_INT_MIN ); + add_filter( 'the_content', [ __CLASS__, 'restore_embed_filters' ], PHP_INT_MAX ); // Offset subsequent paginations of front page by number of posts on front page. add_action( 'pre_get_posts', [ __CLASS__, 'offset_front_page_paginations' ], 11 ); @@ -259,14 +260,27 @@ public static function use_photo_url_instead_of_media_permalink_url( $url, $post return wp_get_attachment_url( $post_id ); } + /** + * `WP_Embed` callbacks unhooked while a photo's content is being filtered. + * + * @var string[] + */ + private static $removed_embed_filters = []; + /** * Renders a photo's content as the plain text it is. * * A photo's content is the alternative text submitted with it. The submit * form and its sanitization treat that as plain text, so the content must * not be interpreted as post markup on output either. It is escaped here, - * ahead of every other 'the_content' callback, so that blocks, shortcodes, - * and embeds only ever see text. + * ahead of every other 'the_content' callback, so that blocks and + * shortcodes only ever see text. The `WP_Embed` callbacks are unhooked for + * the rest of this run, since a bare URL would otherwise be embedded + * whether it sits on its own line or inside a paragraph, and hooked back + * up by `restore_embed_filters()`. + * + * Keys on the global post, like core's own content callbacks, so it applies + * to whatever 'the_content' is run for while a photo is the current post. * * @param string $content Post content. * @return string @@ -276,23 +290,60 @@ public static function render_content_as_plain_text( $content ) { return $content; } + $removed = self::remove_embed_filters(); + if ( $removed ) { + self::$removed_embed_filters = $removed; + } + return self::plain_text_to_html( $content ); } /** - * Converts plain text into paragraphs that later content filters leave as text. + * Hooks the `WP_Embed` callbacks back up once a photo's content has been filtered. + * + * @param string $content Filtered post content. + * @return string + */ + public static function restore_embed_filters( $content ) { + foreach ( self::$removed_embed_filters as $method ) { + add_filter( 'the_content', [ $GLOBALS['wp_embed'], $method ], 8 ); + } + self::$removed_embed_filters = []; + + return $content; + } + + /** + * Unhooks the `WP_Embed` 'the_content' callbacks. + * + * @return string[] Names of the callbacks that were hooked and are now removed. + */ + private static function remove_embed_filters() { + $removed = []; + $wp_embed = $GLOBALS['wp_embed'] ?? null; + + if ( $wp_embed instanceof \WP_Embed ) { + foreach ( [ 'run_shortcode', 'autoembed' ] as $method ) { + if ( remove_filter( 'the_content', [ $wp_embed, $method ], 8 ) ) { + $removed[] = $method; + } + } + } + + return $removed; + } + + /** + * Escapes plain text so that later content filters leave it as text. * * @param string $text Plain text. * @return string */ - public static function plain_text_to_html( $text ) { + private static function plain_text_to_html( $text ) { $html = esc_html( $text ); // Shortcode syntax is part of the text; keep `do_shortcode()` from seeing its delimiter. - $html = str_replace( '[', '[', $html ); - - // Wrap paragraphs now, so a URL on a line of its own is not auto-embedded. - return wpautop( $html ); + return str_replace( '[', '[', $html ); } /** From f133f2c93f41561c7de901f8fa4fcdbb43383587 Mon Sep 17 00:00:00 2001 From: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com> Date: Wed, 2 Sep 2026 16:50:12 +0200 Subject: [PATCH 3/4] Photo Directory: simplify the plain-text rendering of the description Encode the characters later content filters key on instead of unhooking and rehooking them. Co-Authored-By: Claude Fable 5.1 --- .../plugins/photo-directory/inc/posts.php | 71 ++----------------- 1 file changed, 5 insertions(+), 66 deletions(-) diff --git a/wordpress.org/public_html/wp-content/plugins/photo-directory/inc/posts.php b/wordpress.org/public_html/wp-content/plugins/photo-directory/inc/posts.php index 1194caf929..782d1cfc5c 100644 --- a/wordpress.org/public_html/wp-content/plugins/photo-directory/inc/posts.php +++ b/wordpress.org/public_html/wp-content/plugins/photo-directory/inc/posts.php @@ -37,7 +37,6 @@ public static function init() { // Photo content is plain text (the alternative text), never post markup. add_filter( 'the_content', [ __CLASS__, 'render_content_as_plain_text' ], PHP_INT_MIN ); - add_filter( 'the_content', [ __CLASS__, 'restore_embed_filters' ], PHP_INT_MAX ); // Offset subsequent paginations of front page by number of posts on front page. add_action( 'pre_get_posts', [ __CLASS__, 'offset_front_page_paginations' ], 11 ); @@ -260,24 +259,14 @@ public static function use_photo_url_instead_of_media_permalink_url( $url, $post return wp_get_attachment_url( $post_id ); } - /** - * `WP_Embed` callbacks unhooked while a photo's content is being filtered. - * - * @var string[] - */ - private static $removed_embed_filters = []; - /** * Renders a photo's content as the plain text it is. * * A photo's content is the alternative text submitted with it. The submit * form and its sanitization treat that as plain text, so the content must * not be interpreted as post markup on output either. It is escaped here, - * ahead of every other 'the_content' callback, so that blocks and - * shortcodes only ever see text. The `WP_Embed` callbacks are unhooked for - * the rest of this run, since a bare URL would otherwise be embedded - * whether it sits on its own line or inside a paragraph, and hooked back - * up by `restore_embed_filters()`. + * ahead of every other 'the_content' callback, so that they only ever see + * text. * * Keys on the global post, like core's own content callbacks, so it applies * to whatever 'the_content' is run for while a photo is the current post. @@ -290,60 +279,10 @@ public static function render_content_as_plain_text( $content ) { return $content; } - $removed = self::remove_embed_filters(); - if ( $removed ) { - self::$removed_embed_filters = $removed; - } - - return self::plain_text_to_html( $content ); - } - - /** - * Hooks the `WP_Embed` callbacks back up once a photo's content has been filtered. - * - * @param string $content Filtered post content. - * @return string - */ - public static function restore_embed_filters( $content ) { - foreach ( self::$removed_embed_filters as $method ) { - add_filter( 'the_content', [ $GLOBALS['wp_embed'], $method ], 8 ); - } - self::$removed_embed_filters = []; - - return $content; - } - - /** - * Unhooks the `WP_Embed` 'the_content' callbacks. - * - * @return string[] Names of the callbacks that were hooked and are now removed. - */ - private static function remove_embed_filters() { - $removed = []; - $wp_embed = $GLOBALS['wp_embed'] ?? null; - - if ( $wp_embed instanceof \WP_Embed ) { - foreach ( [ 'run_shortcode', 'autoembed' ] as $method ) { - if ( remove_filter( 'the_content', [ $wp_embed, $method ], 8 ) ) { - $removed[] = $method; - } - } - } - - return $removed; - } - - /** - * Escapes plain text so that later content filters leave it as text. - * - * @param string $text Plain text. - * @return string - */ - private static function plain_text_to_html( $text ) { - $html = esc_html( $text ); + $content = esc_html( $content ); - // Shortcode syntax is part of the text; keep `do_shortcode()` from seeing its delimiter. - return str_replace( '[', '[', $html ); + // Shortcode and URL syntax stay visible text: hide the characters shortcodes and embeds key on. + return str_replace( [ '[', '://' ], [ '[', '://' ], $content ); } /** From e961633f78f3e3c8de42017cf5b976cdcc826c9b Mon Sep 17 00:00:00 2001 From: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com> Date: Thu, 3 Sep 2026 10:23:31 +0200 Subject: [PATCH 4/4] Photo Directory: note the global-post trade-off in the description filter docblock Co-Authored-By: Claude Fable 5.1 --- .../wp-content/plugins/photo-directory/inc/posts.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wordpress.org/public_html/wp-content/plugins/photo-directory/inc/posts.php b/wordpress.org/public_html/wp-content/plugins/photo-directory/inc/posts.php index 782d1cfc5c..088e310903 100644 --- a/wordpress.org/public_html/wp-content/plugins/photo-directory/inc/posts.php +++ b/wordpress.org/public_html/wp-content/plugins/photo-directory/inc/posts.php @@ -270,6 +270,9 @@ public static function use_photo_url_instead_of_media_permalink_url( $url, $post * * Keys on the global post, like core's own content callbacks, so it applies * to whatever 'the_content' is run for while a photo is the current post. + * The reverse also holds: a photo's content filtered while another post is + * global, such as an excerpt built outside the loop, is not escaped here. + * Nothing on the site does that. * * @param string $content Post content. * @return string