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