PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | modules/markdown/easy-markdown.php +899 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,899 @@
1 +<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 +/**
3 + * Plugin URI: https://automattic.com/
4 + * Plugin Name: Easy Markdown
5 + * Description: Write in Markdown, publish in WordPress
6 + * Version: 0.1
7 + * Author: Matt Wiebe
8 + * Author URI: https://automattic.com/
9 + * Text Domain: jetpack
10 + *
11 + * @package automattic/jetpack
12 + */
13 +
14 +/**
15 + * Copyright (c) Automattic. All rights reserved.
16 + *
17 + * Released under the GPL license
18 + * https://www.opensource.org/licenses/gpl-license.php
19 + *
20 + * This is an add-on for WordPress
21 + * https://wordpress.org/
22 + *
23 + * **********************************************************************
24 + * This program is free software; you can redistribute it and/or modify
25 + * it under the terms of the GNU General Public License as published by
26 + * the Free Software Foundation; either version 2 of the License, or
27 + * (at your option) any later version.
28 + *
29 + * This program is distributed in the hope that it will be useful,
30 + * but WITHOUT ANY WARRANTY; without even the implied warranty of
31 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32 + * GNU General Public License for more details.
33 + * **********************************************************************
34 + */
35 +
36 +if ( ! defined( 'ABSPATH' ) ) {
37 + exit( 0 );
38 +}
39 +
40 +/**
41 + * WPCom_Markdown class.
42 + */
43 +class WPCom_Markdown {
44 +
45 + const POST_OPTION = 'wpcom_publish_posts_with_markdown';
46 + const COMMENT_OPTION = 'wpcom_publish_comments_with_markdown';
47 + const POST_TYPE_SUPPORT = 'wpcom-markdown';
48 + const IS_MD_META = '_wpcom_is_markdown';
49 +
50 + /**
51 + * Our markdown parser.
52 + *
53 + * @var WPCom_GHF_Markdown_Parser
54 + */
55 + private static $parser;
56 +
57 + /**
58 + * An instance of the markdown class.
59 + *
60 + * @var WPCom_Markdown
61 + */
62 + private static $instance;
63 +
64 + /**
65 + * To ensure that our munged posts over xml-rpc are removed from the cache.
66 + *
67 + * @var array
68 + */
69 + public $posts_to_uncache = array();
70 +
71 + /**
72 + * Posts and parents to monitor.
73 + *
74 + * @var array
75 + */
76 + private $monitoring = array(
77 + 'post' => array(),
78 + 'parent' => array(),
79 + );
80 +
81 + /**
82 + * Whether or not kses filters were removed. Only set if removal was attempted.
83 + *
84 + * @var ?bool
85 + */
86 + public $kses;
87 +
88 + /**
89 + * Yay singletons!
90 + *
91 + * @return object WPCom_Markdown instance
92 + */
93 + public static function get_instance() {
94 + if ( ! self::$instance ) {
95 + self::$instance = new self();
96 + }
97 + return self::$instance;
98 + }
99 +
100 + /**
101 + * Kicks things off on `init` action
102 + */
103 + public function load() {
104 + $this->add_default_post_type_support();
105 + $this->maybe_load_actions_and_filters();
106 + if ( defined( 'REST_API_REQUEST' ) && REST_API_REQUEST ) {
107 + // phpcs:ignore WPCUT.SwitchBlog.SwitchBlog -- wpcom flags **every** use of switch_blog, apparently expecting valid instances to ignore or suppress the sniff.
108 + add_action( 'switch_blog', array( $this, 'maybe_load_actions_and_filters' ), 10, 2 );
109 + }
110 + add_action( 'admin_init', array( $this, 'register_setting' ) );
111 + add_action( 'admin_init', array( $this, 'maybe_unload_for_bulk_edit' ) );
112 + if ( current_theme_supports( 'o2' ) || class_exists( 'P2' ) ) {
113 + $this->add_o2_helpers();
114 + }
115 + }
116 +
117 + /**
118 + * If we're in a bulk edit session, unload so that we don't lose our markdown metadata
119 + */
120 + public function maybe_unload_for_bulk_edit() {
121 + if ( isset( $_REQUEST['bulk_edit'] ) && $this->is_posting_enabled() ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
122 + $this->unload_markdown_for_posts();
123 + }
124 + }
125 +
126 + /**
127 + * Called on init and fires on switch_blog to decide if our actions and filters
128 + * should be running.
129 + *
130 + * @param int|null $new_blog_id New blog ID.
131 + * @param int|null $old_blog_id Old blog ID.
132 + * @return null
133 + */
134 + public function maybe_load_actions_and_filters( $new_blog_id = null, $old_blog_id = null ) {
135 +
136 + // When WP sites are being installed, the options table is not available yet.
137 + if ( function_exists( 'wp_installing' ) && wp_installing() ) {
138 + return;
139 + }
140 +
141 + // If this is a switch_to_blog call, and the blog isn't changing, we'll already be loaded.
142 + if ( $new_blog_id && $new_blog_id === $old_blog_id ) {
143 + return;
144 + }
145 +
146 + if ( $this->is_posting_enabled() ) {
147 + $this->load_markdown_for_posts();
148 + } else {
149 + $this->unload_markdown_for_posts();
150 + }
151 +
152 + if ( $this->is_commenting_enabled() ) {
153 + $this->load_markdown_for_comments();
154 + } else {
155 + $this->unload_markdown_for_comments();
156 + }
157 + }
158 +
159 + /**
160 + * Set up hooks for enabling Markdown conversion on posts
161 + */
162 + public function load_markdown_for_posts() {
163 + add_filter( 'wp_kses_allowed_html', array( $this, 'wp_kses_allowed_html' ), 10, 2 );
164 + add_action( 'after_wp_tiny_mce', array( $this, 'after_wp_tiny_mce' ) );
165 + add_action( 'wp_insert_post', array( $this, 'wp_insert_post' ) );
166 + add_filter( 'wp_insert_post_data', array( $this, 'wp_insert_post_data' ), 10, 2 );
167 + add_filter( 'edit_post_content', array( $this, 'edit_post_content' ), 10, 2 );
168 + add_filter( 'edit_post_content_filtered', array( $this, 'edit_post_content_filtered' ), 10, 2 );
169 + add_action( 'wp_restore_post_revision', array( $this, 'wp_restore_post_revision' ), 10, 2 );
170 + add_filter( '_wp_post_revision_fields', array( $this, 'wp_post_revision_fields' ) );
171 + add_action( 'xmlrpc_call', array( $this, 'xmlrpc_actions' ) );
172 + add_filter( 'content_save_pre', array( $this, 'preserve_code_blocks' ), 1 );
173 + if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
174 + $this->check_for_early_methods();
175 + }
176 + }
177 +
178 + /**
179 + * Removes hooks to disable Markdown conversion on posts
180 + */
181 + public function unload_markdown_for_posts() {
182 + remove_filter( 'wp_kses_allowed_html', array( $this, 'wp_kses_allowed_html' ) );
183 + remove_action( 'after_wp_tiny_mce', array( $this, 'after_wp_tiny_mce' ) );
184 + remove_action( 'wp_insert_post', array( $this, 'wp_insert_post' ) );
185 + remove_filter( 'wp_insert_post_data', array( $this, 'wp_insert_post_data' ), 10 );
186 + remove_filter( 'edit_post_content', array( $this, 'edit_post_content' ), 10 );
187 + remove_filter( 'edit_post_content_filtered', array( $this, 'edit_post_content_filtered' ), 10 );
188 + remove_action( 'wp_restore_post_revision', array( $this, 'wp_restore_post_revision' ), 10 );
189 + remove_filter( '_wp_post_revision_fields', array( $this, 'wp_post_revision_fields' ) );
190 + remove_action( 'xmlrpc_call', array( $this, 'xmlrpc_actions' ) );
191 + remove_filter( 'content_save_pre', array( $this, 'preserve_code_blocks' ), 1 );
192 + }
193 +
194 + /**
195 + * Set up hooks for enabling Markdown conversion on comments
196 + */
197 + protected function load_markdown_for_comments() {
198 + // Use priority 9 so that Markdown runs before KSES, which can clean up
199 + // any munged HTML.
200 + add_filter( 'pre_comment_content', array( $this, 'pre_comment_content' ), 9 );
201 + }
202 +
203 + /**
204 + * Removes hooks to disable Markdown conversion
205 + */
206 + protected function unload_markdown_for_comments() {
207 + remove_filter( 'pre_comment_content', array( $this, 'pre_comment_content' ), 9 );
208 + }
209 +
210 + /**
211 + * The o2 plugin does some of what we do. Let's take precedence.
212 + */
213 + public function add_o2_helpers() {
214 + if ( $this->is_posting_enabled() ) {
215 + add_filter( 'content_save_pre', array( $this, 'o2_escape_lists' ), 1 );
216 + }
217 +
218 + add_filter( 'o2_preview_post', array( $this, 'o2_preview_post' ) );
219 + add_filter( 'o2_preview_comment', array( $this, 'o2_preview_comment' ) );
220 +
221 + add_filter( 'wpcom_markdown_transform_pre', array( $this, 'o2_unescape_lists' ) );
222 + add_filter( 'wpcom_untransformed_content', array( $this, 'o2_unescape_lists' ) );
223 + }
224 +
225 + /**
226 + * If Markdown is enabled for posts on this blog, filter the text for o2 previews
227 + *
228 + * @param string $text Post text.
229 + * @return string Post text transformed through the magic of Markdown
230 + */
231 + public function o2_preview_post( $text ) {
232 + if ( $this->is_posting_enabled() ) {
233 + $text = $this->transform( $text, array( 'unslash' => false ) );
234 + }
235 + return $text;
236 + }
237 +
238 + /**
239 + * If Markdown is enabled for comments on this blog, filter the text for o2 previews
240 + *
241 + * @param string $text Comment text.
242 + * @return string Comment text transformed through the magic of Markdown
243 + */
244 + public function o2_preview_comment( $text ) {
245 + if ( $this->is_commenting_enabled() ) {
246 + $text = $this->transform( $text, array( 'unslash' => false ) );
247 + }
248 + return $text;
249 + }
250 +
251 + /**
252 + * Escapes lists so that o2 doesn't trounce them
253 + *
254 + * @param string $text Post/comment text.
255 + * @return string Text escaped with HTML entity for asterisk.
256 + */
257 + public function o2_escape_lists( $text ) {
258 + return preg_replace( '/^\\* /um', '&#42; ', $text );
259 + }
260 +
261 + /**
262 + * Unescapes the token we inserted on o2_escape_lists
263 + *
264 + * @param string $text Post/comment text with HTML entities for asterisks.
265 + * @return string Text with the HTML entity removed
266 + */
267 + public function o2_unescape_lists( $text ) {
268 + return preg_replace( '/^[&]\#042; /um', '* ', $text );
269 + }
270 +
271 + /**
272 + * Preserve code blocks from being munged by KSES before they have a chance
273 + *
274 + * @param string $text post content.
275 + * @return string post content with code blocks escaped.
276 + */
277 + public function preserve_code_blocks( $text ) {
278 + return $this->get_parser()->codeblock_preserve( $text );
279 + }
280 +
281 + /**
282 + * Remove KSES if it's there. Store the result to manually invoke later if needed.
283 + */
284 + public function maybe_remove_kses() {
285 + // Filters return true if they existed before you removed them.
286 + if ( $this->is_posting_enabled() ) {
287 + $this->kses = remove_filter( 'content_filtered_save_pre', 'wp_filter_post_kses' ) && remove_filter( 'content_save_pre', 'wp_filter_post_kses' );
288 + }
289 + }
290 +
291 + /**
292 + * Add our Writing and Discussion settings.
293 + */
294 + public function register_setting() {
295 + add_settings_field( self::POST_OPTION, __( 'Markdown', 'jetpack' ), array( $this, 'post_field' ), 'writing' );
296 + register_setting( 'writing', self::POST_OPTION, array( $this, 'sanitize_setting' ) );
297 + add_settings_field( self::COMMENT_OPTION, __( 'Markdown', 'jetpack' ), array( $this, 'comment_field' ), 'discussion' );
298 + register_setting( 'discussion', self::COMMENT_OPTION, array( $this, 'sanitize_setting' ) );
299 + }
300 +
301 + /**
302 + * Sanitize setting. Don't really want to store "on" value, so we'll store "1" instead!
303 + *
304 + * @param string $input Value received by settings API via $_POST.
305 + * @return bool Cast to boolean.
306 + */
307 + public function sanitize_setting( $input ) {
308 + return (bool) $input;
309 + }
310 +
311 + /**
312 + * Prints HTML for the Writing setting
313 + */
314 + public function post_field() {
315 + printf(
316 + '<label><input name="%1$s" id="%1$s" type="checkbox"%2$s /> %3$s</label><p class="description">%4$s</p>',
317 + esc_attr( self::POST_OPTION ),
318 + checked( $this->is_posting_enabled(), true, false ),
319 + esc_html__( 'Use Markdown for posts and pages.', 'jetpack' ),
320 + sprintf( '<a href="%s" data-target="wpcom-help-center">%s</a>', esc_url( $this->get_support_url() ), esc_html__( 'Learn more about Markdown.', 'jetpack' ) )
321 + );
322 + }
323 +
324 + /**
325 + * Prints HTML for the Discussion setting
326 + */
327 + public function comment_field() {
328 + printf(
329 + '<label><input name="%1$s" id="%1$s" type="checkbox"%2$s /> %3$s</label><p class="description">%4$s</p>',
330 + esc_attr( self::COMMENT_OPTION ),
331 + checked( $this->is_commenting_enabled(), true, false ),
332 + esc_html__( 'Use Markdown for comments.', 'jetpack' ),
333 + sprintf( '<a href="%s" data-target="wpcom-help-center">%s</a>', esc_url( $this->get_support_url() ), esc_html__( 'Learn more about Markdown.', 'jetpack' ) )
334 + );
335 + }
336 +
337 + /**
338 + * Get the support url for Markdown
339 + *
340 + * @uses apply_filters
341 + * @return string support url
342 + */
343 + protected function get_support_url() {
344 + /**
345 + * Filter the Markdown support URL.
346 + *
347 + * @module markdown
348 + *
349 + * @since 2.8.0
350 + *
351 + * @param string $url Markdown support URL.
352 + */
353 + return apply_filters( 'easy_markdown_support_url', 'https://en.support.wordpress.com/markdown-quick-reference/' );
354 + }
355 +
356 + /**
357 + * Is Mardown conversion for posts enabled?
358 + *
359 + * @return boolean
360 + */
361 + public function is_posting_enabled() {
362 + return (bool) Jetpack_Options::get_option_and_ensure_autoload( self::POST_OPTION, '' );
363 + }
364 +
365 + /**
366 + * Is Markdown conversion for comments enabled?
367 + *
368 + * @return boolean
369 + */
370 + public function is_commenting_enabled() {
371 + return (bool) Jetpack_Options::get_option_and_ensure_autoload( self::COMMENT_OPTION, '' );
372 + }
373 +
374 + /**
375 + * Check if a $post_id has Markdown enabled
376 + *
377 + * @param int $post_id A post ID.
378 + * @return boolean
379 + */
380 + public function is_markdown( $post_id ) {
381 + return get_metadata( 'post', $post_id, self::IS_MD_META, true );
382 + }
383 +
384 + /**
385 + * Set Markdown as enabled on a post_id. We skip over update_postmeta so we
386 + * can sneakily set metadata on post revisions, which we need.
387 + *
388 + * @param int $post_id A post ID.
389 + * @return bool The metadata was successfully set.
390 + */
391 + protected function set_as_markdown( $post_id ) {
392 + return update_metadata( 'post', $post_id, self::IS_MD_META, true );
393 + }
394 +
395 + /**
396 + * Get our Markdown parser object, optionally requiring all of our needed classes and
397 + * instantiating our parser.
398 + *
399 + * @return object WPCom_GHF_Markdown_Parser instance.
400 + */
401 + public function get_parser() {
402 +
403 + if ( ! self::$parser ) {
404 + require_once JETPACK__PLUGIN_DIR . '/_inc/lib/markdown.php';
405 + self::$parser = new WPCom_GHF_Markdown_Parser();
406 + }
407 +
408 + return self::$parser;
409 + }
410 +
411 + /**
412 + * We don't want Markdown conversion all over the place.
413 + */
414 + public function add_default_post_type_support() {
415 + add_post_type_support( 'post', self::POST_TYPE_SUPPORT );
416 + add_post_type_support( 'page', self::POST_TYPE_SUPPORT );
417 + add_post_type_support( 'revision', self::POST_TYPE_SUPPORT );
418 + }
419 +
420 + /**
421 + * Figure out the post type of the post screen we're on
422 + *
423 + * @deprecated since 10.8
424 + * @return string Current post_type
425 + */
426 + protected function get_post_screen_post_type() {
427 + _deprecated_function( __METHOD__, 'jetpack-10.8', '' );
428 +
429 + global $pagenow;
430 + $post_type = filter_input( INPUT_GET, 'post_type', FILTER_UNSAFE_RAW );
431 + $post_id = filter_input( INPUT_GET, 'post', FILTER_SANITIZE_NUMBER_INT );
432 +
433 + if ( 'post-new.php' === $pagenow ) {
434 + return ! empty( $post_type ) ? $post_type : 'post';
435 + }
436 +
437 + if ( $post_id ) {
438 + $post_type = get_post_type( $post_id );
439 + }
440 +
441 + return ! empty( $post_type ) ? $post_type : 'post';
442 + }
443 +
444 + /**
445 + * Swap post_content and post_content_filtered for editing
446 + *
447 + * @param string $content Post content.
448 + * @param int $id post ID.
449 + * @return string Swapped content
450 + */
451 + public function edit_post_content( $content, $id ) {
452 + if ( $this->is_markdown( $id ) ) {
453 + $post = get_post( $id );
454 + if ( $post && ! empty( $post->post_content_filtered ) ) {
455 + $post = $this->swap_for_editing( $post );
456 + return $post->post_content;
457 + }
458 + }
459 + return $content;
460 + }
461 +
462 + /**
463 + * Swap post_content_filtered and post_content for editing
464 + *
465 + * @param string $content Post content_filtered.
466 + * @param int $id post ID.
467 + * @return string Swapped content
468 + */
469 + public function edit_post_content_filtered( $content, $id ) {
470 + // if markdown was disabled, let's turn this off.
471 + if ( ! $this->is_posting_enabled() && $this->is_markdown( $id ) ) {
472 + $post = get_post( $id );
473 + if ( $post && ! empty( $post->post_content_filtered ) ) {
474 + $content = '';
475 + }
476 + }
477 + return $content;
478 + }
479 +
480 + /**
481 + * Some tags are allowed to have a 'markdown' attribute, allowing them to contain Markdown.
482 + * We need to tell KSES about those tags.
483 + *
484 + * @param array $tags List of tags that KSES allows.
485 + * @param string $context The context that KSES is allowing these tags.
486 + * @return array The tags that KSES allows, with our extra 'markdown' parameter where necessary.
487 + */
488 + public function wp_kses_allowed_html( $tags, $context ) {
489 + if ( 'post' !== $context ) {
490 + return $tags;
491 + }
492 +
493 + $re = '/' . $this->get_parser()->contain_span_tags_re . '/';
494 + foreach ( $tags as $tag => $attributes ) {
495 +
496 + // In case other filters have changed the value to a non-array, we skip it.
497 + if ( ! is_array( $attributes ) ) {
498 + continue;
499 + }
500 +
501 + if ( preg_match( $re, $tag ) ) {
502 + $attributes['markdown'] = true;
503 + $tags[ $tag ] = $attributes;
504 + }
505 + }
506 +
507 + return $tags;
508 + }
509 +
510 + /**
511 + * TinyMCE needs to know not to strip the 'markdown' attribute. Unfortunately, it doesn't
512 + * really offer a nice API for allowed attributes, so we have to manually add it
513 + * to the schema instead.
514 + */
515 + public function after_wp_tiny_mce() {
516 + ?>
517 +<script type="text/javascript">
518 +jQuery( function() {
519 + ( 'undefined' !== typeof tinymce ) && tinymce.on( 'AddEditor', function( event ) {
520 + event.editor.on( 'BeforeSetContent', function( event ) {
521 + var editor = event.target;
522 + Object.keys( editor.schema.elements ).forEach( function( key, index ) {
523 + editor.schema.elements[ key ].attributes['markdown'] = {};
524 + editor.schema.elements[ key ].attributesOrder.push( 'markdown' );
525 + } );
526 + } );
527 + }, true );
528 +} );
529 +</script>
530 + <?php
531 + }
532 +
533 + /**
534 + * Magic happens here. Markdown is converted and stored on post_content. Original Markdown is stored
535 + * in post_content_filtered so that we can continue editing as Markdown.
536 + *
537 + * @param array $post_data The post data that will be inserted into the DB. Slashed.
538 + * @param array $postarr All the stuff that was in $_POST.
539 + * @return array $post_data with post_content and post_content_filtered modified
540 + */
541 + public function wp_insert_post_data( $post_data, $postarr ) {
542 + // $post_data array is slashed!
543 + $post_id = $postarr['ID'] ?? false;
544 + // bail early if markdown is disabled or this post type is unsupported.
545 + if ( ! $this->is_posting_enabled() || ! post_type_supports( $post_data['post_type'], self::POST_TYPE_SUPPORT ) ) {
546 + // it's disabled, but maybe this *was* a markdown post before.
547 + if ( $this->is_markdown( $post_id ) && ! empty( $post_data['post_content_filtered'] ) ) {
548 + $post_data['post_content_filtered'] = '';
549 + }
550 + // we have no context to determine supported post types in the `post_content_pre` hook,
551 + // which already ran to sanitize code blocks. Undo that.
552 + $post_data['post_content'] = $this->get_parser()->codeblock_restore( $post_data['post_content'] );
553 + return $post_data;
554 + }
555 + // rejigger post_content and post_content_filtered
556 + // revisions are already in the right place, except when we're restoring, but that's taken care of elsewhere
557 + // also prevent quick edit feature from overriding already-saved markdown (issue https://github.com/Automattic/jetpack/issues/636).
558 + if ( 'revision' !== $post_data['post_type'] && ! isset( $_POST['_inline_edit'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
559 + /**
560 + * Filter the original post content passed to Markdown.
561 + *
562 + * @module markdown
563 + *
564 + * @since 2.8.0
565 + *
566 + * @param string $post_data['post_content'] Untransformed post content.
567 + */
568 + $post_data['post_content_filtered'] = apply_filters( 'wpcom_untransformed_content', $post_data['post_content'] );
569 + $post_data['post_content'] = $this->transform( $post_data['post_content'], array( 'id' => $post_id ) );
570 + /** This filter is already documented in core/wp-includes/default-filters.php */
571 + $post_data['post_content'] = apply_filters( 'content_save_pre', $post_data['post_content'] );
572 + } elseif ( str_starts_with( $post_data['post_name'], $post_data['post_parent'] . '-autosave' ) ) {
573 + // autosaves for previews are weird.
574 + /** This filter is already documented in modules/markdown/easy-markdown.php */
575 + $post_data['post_content_filtered'] = apply_filters( 'wpcom_untransformed_content', $post_data['post_content'] );
576 + $post_data['post_content'] = $this->transform( $post_data['post_content'], array( 'id' => $post_data['post_parent'] ) );
577 + /** This filter is already documented in core/wp-includes/default-filters.php */
578 + $post_data['post_content'] = apply_filters( 'content_save_pre', $post_data['post_content'] );
579 + }
580 +
581 + // set as markdown on the wp_insert_post hook later.
582 + if ( $post_id ) {
583 + $this->monitoring['post'][ $post_id ] = true;
584 + } else {
585 + $this->monitoring['content'] = wp_unslash( $post_data['post_content'] );
586 + }
587 + if ( 'revision' === $postarr['post_type'] && $this->is_markdown( $postarr['post_parent'] ) ) {
588 + $this->monitoring['parent'][ $postarr['post_parent'] ] = true;
589 + }
590 +
591 + return $post_data;
592 + }
593 +
594 + /**
595 + * Calls on wp_insert_post action, after wp_insert_post_data. This way we can
596 + * still set postmeta on our revisions after it's all been deleted.
597 + *
598 + * @param int $post_id The post ID that has just been added/updated.
599 + */
600 + public function wp_insert_post( $post_id ) {
601 + $post_parent = get_post_field( 'post_parent', $post_id );
602 + // this didn't have an ID yet. Compare the content that was just saved.
603 + if ( isset( $this->monitoring['content'] ) && get_post_field( 'post_content', $post_id ) === $this->monitoring['content'] ) {
604 + unset( $this->monitoring['content'] );
605 + $this->set_as_markdown( $post_id );
606 + }
607 + if ( isset( $this->monitoring['post'][ $post_id ] ) ) {
608 + unset( $this->monitoring['post'][ $post_id ] );
609 + $this->set_as_markdown( $post_id );
610 + } elseif ( isset( $this->monitoring['parent'][ $post_parent ] ) ) {
611 + unset( $this->monitoring['parent'][ $post_parent ] );
612 + $this->set_as_markdown( $post_id );
613 + }
614 + }
615 +
616 + /**
617 + * Run a comment through Markdown. Easy peasy.
618 + *
619 + * @param string $content - the content.
620 + * @return string
621 + */
622 + public function pre_comment_content( $content ) {
623 + return $this->transform(
624 + $content,
625 + array(
626 + 'id' => $this->comment_hash( $content ),
627 + )
628 + );
629 + }
630 +
631 + /**
632 + * Return a comment hash.
633 + *
634 + * @param string $content - the content of the comment.
635 + */
636 + protected function comment_hash( $content ) {
637 + return 'c-' . substr( md5( $content ), 0, 8 );
638 + }
639 +
640 + /**
641 + * Markdown conversion. Some DRYness for repetitive tasks.
642 + *
643 + * @param string $text Content to be run through Markdown.
644 + * @param array $args Arguments, with keys:
645 + * id: provide a string to prefix footnotes with a unique identifier
646 + * unslash: when true, expects and returns slashed data
647 + * decode_code_blocks: when true, assume that text in fenced code blocks is already
648 + * HTML encoded and should be decoded before being passed to Markdown, which does
649 + * its own encoding.
650 + * @return string Markdown-processed content
651 + */
652 + public function transform( $text, $args = array() ) {
653 + // If this contains Gutenberg content, let's keep it intact.
654 + if ( has_blocks( $text ) ) {
655 + return $text;
656 + }
657 +
658 + $args = wp_parse_args(
659 + $args,
660 + array(
661 + 'id' => false,
662 + 'unslash' => true,
663 + 'decode_code_blocks' => ! $this->get_parser()->use_code_shortcode,
664 + )
665 + );
666 + // probably need to unslash.
667 + if ( $args['unslash'] ) {
668 + $text = wp_unslash( $text );
669 + }
670 +
671 + /**
672 + * Filter the content to be run through Markdown, before it's transformed by Markdown.
673 + *
674 + * @module markdown
675 + *
676 + * @since 2.8.0
677 + *
678 + * @param string $text Content to be run through Markdown
679 + * @param array $args Array of Markdown options.
680 + */
681 + $text = apply_filters( 'wpcom_markdown_transform_pre', $text, $args ) ?? '';
682 + // ensure our paragraphs are separated.
683 + $text = str_replace( array( '</p><p>', "</p>\n<p>" ), "</p>\n\n<p>", $text );
684 + // visual editor likes to add <p>s. Buh-bye.
685 + $text = $this->get_parser()->unp( $text );
686 + // sometimes we get an encoded > at start of line, breaking blockquotes.
687 + $text = preg_replace( '/^&gt;/m', '>', $text );
688 + // prefixes are because we need to namespace footnotes by post_id.
689 + $this->get_parser()->fn_id_prefix = $args['id'] ? $args['id'] . '-' : '';
690 + // If we're not using the code shortcode, prevent over-encoding.
691 + if ( $args['decode_code_blocks'] ) {
692 + $text = $this->get_parser()->codeblock_restore( $text );
693 + }
694 + // Transform it!
695 + $text = $this->get_parser()->transform( $text );
696 + // Fix footnotes - kses doesn't like the : IDs it supplies.
697 + $text = preg_replace( '/((id|href)="#?fn(ref)?):/', '$1-', $text );
698 + // Markdown inserts extra spaces to make itself work. Buh-bye.
699 + $text = rtrim( $text );
700 + /**
701 + * Filter the content to be run through Markdown, after it was transformed by Markdown.
702 + *
703 + * @module markdown
704 + *
705 + * @since 2.8.0
706 + *
707 + * @param string $text Content to be run through Markdown
708 + * @param array $args Array of Markdown options.
709 + */
710 + $text = apply_filters( 'wpcom_markdown_transform_post', $text, $args );
711 +
712 + // probably need to re-slash.
713 + if ( $args['unslash'] ) {
714 + $text = wp_slash( $text );
715 + }
716 +
717 + return $text;
718 + }
719 +
720 + /**
721 + * Shows Markdown in the Revisions screen, and ensures that post_content_filtered
722 + * is maintained on revisions
723 + *
724 + * @param array $fields Post fields pertinent to revisions.
725 + */
726 + public function wp_post_revision_fields( $fields ) {
727 + $fields['post_content_filtered'] = __( 'Markdown content', 'jetpack' );
728 + return $fields;
729 + }
730 +
731 + /**
732 + * Do some song and dance to keep all post_content and post_content_filtered content
733 + * in the expected place when a post revision is restored.
734 + *
735 + * @param int $post_id The post ID have a restore done to it.
736 + * @param int $revision_id The revision ID being restored.
737 + */
738 + public function wp_restore_post_revision( $post_id, $revision_id ) {
739 + if ( $this->is_markdown( $revision_id ) ) {
740 + $revision = get_post( $revision_id, ARRAY_A );
741 + $post = get_post( $post_id, ARRAY_A );
742 + $post['post_content'] = $revision['post_content_filtered']; // Yes, we put it in post_content, because our wp_insert_post_data() expects that.
743 + // set this flag so we can restore the post_content_filtered on the last revision later.
744 + $this->monitoring['restore'] = true;
745 + // let's not make a revision of our fixing update.
746 + add_filter( 'wp_revisions_to_keep', '__return_false', 99 );
747 + wp_update_post( $post );
748 + $this->fix_latest_revision_on_restore( $post_id );
749 + remove_filter( 'wp_revisions_to_keep', '__return_false', 99 );
750 + }
751 + }
752 +
753 + /**
754 + * We need to ensure the last revision has Markdown, not HTML in its post_content_filtered
755 + * column after a restore.
756 + *
757 + * @param int $post_id The post ID that was just restored.
758 + */
759 + protected function fix_latest_revision_on_restore( $post_id ) {
760 + global $wpdb;
761 + $post = get_post( $post_id );
762 + $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 ) );
763 + $last_revision->post_content_filtered = $post->post_content_filtered;
764 + wp_insert_post( (array) $last_revision );
765 + }
766 +
767 + /**
768 + * Kicks off magic for an XML-RPC session. We want to keep editing Markdown
769 + * and publishing HTML.
770 + *
771 + * @param string $xmlrpc_method The current XML-RPC method.
772 + */
773 + public function xmlrpc_actions( $xmlrpc_method ) {
774 + switch ( $xmlrpc_method ) {
775 + case 'metaWeblog.getRecentPosts':
776 + case 'wp.getPosts':
777 + case 'wp.getPages':
778 + add_action( 'parse_query', array( $this, 'make_filterable' ), 10, 1 );
779 + break;
780 + case 'wp.getPost':
781 + $this->prime_post_cache();
782 + break;
783 + }
784 + }
785 +
786 + /**
787 + * Function metaWeblog.getPost and wp.getPage fire xmlrpc_call action *after* get_post() is called.
788 + * So, we have to detect those methods and prime the post cache early.
789 + *
790 + * @return null
791 + */
792 + protected function check_for_early_methods() {
793 + $raw_post_data = file_get_contents( 'php://input' );
794 + if ( ! str_contains( $raw_post_data, 'metaWeblog.getPost' )
795 + && ! str_contains( $raw_post_data, 'wp.getPage' ) ) {
796 + return;
797 + }
798 + include_once ABSPATH . WPINC . '/class-IXR.php';
799 + $message = new IXR_Message( $raw_post_data );
800 + $message->parse();
801 + $post_id_position = 'metaWeblog.getPost' === $message->methodName ? 0 : 1; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
802 + $this->prime_post_cache( $message->params[ $post_id_position ] ?? false );
803 + }
804 +
805 + /**
806 + * Prime the post cache with swapped post_content. This is a sneaky way of getting around
807 + * the fact that there are no good hooks to call on the *.getPost xmlrpc methods.
808 + *
809 + * @param bool $post_id - the post ID that we're priming.
810 + */
811 + private function prime_post_cache( $post_id = false ) {
812 + global $wp_xmlrpc_server;
813 + if ( ! $post_id ) {
814 + if ( isset( $wp_xmlrpc_server->message->params[3] ) ) {
815 + $post_id = $wp_xmlrpc_server->message->params[3];
816 + } else {
817 + return; // Exit early if we can't get a valid post_id
818 + }
819 + }
820 +
821 + // prime the post cache.
822 + if ( $this->is_markdown( $post_id ) ) {
823 + $post = get_post( $post_id );
824 + if ( ! empty( $post->post_content_filtered ) ) {
825 + wp_cache_delete( $post->ID, 'posts' );
826 + $post = $this->swap_for_editing( $post );
827 + wp_cache_add( $post->ID, $post, 'posts' );
828 + $this->posts_to_uncache[] = $post_id;
829 + }
830 + }
831 + // uncache munged posts if using a persistent object cache.
832 + if ( wp_using_ext_object_cache() ) {
833 + add_action( 'shutdown', array( $this, 'uncache_munged_posts' ) );
834 + }
835 + }
836 +
837 + /**
838 + * Swaps `post_content_filtered` back to `post_content` for editing purposes.
839 + *
840 + * @param object $post WP_Post object.
841 + * @return object WP_Post object with swapped `post_content_filtered` and `post_content`.
842 + */
843 + protected function swap_for_editing( $post ) {
844 + $markdown = $post->post_content_filtered;
845 + // unencode encoded code blocks.
846 + $markdown = $this->get_parser()->codeblock_restore( $markdown );
847 + // restore beginning of line blockquotes.
848 + $markdown = preg_replace( '/^&gt; /m', '> ', $markdown );
849 + $post->post_content_filtered = $post->post_content;
850 + $post->post_content = $markdown;
851 + return $post;
852 + }
853 +
854 + /**
855 + * We munge the post cache to serve proper markdown content to XML-RPC clients.
856 + * Uncache these after the XML-RPC session ends.
857 + */
858 + public function uncache_munged_posts() {
859 + // $this context gets lost in testing sometimes. Weird.
860 + foreach ( self::get_instance()->posts_to_uncache as $post_id ) {
861 + wp_cache_delete( $post_id, 'posts' );
862 + }
863 + }
864 +
865 + /**
866 + * Since *.(get)?[Rr]ecentPosts calls get_posts with suppress filters on, we need to
867 + * turn them back on so that we can swap things for editing.
868 + *
869 + * @param object $wp_query WP_Query object.
870 + */
871 + public function make_filterable( $wp_query ) {
872 + $wp_query->set( 'suppress_filters', false );
873 + add_action( 'the_posts', array( $this, 'the_posts' ), 10, 2 );
874 + }
875 +
876 + /**
877 + * Swaps post_content and post_content_filtered for editing.
878 + *
879 + * @param array $posts Posts returned by the just-completed query.
880 + * @return array Modified $posts
881 + */
882 + public function the_posts( $posts ) {
883 + foreach ( $posts as $key => $post ) {
884 + if ( $this->is_markdown( $post->ID ) && ! empty( $posts[ $key ]->post_content_filtered ) ) {
885 + $markdown = $posts[ $key ]->post_content_filtered;
886 + $posts[ $key ]->post_content_filtered = $posts[ $key ]->post_content;
887 + $posts[ $key ]->post_content = $markdown;
888 + }
889 + }
890 + return $posts;
891 + }
892 +
893 + /**
894 + * Singleton silence is golden
895 + */
896 + private function __construct() {}
897 +}
898 +
899 +add_action( 'init', array( WPCom_Markdown::get_instance(), 'load' ) );