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