PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 12.9.5
Jetpack – WP Security, Backup, Speed, & Growth v12.9.5
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 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 14.4.2 14.5.1 14.6.1 14.7.1 14.8.1 14.9.2 15.0.3 15.1.2 15.2.1 15.3.2 15.4.1 15.5.1 15.6.1 15.7.2 15.8.1 15.9.2 16.0.2 16.1.3 16.2-a.5 16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / modules / markdown / easy-markdown.php
jetpack / modules / markdown Last commit date
easy-markdown.php 2 years ago
easy-markdown.php
878 lines
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 /**
37 * WPCom_Markdown class.
38 */
39 class WPCom_Markdown {
40
41 const POST_OPTION = 'wpcom_publish_posts_with_markdown';
42 const COMMENT_OPTION = 'wpcom_publish_comments_with_markdown';
43 const POST_TYPE_SUPPORT = 'wpcom-markdown';
44 const IS_MD_META = '_wpcom_is_markdown';
45
46 /**
47 * Our markdown parser.
48 *
49 * @var WPCom_GHF_Markdown_Parser
50 */
51 private static $parser;
52
53 /**
54 * An instance of the markdown class.
55 *
56 * @var WPCom_Markdown
57 */
58 private static $instance;
59
60 /**
61 * To ensure that our munged posts over xml-rpc are removed from the cache.
62 *
63 * @var array
64 */
65 public $posts_to_uncache = array();
66
67 /**
68 * Posts and parents to monitor.
69 *
70 * @var array
71 */
72 private $monitoring = array(
73 'post' => array(),
74 'parent' => array(),
75 );
76
77 /**
78 * Yay singletons!
79 *
80 * @return object WPCom_Markdown instance
81 */
82 public static function get_instance() {
83 if ( ! self::$instance ) {
84 self::$instance = new self();
85 }
86 return self::$instance;
87 }
88
89 /**
90 * Kicks things off on `init` action
91 */
92 public function load() {
93 $this->add_default_post_type_support();
94 $this->maybe_load_actions_and_filters();
95 if ( defined( 'REST_API_REQUEST' ) && REST_API_REQUEST ) {
96 add_action( 'switch_blog', array( $this, 'maybe_load_actions_and_filters' ), 10, 2 );
97 }
98 add_action( 'admin_init', array( $this, 'register_setting' ) );
99 add_action( 'admin_init', array( $this, 'maybe_unload_for_bulk_edit' ) );
100 if ( current_theme_supports( 'o2' ) || class_exists( 'P2' ) ) {
101 $this->add_o2_helpers();
102 }
103 }
104
105 /**
106 * If we're in a bulk edit session, unload so that we don't lose our markdown metadata
107 */
108 public function maybe_unload_for_bulk_edit() {
109 if ( isset( $_REQUEST['bulk_edit'] ) && $this->is_posting_enabled() ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
110 $this->unload_markdown_for_posts();
111 }
112 }
113
114 /**
115 * Called on init and fires on switch_blog to decide if our actions and filters
116 * should be running.
117 *
118 * @param int|null $new_blog_id New blog ID.
119 * @param int|null $old_blog_id Old blog ID.
120 * @return null
121 */
122 public function maybe_load_actions_and_filters( $new_blog_id = null, $old_blog_id = null ) {
123
124 // When WP sites are being installed, the options table is not available yet.
125 if ( function_exists( 'wp_installing' ) && wp_installing() ) {
126 return;
127 }
128
129 // If this is a switch_to_blog call, and the blog isn't changing, we'll already be loaded.
130 if ( $new_blog_id && $new_blog_id === $old_blog_id ) {
131 return;
132 }
133
134 if ( $this->is_posting_enabled() ) {
135 $this->load_markdown_for_posts();
136 } else {
137 $this->unload_markdown_for_posts();
138 }
139
140 if ( $this->is_commenting_enabled() ) {
141 $this->load_markdown_for_comments();
142 } else {
143 $this->unload_markdown_for_comments();
144 }
145 }
146
147 /**
148 * Set up hooks for enabling Markdown conversion on posts
149 */
150 public function load_markdown_for_posts() {
151 add_filter( 'wp_kses_allowed_html', array( $this, 'wp_kses_allowed_html' ), 10, 2 );
152 add_action( 'after_wp_tiny_mce', array( $this, 'after_wp_tiny_mce' ) );
153 add_action( 'wp_insert_post', array( $this, 'wp_insert_post' ) );
154 add_filter( 'wp_insert_post_data', array( $this, 'wp_insert_post_data' ), 10, 2 );
155 add_filter( 'edit_post_content', array( $this, 'edit_post_content' ), 10, 2 );
156 add_filter( 'edit_post_content_filtered', array( $this, 'edit_post_content_filtered' ), 10, 2 );
157 add_action( 'wp_restore_post_revision', array( $this, 'wp_restore_post_revision' ), 10, 2 );
158 add_filter( '_wp_post_revision_fields', array( $this, 'wp_post_revision_fields' ) );
159 add_action( 'xmlrpc_call', array( $this, 'xmlrpc_actions' ) );
160 add_filter( 'content_save_pre', array( $this, 'preserve_code_blocks' ), 1 );
161 if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
162 $this->check_for_early_methods();
163 }
164 }
165
166 /**
167 * Removes hooks to disable Markdown conversion on posts
168 */
169 public function unload_markdown_for_posts() {
170 remove_filter( 'wp_kses_allowed_html', array( $this, 'wp_kses_allowed_html' ) );
171 remove_action( 'after_wp_tiny_mce', array( $this, 'after_wp_tiny_mce' ) );
172 remove_action( 'wp_insert_post', array( $this, 'wp_insert_post' ) );
173 remove_filter( 'wp_insert_post_data', array( $this, 'wp_insert_post_data' ), 10, 2 );
174 remove_filter( 'edit_post_content', array( $this, 'edit_post_content' ), 10, 2 );
175 remove_filter( 'edit_post_content_filtered', array( $this, 'edit_post_content_filtered' ), 10, 2 );
176 remove_action( 'wp_restore_post_revision', array( $this, 'wp_restore_post_revision' ), 10, 2 );
177 remove_filter( '_wp_post_revision_fields', array( $this, 'wp_post_revision_fields' ) );
178 remove_action( 'xmlrpc_call', array( $this, 'xmlrpc_actions' ) );
179 remove_filter( 'content_save_pre', array( $this, 'preserve_code_blocks' ), 1 );
180 }
181
182 /**
183 * Set up hooks for enabling Markdown conversion on comments
184 */
185 protected function load_markdown_for_comments() {
186 // Use priority 9 so that Markdown runs before KSES, which can clean up
187 // any munged HTML.
188 add_filter( 'pre_comment_content', array( $this, 'pre_comment_content' ), 9 );
189 }
190
191 /**
192 * Removes hooks to disable Markdown conversion
193 */
194 protected function unload_markdown_for_comments() {
195 remove_filter( 'pre_comment_content', array( $this, 'pre_comment_content' ), 9 );
196 }
197
198 /**
199 * The o2 plugin does some of what we do. Let's take precedence.
200 */
201 public function add_o2_helpers() {
202 if ( $this->is_posting_enabled() ) {
203 add_filter( 'content_save_pre', array( $this, 'o2_escape_lists' ), 1 );
204 }
205
206 add_filter( 'o2_preview_post', array( $this, 'o2_preview_post' ) );
207 add_filter( 'o2_preview_comment', array( $this, 'o2_preview_comment' ) );
208
209 add_filter( 'wpcom_markdown_transform_pre', array( $this, 'o2_unescape_lists' ) );
210 add_filter( 'wpcom_untransformed_content', array( $this, 'o2_unescape_lists' ) );
211 }
212
213 /**
214 * If Markdown is enabled for posts on this blog, filter the text for o2 previews
215 *
216 * @param string $text Post text.
217 * @return string Post text transformed through the magic of Markdown
218 */
219 public function o2_preview_post( $text ) {
220 if ( $this->is_posting_enabled() ) {
221 $text = $this->transform( $text, array( 'unslash' => false ) );
222 }
223 return $text;
224 }
225
226 /**
227 * If Markdown is enabled for comments on this blog, filter the text for o2 previews
228 *
229 * @param string $text Comment text.
230 * @return string Comment text transformed through the magic of Markdown
231 */
232 public function o2_preview_comment( $text ) {
233 if ( $this->is_commenting_enabled() ) {
234 $text = $this->transform( $text, array( 'unslash' => false ) );
235 }
236 return $text;
237 }
238
239 /**
240 * Escapes lists so that o2 doesn't trounce them
241 *
242 * @param string $text Post/comment text.
243 * @return string Text escaped with HTML entity for asterisk.
244 */
245 public function o2_escape_lists( $text ) {
246 return preg_replace( '/^\\* /um', '&#42; ', $text );
247 }
248
249 /**
250 * Unescapes the token we inserted on o2_escape_lists
251 *
252 * @param string $text Post/comment text with HTML entities for asterisks.
253 * @return string Text with the HTML entity removed
254 */
255 public function o2_unescape_lists( $text ) {
256 return preg_replace( '/^[&]\#042; /um', '* ', $text );
257 }
258
259 /**
260 * Preserve code blocks from being munged by KSES before they have a chance
261 *
262 * @param string $text post content.
263 * @return string post content with code blocks escaped.
264 */
265 public function preserve_code_blocks( $text ) {
266 return $this->get_parser()->codeblock_preserve( $text );
267 }
268
269 /**
270 * Remove KSES if it's there. Store the result to manually invoke later if needed.
271 */
272 public function maybe_remove_kses() {
273 // Filters return true if they existed before you removed them.
274 if ( $this->is_posting_enabled() ) {
275 $this->kses = remove_filter( 'content_filtered_save_pre', 'wp_filter_post_kses' ) && remove_filter( 'content_save_pre', 'wp_filter_post_kses' );
276 }
277 }
278
279 /**
280 * Add our Writing and Discussion settings.
281 */
282 public function register_setting() {
283 add_settings_field( self::POST_OPTION, __( 'Markdown', 'jetpack' ), array( $this, 'post_field' ), 'writing' );
284 register_setting( 'writing', self::POST_OPTION, array( $this, 'sanitize_setting' ) );
285 add_settings_field( self::COMMENT_OPTION, __( 'Markdown', 'jetpack' ), array( $this, 'comment_field' ), 'discussion' );
286 register_setting( 'discussion', self::COMMENT_OPTION, array( $this, 'sanitize_setting' ) );
287 }
288
289 /**
290 * Sanitize setting. Don't really want to store "on" value, so we'll store "1" instead!
291 *
292 * @param string $input Value received by settings API via $_POST.
293 * @return bool Cast to boolean.
294 */
295 public function sanitize_setting( $input ) {
296 return (bool) $input;
297 }
298
299 /**
300 * Prints HTML for the Writing setting
301 */
302 public function post_field() {
303 printf(
304 '<label><input name="%1$s" id="%1$s" type="checkbox"%2$s /> %3$s</label><p class="description">%4$s</p>',
305 esc_attr( self::POST_OPTION ),
306 checked( $this->is_posting_enabled(), true, false ),
307 esc_html__( 'Use Markdown for posts and pages.', 'jetpack' ),
308 sprintf( '<a href="%s">%s</a>', esc_url( $this->get_support_url() ), esc_html__( 'Learn more about Markdown.', 'jetpack' ) )
309 );
310 }
311
312 /**
313 * Prints HTML for the Discussion setting
314 */
315 public function comment_field() {
316 printf(
317 '<label><input name="%1$s" id="%1$s" type="checkbox"%2$s /> %3$s</label><p class="description">%4$s</p>',
318 esc_attr( self::COMMENT_OPTION ),
319 checked( $this->is_commenting_enabled(), true, false ),
320 esc_html__( 'Use Markdown for comments.', 'jetpack' ),
321 sprintf( '<a href="%s">%s</a>', esc_url( $this->get_support_url() ), esc_html__( 'Learn more about Markdown.', 'jetpack' ) )
322 );
323 }
324
325 /**
326 * Get the support url for Markdown
327 *
328 * @uses apply_filters
329 * @return string support url
330 */
331 protected function get_support_url() {
332 /**
333 * Filter the Markdown support URL.
334 *
335 * @module markdown
336 *
337 * @since 2.8.0
338 *
339 * @param string $url Markdown support URL.
340 */
341 return apply_filters( 'easy_markdown_support_url', 'https://en.support.wordpress.com/markdown-quick-reference/' );
342 }
343
344 /**
345 * Is Mardown conversion for posts enabled?
346 *
347 * @return boolean
348 */
349 public function is_posting_enabled() {
350 return (bool) Jetpack_Options::get_option_and_ensure_autoload( self::POST_OPTION, '' );
351 }
352
353 /**
354 * Is Markdown conversion for comments enabled?
355 *
356 * @return boolean
357 */
358 public function is_commenting_enabled() {
359 return (bool) Jetpack_Options::get_option_and_ensure_autoload( self::COMMENT_OPTION, '' );
360 }
361
362 /**
363 * Check if a $post_id has Markdown enabled
364 *
365 * @param int $post_id A post ID.
366 * @return boolean
367 */
368 public function is_markdown( $post_id ) {
369 return get_metadata( 'post', $post_id, self::IS_MD_META, true );
370 }
371
372 /**
373 * Set Markdown as enabled on a post_id. We skip over update_postmeta so we
374 * can sneakily set metadata on post revisions, which we need.
375 *
376 * @param int $post_id A post ID.
377 * @return bool The metadata was successfully set.
378 */
379 protected function set_as_markdown( $post_id ) {
380 return update_metadata( 'post', $post_id, self::IS_MD_META, true );
381 }
382
383 /**
384 * Get our Markdown parser object, optionally requiring all of our needed classes and
385 * instantiating our parser.
386 *
387 * @return object WPCom_GHF_Markdown_Parser instance.
388 */
389 public function get_parser() {
390
391 if ( ! self::$parser ) {
392 require_once JETPACK__PLUGIN_DIR . '/_inc/lib/markdown.php';
393 self::$parser = new WPCom_GHF_Markdown_Parser();
394 }
395
396 return self::$parser;
397 }
398
399 /**
400 * We don't want Markdown conversion all over the place.
401 */
402 public function add_default_post_type_support() {
403 add_post_type_support( 'post', self::POST_TYPE_SUPPORT );
404 add_post_type_support( 'page', self::POST_TYPE_SUPPORT );
405 add_post_type_support( 'revision', self::POST_TYPE_SUPPORT );
406 }
407
408 /**
409 * Figure out the post type of the post screen we're on
410 *
411 * @deprecated since 10.8
412 * @return string Current post_type
413 */
414 protected function get_post_screen_post_type() {
415 _deprecated_function( __METHOD__, 'jetpack-10.8', '' );
416
417 global $pagenow;
418 $post_type = filter_input( INPUT_GET, 'post_type', FILTER_UNSAFE_RAW );
419 $post_id = filter_input( INPUT_GET, 'post', FILTER_SANITIZE_NUMBER_INT );
420
421 if ( 'post-new.php' === $pagenow ) {
422 return ! empty( $post_type ) ? $post_type : 'post';
423 }
424
425 if ( $post_id ) {
426 $post_type = get_post_type( $post_id );
427 }
428
429 return ! empty( $post_type ) ? $post_type : 'post';
430 }
431
432 /**
433 * Swap post_content and post_content_filtered for editing
434 *
435 * @param string $content Post content.
436 * @param int $id post ID.
437 * @return string Swapped content
438 */
439 public function edit_post_content( $content, $id ) {
440 if ( $this->is_markdown( $id ) ) {
441 $post = get_post( $id );
442 if ( $post && ! empty( $post->post_content_filtered ) ) {
443 $post = $this->swap_for_editing( $post );
444 return $post->post_content;
445 }
446 }
447 return $content;
448 }
449
450 /**
451 * Swap post_content_filtered and post_content for editing
452 *
453 * @param string $content Post content_filtered.
454 * @param int $id post ID.
455 * @return string Swapped content
456 */
457 public function edit_post_content_filtered( $content, $id ) {
458 // if markdown was disabled, let's turn this off.
459 if ( ! $this->is_posting_enabled() && $this->is_markdown( $id ) ) {
460 $post = get_post( $id );
461 if ( $post && ! empty( $post->post_content_filtered ) ) {
462 $content = '';
463 }
464 }
465 return $content;
466 }
467
468 /**
469 * Some tags are allowed to have a 'markdown' attribute, allowing them to contain Markdown.
470 * We need to tell KSES about those tags.
471 *
472 * @param array $tags List of tags that KSES allows.
473 * @param string $context The context that KSES is allowing these tags.
474 * @return array The tags that KSES allows, with our extra 'markdown' parameter where necessary.
475 */
476 public function wp_kses_allowed_html( $tags, $context ) {
477 if ( 'post' !== $context ) {
478 return $tags;
479 }
480
481 $re = '/' . $this->get_parser()->contain_span_tags_re . '/';
482 foreach ( $tags as $tag => $attributes ) {
483 if ( preg_match( $re, $tag ) ) {
484 $attributes['markdown'] = true;
485 $tags[ $tag ] = $attributes;
486 }
487 }
488
489 return $tags;
490 }
491
492 /**
493 * TinyMCE needs to know not to strip the 'markdown' attribute. Unfortunately, it doesn't
494 * really offer a nice API for allowed attributes, so we have to manually add it
495 * to the schema instead.
496 */
497 public function after_wp_tiny_mce() {
498 ?>
499 <script type="text/javascript">
500 jQuery( function() {
501 ( 'undefined' !== typeof tinymce ) && tinymce.on( 'AddEditor', function( event ) {
502 event.editor.on( 'BeforeSetContent', function( event ) {
503 var editor = event.target;
504 Object.keys( editor.schema.elements ).forEach( function( key, index ) {
505 editor.schema.elements[ key ].attributes['markdown'] = {};
506 editor.schema.elements[ key ].attributesOrder.push( 'markdown' );
507 } );
508 } );
509 }, true );
510 } );
511 </script>
512 <?php
513 }
514
515 /**
516 * Magic happens here. Markdown is converted and stored on post_content. Original Markdown is stored
517 * in post_content_filtered so that we can continue editing as Markdown.
518 *
519 * @param array $post_data The post data that will be inserted into the DB. Slashed.
520 * @param array $postarr All the stuff that was in $_POST.
521 * @return array $post_data with post_content and post_content_filtered modified
522 */
523 public function wp_insert_post_data( $post_data, $postarr ) {
524 // $post_data array is slashed!
525 $post_id = isset( $postarr['ID'] ) ? $postarr['ID'] : false;
526 // bail early if markdown is disabled or this post type is unsupported.
527 if ( ! $this->is_posting_enabled() || ! post_type_supports( $post_data['post_type'], self::POST_TYPE_SUPPORT ) ) {
528 // it's disabled, but maybe this *was* a markdown post before.
529 if ( $this->is_markdown( $post_id ) && ! empty( $post_data['post_content_filtered'] ) ) {
530 $post_data['post_content_filtered'] = '';
531 }
532 // we have no context to determine supported post types in the `post_content_pre` hook,
533 // which already ran to sanitize code blocks. Undo that.
534 $post_data['post_content'] = $this->get_parser()->codeblock_restore( $post_data['post_content'] );
535 return $post_data;
536 }
537 // rejigger post_content and post_content_filtered
538 // revisions are already in the right place, except when we're restoring, but that's taken care of elsewhere
539 // also prevent quick edit feature from overriding already-saved markdown (issue https://github.com/Automattic/jetpack/issues/636).
540 if ( 'revision' !== $post_data['post_type'] && ! isset( $_POST['_inline_edit'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
541 /**
542 * Filter the original post content passed to Markdown.
543 *
544 * @module markdown
545 *
546 * @since 2.8.0
547 *
548 * @param string $post_data['post_content'] Untransformed post content.
549 */
550 $post_data['post_content_filtered'] = apply_filters( 'wpcom_untransformed_content', $post_data['post_content'] );
551 $post_data['post_content'] = $this->transform( $post_data['post_content'], array( 'id' => $post_id ) );
552 /** This filter is already documented in core/wp-includes/default-filters.php */
553 $post_data['post_content'] = apply_filters( 'content_save_pre', $post_data['post_content'] );
554 } elseif ( str_starts_with( $post_data['post_name'], $post_data['post_parent'] . '-autosave' ) ) {
555 // autosaves for previews are weird.
556 /** This filter is already documented in modules/markdown/easy-markdown.php */
557 $post_data['post_content_filtered'] = apply_filters( 'wpcom_untransformed_content', $post_data['post_content'] );
558 $post_data['post_content'] = $this->transform( $post_data['post_content'], array( 'id' => $post_data['post_parent'] ) );
559 /** This filter is already documented in core/wp-includes/default-filters.php */
560 $post_data['post_content'] = apply_filters( 'content_save_pre', $post_data['post_content'] );
561 }
562
563 // set as markdown on the wp_insert_post hook later.
564 if ( $post_id ) {
565 $this->monitoring['post'][ $post_id ] = true;
566 } else {
567 $this->monitoring['content'] = wp_unslash( $post_data['post_content'] );
568 }
569 if ( 'revision' === $postarr['post_type'] && $this->is_markdown( $postarr['post_parent'] ) ) {
570 $this->monitoring['parent'][ $postarr['post_parent'] ] = true;
571 }
572
573 return $post_data;
574 }
575
576 /**
577 * Calls on wp_insert_post action, after wp_insert_post_data. This way we can
578 * still set postmeta on our revisions after it's all been deleted.
579 *
580 * @param int $post_id The post ID that has just been added/updated.
581 */
582 public function wp_insert_post( $post_id ) {
583 $post_parent = get_post_field( 'post_parent', $post_id );
584 // this didn't have an ID yet. Compare the content that was just saved.
585 if ( isset( $this->monitoring['content'] ) && get_post_field( 'post_content', $post_id ) === $this->monitoring['content'] ) {
586 unset( $this->monitoring['content'] );
587 $this->set_as_markdown( $post_id );
588 }
589 if ( isset( $this->monitoring['post'][ $post_id ] ) ) {
590 unset( $this->monitoring['post'][ $post_id ] );
591 $this->set_as_markdown( $post_id );
592 } elseif ( isset( $this->monitoring['parent'][ $post_parent ] ) ) {
593 unset( $this->monitoring['parent'][ $post_parent ] );
594 $this->set_as_markdown( $post_id );
595 }
596 }
597
598 /**
599 * Run a comment through Markdown. Easy peasy.
600 *
601 * @param string $content - the content.
602 * @return string
603 */
604 public function pre_comment_content( $content ) {
605 return $this->transform(
606 $content,
607 array(
608 'id' => $this->comment_hash( $content ),
609 )
610 );
611 }
612
613 /**
614 * Return a comment hash.
615 *
616 * @param string $content - the content of the comment.
617 */
618 protected function comment_hash( $content ) {
619 return 'c-' . substr( md5( $content ), 0, 8 );
620 }
621
622 /**
623 * Markdown conversion. Some DRYness for repetitive tasks.
624 *
625 * @param string $text Content to be run through Markdown.
626 * @param array $args Arguments, with keys:
627 * id: provide a string to prefix footnotes with a unique identifier
628 * unslash: when true, expects and returns slashed data
629 * decode_code_blocks: when true, assume that text in fenced code blocks is already
630 * HTML encoded and should be decoded before being passed to Markdown, which does
631 * its own encoding.
632 * @return string Markdown-processed content
633 */
634 public function transform( $text, $args = array() ) {
635 // If this contains Gutenberg content, let's keep it intact.
636 if ( has_blocks( $text ) ) {
637 return $text;
638 }
639
640 $args = wp_parse_args(
641 $args,
642 array(
643 'id' => false,
644 'unslash' => true,
645 'decode_code_blocks' => ! $this->get_parser()->use_code_shortcode,
646 )
647 );
648 // probably need to unslash.
649 if ( $args['unslash'] ) {
650 $text = wp_unslash( $text );
651 }
652
653 /**
654 * Filter the content to be run through Markdown, before it's transformed by Markdown.
655 *
656 * @module markdown
657 *
658 * @since 2.8.0
659 *
660 * @param string $text Content to be run through Markdown
661 * @param array $args Array of Markdown options.
662 */
663 $text = apply_filters( 'wpcom_markdown_transform_pre', $text, $args );
664 // ensure our paragraphs are separated.
665 $text = str_replace( array( '</p><p>', "</p>\n<p>" ), "</p>\n\n<p>", $text );
666 // visual editor likes to add <p>s. Buh-bye.
667 $text = $this->get_parser()->unp( $text );
668 // sometimes we get an encoded > at start of line, breaking blockquotes.
669 $text = preg_replace( '/^&gt;/m', '>', $text );
670 // prefixes are because we need to namespace footnotes by post_id.
671 $this->get_parser()->fn_id_prefix = $args['id'] ? $args['id'] . '-' : '';
672 // If we're not using the code shortcode, prevent over-encoding.
673 if ( $args['decode_code_blocks'] ) {
674 $text = $this->get_parser()->codeblock_restore( $text );
675 }
676 // Transform it!
677 $text = $this->get_parser()->transform( $text );
678 // Fix footnotes - kses doesn't like the : IDs it supplies.
679 $text = preg_replace( '/((id|href)="#?fn(ref)?):/', '$1-', $text );
680 // Markdown inserts extra spaces to make itself work. Buh-bye.
681 $text = rtrim( $text );
682 /**
683 * Filter the content to be run through Markdown, after it was transformed by Markdown.
684 *
685 * @module markdown
686 *
687 * @since 2.8.0
688 *
689 * @param string $text Content to be run through Markdown
690 * @param array $args Array of Markdown options.
691 */
692 $text = apply_filters( 'wpcom_markdown_transform_post', $text, $args );
693
694 // probably need to re-slash.
695 if ( $args['unslash'] ) {
696 $text = wp_slash( $text );
697 }
698
699 return $text;
700 }
701
702 /**
703 * Shows Markdown in the Revisions screen, and ensures that post_content_filtered
704 * is maintained on revisions
705 *
706 * @param array $fields Post fields pertinent to revisions.
707 */
708 public function wp_post_revision_fields( $fields ) {
709 $fields['post_content_filtered'] = __( 'Markdown content', 'jetpack' );
710 return $fields;
711 }
712
713 /**
714 * Do some song and dance to keep all post_content and post_content_filtered content
715 * in the expected place when a post revision is restored.
716 *
717 * @param int $post_id The post ID have a restore done to it.
718 * @param int $revision_id The revision ID being restored.
719 */
720 public function wp_restore_post_revision( $post_id, $revision_id ) {
721 if ( $this->is_markdown( $revision_id ) ) {
722 $revision = get_post( $revision_id, ARRAY_A );
723 $post = get_post( $post_id, ARRAY_A );
724 $post['post_content'] = $revision['post_content_filtered']; // Yes, we put it in post_content, because our wp_insert_post_data() expects that.
725 // set this flag so we can restore the post_content_filtered on the last revision later.
726 $this->monitoring['restore'] = true;
727 // let's not make a revision of our fixing update.
728 add_filter( 'wp_revisions_to_keep', '__return_false', 99 );
729 wp_update_post( $post );
730 $this->fix_latest_revision_on_restore( $post_id );
731 remove_filter( 'wp_revisions_to_keep', '__return_false', 99 );
732 }
733 }
734
735 /**
736 * We need to ensure the last revision has Markdown, not HTML in its post_content_filtered
737 * column after a restore.
738 *
739 * @param int $post_id The post ID that was just restored.
740 */
741 protected function fix_latest_revision_on_restore( $post_id ) {
742 global $wpdb;
743 $post = get_post( $post_id );
744 $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 ) );
745 $last_revision->post_content_filtered = $post->post_content_filtered;
746 wp_insert_post( (array) $last_revision );
747 }
748
749 /**
750 * Kicks off magic for an XML-RPC session. We want to keep editing Markdown
751 * and publishing HTML.
752 *
753 * @param string $xmlrpc_method The current XML-RPC method.
754 */
755 public function xmlrpc_actions( $xmlrpc_method ) {
756 switch ( $xmlrpc_method ) {
757 case 'metaWeblog.getRecentPosts':
758 case 'wp.getPosts':
759 case 'wp.getPages':
760 add_action( 'parse_query', array( $this, 'make_filterable' ), 10, 1 );
761 break;
762 case 'wp.getPost':
763 $this->prime_post_cache();
764 break;
765 }
766 }
767
768 /**
769 * Function metaWeblog.getPost and wp.getPage fire xmlrpc_call action *after* get_post() is called.
770 * So, we have to detect those methods and prime the post cache early.
771 *
772 * @return null
773 */
774 protected function check_for_early_methods() {
775 $raw_post_data = file_get_contents( 'php://input' );
776 if ( ! str_contains( $raw_post_data, 'metaWeblog.getPost' )
777 && ! str_contains( $raw_post_data, 'wp.getPage' ) ) {
778 return;
779 }
780 include_once ABSPATH . WPINC . '/class-IXR.php';
781 $message = new IXR_Message( $raw_post_data );
782 $message->parse();
783 $post_id_position = 'metaWeblog.getPost' === $message->methodName ? 0 : 1; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
784 $this->prime_post_cache( $message->params[ $post_id_position ] );
785 }
786
787 /**
788 * Prime the post cache with swapped post_content. This is a sneaky way of getting around
789 * the fact that there are no good hooks to call on the *.getPost xmlrpc methods.
790 *
791 * @param bool $post_id - the post ID that we're priming.
792 */
793 private function prime_post_cache( $post_id = false ) {
794 global $wp_xmlrpc_server;
795 if ( ! $post_id ) {
796 $post_id = $wp_xmlrpc_server->message->params[3];
797 }
798
799 // prime the post cache.
800 if ( $this->is_markdown( $post_id ) ) {
801 $post = get_post( $post_id );
802 if ( ! empty( $post->post_content_filtered ) ) {
803 wp_cache_delete( $post->ID, 'posts' );
804 $post = $this->swap_for_editing( $post );
805 wp_cache_add( $post->ID, $post, 'posts' );
806 $this->posts_to_uncache[] = $post_id;
807 }
808 }
809 // uncache munged posts if using a persistent object cache.
810 if ( wp_using_ext_object_cache() ) {
811 add_action( 'shutdown', array( $this, 'uncache_munged_posts' ) );
812 }
813 }
814
815 /**
816 * Swaps `post_content_filtered` back to `post_content` for editing purposes.
817 *
818 * @param object $post WP_Post object.
819 * @return object WP_Post object with swapped `post_content_filtered` and `post_content`.
820 */
821 protected function swap_for_editing( $post ) {
822 $markdown = $post->post_content_filtered;
823 // unencode encoded code blocks.
824 $markdown = $this->get_parser()->codeblock_restore( $markdown );
825 // restore beginning of line blockquotes.
826 $markdown = preg_replace( '/^&gt; /m', '> ', $markdown );
827 $post->post_content_filtered = $post->post_content;
828 $post->post_content = $markdown;
829 return $post;
830 }
831
832 /**
833 * We munge the post cache to serve proper markdown content to XML-RPC clients.
834 * Uncache these after the XML-RPC session ends.
835 */
836 public function uncache_munged_posts() {
837 // $this context gets lost in testing sometimes. Weird.
838 foreach ( self::get_instance()->posts_to_uncache as $post_id ) {
839 wp_cache_delete( $post_id, 'posts' );
840 }
841 }
842
843 /**
844 * Since *.(get)?[Rr]ecentPosts calls get_posts with suppress filters on, we need to
845 * turn them back on so that we can swap things for editing.
846 *
847 * @param object $wp_query WP_Query object.
848 */
849 public function make_filterable( $wp_query ) {
850 $wp_query->set( 'suppress_filters', false );
851 add_action( 'the_posts', array( $this, 'the_posts' ), 10, 2 );
852 }
853
854 /**
855 * Swaps post_content and post_content_filtered for editing.
856 *
857 * @param array $posts Posts returned by the just-completed query.
858 * @return array Modified $posts
859 */
860 public function the_posts( $posts ) {
861 foreach ( $posts as $key => $post ) {
862 if ( $this->is_markdown( $post->ID ) && ! empty( $posts[ $key ]->post_content_filtered ) ) {
863 $markdown = $posts[ $key ]->post_content_filtered;
864 $posts[ $key ]->post_content_filtered = $posts[ $key ]->post_content;
865 $posts[ $key ]->post_content = $markdown;
866 }
867 }
868 return $posts;
869 }
870
871 /**
872 * Singleton silence is golden
873 */
874 private function __construct() {}
875 }
876
877 add_action( 'init', array( WPCom_Markdown::get_instance(), 'load' ) );
878