Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Comment thread
mcliwanow marked this conversation as resolved.

// 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.
Expand Down Expand Up @@ -256,6 +259,35 @@ 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 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.
* 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
*/
public static function render_content_as_plain_text( $content ) {
if ( Registrations::get_post_type() !== get_post_type() ) {
Comment thread
mcliwanow marked this conversation as resolved.
return $content;
}

$content = esc_html( $content );
Comment thread
mcliwanow marked this conversation as resolved.

// Shortcode and URL syntax stay visible text: hide the characters shortcodes and embeds key on.
return str_replace( [ '[', '://' ], [ '[', '://' ], $content );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

esc_html() calls _wp_specialchars( $text, ENT_QUOTES, 'UTF-8', false ), and with $double_encode = false an already-valid entity passes through untouched. A description submitted as the literal nine characters & survives sanitize_textarea_field intact and then renders as a single &, and [gallery] renders as [gallery].

Neither is exploitable, do_shortcode matches only a literal [, but it contradicts the stated contract that the submitted text is shown verbatim. htmlspecialchars( $content, ENT_QUOTES, 'UTF-8', true ) would be faithful.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's intentional. Intake goes through wp_filter_post_kses(), which stores a typed & as &, so the output has to leave existing entities alone or every ampersand ever submitted shows up as &. From the sandbox, including a real published row:

image

}

/**
* Syncs the photo post content to the caption for the associated photo media.
*
Expand Down