%4$s
', "
" ), "
", $text ); // visual editor likes to add
s. Buh-bye. $text = $this->get_parser()->unp( $text ); // sometimes we get an encoded > at start of line, breaking blockquotes. $text = preg_replace( '/^>/m', '>', $text ); // prefixes are because we need to namespace footnotes by post_id. $this->get_parser()->fn_id_prefix = $args['id'] ? $args['id'] . '-' : ''; // If we're not using the code shortcode, prevent over-encoding. if ( $args['decode_code_blocks'] ) { $text = $this->get_parser()->codeblock_restore( $text ); } // Transform it! $text = $this->get_parser()->transform( $text ); // Fix footnotes - kses doesn't like the : IDs it supplies. $text = preg_replace( '/((id|href)="#?fn(ref)?):/', '$1-', $text ); // Markdown inserts extra spaces to make itself work. Buh-bye. $text = rtrim( $text ); /** * Filter the content to be run through Markdown, after it was transformed by Markdown. * * @module markdown * * @since 2.8.0 * * @param string $text Content to be run through Markdown * @param array $args Array of Markdown options. */ $text = apply_filters( 'wpcom_markdown_transform_post', $text, $args ); // probably need to re-slash. if ( $args['unslash'] ) { $text = wp_slash( $text ); } return $text; } /** * Shows Markdown in the Revisions screen, and ensures that post_content_filtered * is maintained on revisions * * @param array $fields Post fields pertinent to revisions. */ public function wp_post_revision_fields( $fields ) { $fields['post_content_filtered'] = __( 'Markdown content', 'jetpack' ); return $fields; } /** * Do some song and dance to keep all post_content and post_content_filtered content * in the expected place when a post revision is restored. * * @param int $post_id The post ID have a restore done to it. * @param int $revision_id The revision ID being restored. */ public function wp_restore_post_revision( $post_id, $revision_id ) { if ( $this->is_markdown( $revision_id ) ) { $revision = get_post( $revision_id, ARRAY_A ); $post = get_post( $post_id, ARRAY_A ); $post['post_content'] = $revision['post_content_filtered']; // Yes, we put it in post_content, because our wp_insert_post_data() expects that. // set this flag so we can restore the post_content_filtered on the last revision later. $this->monitoring['restore'] = true; // let's not make a revision of our fixing update. add_filter( 'wp_revisions_to_keep', '__return_false', 99 ); wp_update_post( $post ); $this->fix_latest_revision_on_restore( $post_id ); remove_filter( 'wp_revisions_to_keep', '__return_false', 99 ); } } /** * We need to ensure the last revision has Markdown, not HTML in its post_content_filtered * column after a restore. * * @param int $post_id The post ID that was just restored. */ protected function fix_latest_revision_on_restore( $post_id ) { global $wpdb; $post = get_post( $post_id ); $last_revision = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE post_type = 'revision' AND post_parent = %d ORDER BY ID DESC", $post->ID ) ); $last_revision->post_content_filtered = $post->post_content_filtered; wp_insert_post( (array) $last_revision ); } /** * Kicks off magic for an XML-RPC session. We want to keep editing Markdown * and publishing HTML. * * @param string $xmlrpc_method The current XML-RPC method. */ public function xmlrpc_actions( $xmlrpc_method ) { switch ( $xmlrpc_method ) { case 'metaWeblog.getRecentPosts': case 'wp.getPosts': case 'wp.getPages': add_action( 'parse_query', array( $this, 'make_filterable' ), 10, 1 ); break; case 'wp.getPost': $this->prime_post_cache(); break; } } /** * Function metaWeblog.getPost and wp.getPage fire xmlrpc_call action *after* get_post() is called. * So, we have to detect those methods and prime the post cache early. * * @return null */ protected function check_for_early_methods() { $raw_post_data = file_get_contents( 'php://input' ); if ( ! str_contains( $raw_post_data, 'metaWeblog.getPost' ) && ! str_contains( $raw_post_data, 'wp.getPage' ) ) { return; } include_once ABSPATH . WPINC . '/class-IXR.php'; $message = new IXR_Message( $raw_post_data ); $message->parse(); $post_id_position = 'metaWeblog.getPost' === $message->methodName ? 0 : 1; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $this->prime_post_cache( $message->params[ $post_id_position ] ?? false ); } /** * Prime the post cache with swapped post_content. This is a sneaky way of getting around * the fact that there are no good hooks to call on the *.getPost xmlrpc methods. * * @param bool $post_id - the post ID that we're priming. */ private function prime_post_cache( $post_id = false ) { global $wp_xmlrpc_server; if ( ! $post_id ) { if ( isset( $wp_xmlrpc_server->message->params[3] ) ) { $post_id = $wp_xmlrpc_server->message->params[3]; } else { return; // Exit early if we can't get a valid post_id } } // prime the post cache. if ( $this->is_markdown( $post_id ) ) { $post = get_post( $post_id ); if ( ! empty( $post->post_content_filtered ) ) { wp_cache_delete( $post->ID, 'posts' ); $post = $this->swap_for_editing( $post ); wp_cache_add( $post->ID, $post, 'posts' ); $this->posts_to_uncache[] = $post_id; } } // uncache munged posts if using a persistent object cache. if ( wp_using_ext_object_cache() ) { add_action( 'shutdown', array( $this, 'uncache_munged_posts' ) ); } } /** * Swaps `post_content_filtered` back to `post_content` for editing purposes. * * @param object $post WP_Post object. * @return object WP_Post object with swapped `post_content_filtered` and `post_content`. */ protected function swap_for_editing( $post ) { $markdown = $post->post_content_filtered; // unencode encoded code blocks. $markdown = $this->get_parser()->codeblock_restore( $markdown ); // restore beginning of line blockquotes. $markdown = preg_replace( '/^> /m', '> ', $markdown ); $post->post_content_filtered = $post->post_content; $post->post_content = $markdown; return $post; } /** * We munge the post cache to serve proper markdown content to XML-RPC clients. * Uncache these after the XML-RPC session ends. */ public function uncache_munged_posts() { // $this context gets lost in testing sometimes. Weird. foreach ( self::get_instance()->posts_to_uncache as $post_id ) { wp_cache_delete( $post_id, 'posts' ); } } /** * Since *.(get)?[Rr]ecentPosts calls get_posts with suppress filters on, we need to * turn them back on so that we can swap things for editing. * * @param object $wp_query WP_Query object. */ public function make_filterable( $wp_query ) { $wp_query->set( 'suppress_filters', false ); add_action( 'the_posts', array( $this, 'the_posts' ), 10, 2 ); } /** * Swaps post_content and post_content_filtered for editing. * * @param array $posts Posts returned by the just-completed query. * @return array Modified $posts */ public function the_posts( $posts ) { foreach ( $posts as $key => $post ) { if ( $this->is_markdown( $post->ID ) && ! empty( $posts[ $key ]->post_content_filtered ) ) { $markdown = $posts[ $key ]->post_content_filtered; $posts[ $key ]->post_content_filtered = $posts[ $key ]->post_content; $posts[ $key ]->post_content = $markdown; } } return $posts; } /** * Singleton silence is golden */ private function __construct() {} } add_action( 'init', array( WPCom_Markdown::get_instance(), 'load' ) );