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