Completed
Push — add/changelog-62 ( 9977bb...d371c2 )
by
unknown
10:21
created

WPCom_Markdown::wp_kses_allowed_html()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 4
nop 2
dl 0
loc 15
rs 9.2
c 0
b 0
f 0
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 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $new_blog_id of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
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_action( 'wp_insert_post', array( $this, 'wp_insert_post' ) );
119
		add_filter( 'wp_insert_post_data', array( $this, 'wp_insert_post_data' ), 10, 2 );
120
		add_filter( 'edit_post_content', array( $this, 'edit_post_content' ), 10, 2 );
121
		add_filter( 'edit_post_content_filtered', array( $this, 'edit_post_content_filtered' ), 10, 2 );
122
		add_action( 'wp_restore_post_revision', array( $this, 'wp_restore_post_revision' ), 10, 2 );
123
		add_filter( '_wp_post_revision_fields', array( $this, '_wp_post_revision_fields' ) );
124
		add_action( 'xmlrpc_call', array( $this, 'xmlrpc_actions' ) );
125
		add_filter( 'content_save_pre', array( $this, 'preserve_code_blocks' ), 1 );
126
		if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
127
			$this->check_for_early_methods();
128
		}
129
	}
130
131
	/**
132
	 * Removes hooks to disable Markdown conversion on posts
133
	 * @return null
134
	 */
135
	public function unload_markdown_for_posts() {
136
		remove_action( 'wp_insert_post', array( $this, 'wp_insert_post' ) );
137
		remove_filter( 'wp_insert_post_data', array( $this, 'wp_insert_post_data' ), 10, 2 );
138
		remove_filter( 'edit_post_content', array( $this, 'edit_post_content' ), 10, 2 );
139
		remove_filter( 'edit_post_content_filtered', array( $this, 'edit_post_content_filtered' ), 10, 2 );
140
		remove_action( 'wp_restore_post_revision', array( $this, 'wp_restore_post_revision' ), 10, 2 );
141
		remove_filter( '_wp_post_revision_fields', array( $this, '_wp_post_revision_fields' ) );
142
		remove_action( 'xmlrpc_call', array( $this, 'xmlrpc_actions' ) );
143
		remove_filter( 'content_save_pre', array( $this, 'preserve_code_blocks' ), 1 );
144
	}
145
146
	/**
147
	 * Set up hooks for enabling Markdown conversion on comments
148
	 * @return null
149
	 */
150
	protected function load_markdown_for_comments() {
151
		// Use priority 9 so that Markdown runs before KSES, which can clean up
152
		// any munged HTML.
153
		add_filter( 'pre_comment_content', array( $this, 'pre_comment_content' ), 9 );
154
	}
155
156
	/**
157
	 * Removes hooks to disable Markdown conversion
158
	 * @return null
159
	 */
160
	protected function unload_markdown_for_comments() {
161
		remove_filter( 'pre_comment_content', array( $this, 'pre_comment_content' ), 9 );
162
	}
163
164
	/**
165
	 * o2 does some of what we do. Let's take precedence.
166
	 * @return null
167
	 */
168
	public function add_o2_helpers() {
169
		if ( $this->is_posting_enabled() ) {
170
			add_filter( 'content_save_pre', array( $this, 'o2_escape_lists' ), 1 );
171
		}
172
173
		add_filter( 'o2_preview_post', array( $this, 'o2_preview_post' ) );
174
		add_filter( 'o2_preview_comment', array( $this, 'o2_preview_comment' ) );
175
176
		add_filter( 'wpcom_markdown_transform_pre', array( $this, 'o2_unescape_lists' ) );
177
		add_filter( 'wpcom_untransformed_content', array( $this, 'o2_unescape_lists' ) );
178
	}
179
180
	/**
181
	 * If Markdown is enabled for posts on this blog, filter the text for o2 previews
182
	 * @param  string $text Post text
183
	 * @return string       Post text transformed through the magic of Markdown
184
	 */
185
	public function o2_preview_post( $text ) {
186
		if ( $this->is_posting_enabled() ) {
187
			$text = $this->transform( $text, array( 'unslash' => false ) );
188
		}
189
		return $text;
190
	}
191
192
	/**
193
	 * If Markdown is enabled for comments on this blog, filter the text for o2 previews
194
	 * @param  string $text Comment text
195
	 * @return string       Comment text transformed through the magic of Markdown
196
	 */
197
	public function o2_preview_comment( $text ) {
198
		if ( $this->is_commenting_enabled() ) {
199
			$text = $this->transform( $text, array( 'unslash' => false ) );
200
		}
201
		return $text;
202
	}
203
204
	/**
205
	 * Escapes lists so that o2 doesn't trounce them
206
	 * @param  string $text Post/comment text
207
	 * @return string       Text escaped with HTML entity for asterisk
208
	 */
209
	public function o2_escape_lists( $text ) {
210
		return preg_replace( '/^\\* /um', '&#42; ', $text );
211
	}
212
213
	/**
214
	 * Unescapes the token we inserted on o2_escape_lists
215
	 * @param  string $text Post/comment text with HTML entities for asterisks
216
	 * @return string       Text with the HTML entity removed
217
	 */
218
	public function o2_unescape_lists( $text ) {
219
		return preg_replace( '/^[&]\#042; /um', '* ', $text );
220
	}
221
222
	/**
223
	 * Preserve code blocks from being munged by KSES before they have a chance
224
	 * @param  string $text post content
225
	 * @return string       post content with code blocks escaped
226
	 */
227
	public function preserve_code_blocks( $text ) {
228
		return $this->get_parser()->codeblock_preserve( $text );
229
	}
230
231
	/**
232
	 * Remove KSES if it's there. Store the result to manually invoke later if needed.
233
	 * @return null
234
	 */
235
	public function maybe_remove_kses() {
236
		// Filters return true if they existed before you removed them
237
		if ( $this->is_posting_enabled() )
238
			$this->kses = remove_filter( 'content_filtered_save_pre', 'wp_filter_post_kses' ) && remove_filter( 'content_save_pre', 'wp_filter_post_kses' );
0 ignored issues
show
Bug introduced by
The property kses does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
239
	}
240
241
	/**
242
	 * Add our Writing and Discussion settings.
243
	 * @return null
244
	 */
245
	public function register_setting() {
246
		add_settings_field( self::POST_OPTION, __( 'Markdown', 'jetpack' ), array( $this, 'post_field' ), 'writing' );
247
		register_setting( 'writing', self::POST_OPTION, array( $this, 'sanitize_setting') );
248
		add_settings_field( self::COMMENT_OPTION, __( 'Markdown', 'jetpack' ), array( $this, 'comment_field' ), 'discussion' );
249
		register_setting( 'discussion', self::COMMENT_OPTION, array( $this, 'sanitize_setting') );
250
	}
251
252
	/**
253
	 * Sanitize setting. Don't really want to store "on" value, so we'll store "1" instead!
254
	 * @param  string $input Value received by settings API via $_POST
255
	 * @return bool          Cast to boolean.
256
	 */
257
	public function sanitize_setting( $input ) {
258
		return (bool) $input;
259
	}
260
261
	/**
262
	 * Prints HTML for the Writing setting
263
	 * @return null
264
	 */
265 View Code Duplication
	public function post_field() {
266
		printf(
267
			'<label><input name="%s" id="%s" type="checkbox"%s /> %s</label><p class="description">%s</p>',
268
			self::POST_OPTION,
269
			self::POST_OPTION,
270
			checked( $this->is_posting_enabled(), true, false ),
271
			esc_html__( 'Use Markdown for posts and pages.', 'jetpack' ),
272
			sprintf( '<a href="%s">%s</a>', esc_url( $this->get_support_url() ), esc_html__( 'Learn more about Markdown.', 'jetpack' ) )
273
		);
274
	}
275
276
	/**
277
	 * Prints HTML for the Discussion setting
278
	 * @return null
279
	 */
280 View Code Duplication
	public function comment_field() {
281
		printf(
282
			'<label><input name="%s" id="%s" type="checkbox"%s /> %s</label><p class="description">%s</p>',
283
			self::COMMENT_OPTION,
284
			self::COMMENT_OPTION,
285
			checked( $this->is_commenting_enabled(), true, false ),
286
			esc_html__( 'Use Markdown for comments.', 'jetpack' ),
287
			sprintf( '<a href="%s">%s</a>', esc_url( $this->get_support_url() ), esc_html__( 'Learn more about Markdown.', 'jetpack' ) )
288
		);
289
	}
290
291
	/**
292
	 * Get the support url for Markdown
293
	 * @uses   apply_filters
294
	 * @return string support url
295
	 */
296
	protected function get_support_url() {
297
		/**
298
		 * Filter the Markdown support URL.
299
		 *
300
		 * @module markdown
301
		 *
302
		 * @since 2.8.0
303
		 *
304
		 * @param string $url Markdown support URL.
305
		 */
306
		return apply_filters( 'easy_markdown_support_url', 'http://en.support.wordpress.com/markdown-quick-reference/' );
307
	}
308
309
	/**
310
	 * Is Mardown conversion for posts enabled?
311
	 * @return boolean
312
	 */
313
	public function is_posting_enabled() {
314
		return (bool) Jetpack_Options::get_option_and_ensure_autoload( self::POST_OPTION, '' );
315
	}
316
317
	/**
318
	 * Is Markdown conversion for comments enabled?
319
	 * @return boolean
320
	 */
321
	public function is_commenting_enabled() {
322
		return (bool) Jetpack_Options::get_option_and_ensure_autoload( self::COMMENT_OPTION, '' );
323
	}
324
325
	/**
326
	 * Check if a $post_id has Markdown enabled
327
	 * @param  int  $post_id A post ID.
328
	 * @return boolean
329
	 */
330
	public function is_markdown( $post_id ) {
331
		return get_metadata( 'post', $post_id, self::IS_MD_META, true );
332
	}
333
334
	/**
335
	 * Set Markdown as enabled on a post_id. We skip over update_postmeta so we
336
	 * can sneakily set metadata on post revisions, which we need.
337
	 * @param int    $post_id A post ID.
338
	 * @return bool  The metadata was successfully set.
339
	 */
340
	protected function set_as_markdown( $post_id ) {
341
		return update_metadata( 'post', $post_id, self::IS_MD_META, true );
342
	}
343
344
	/**
345
	 * Get our Markdown parser object, optionally requiring all of our needed classes and
346
	 * instantiating our parser.
347
	 * @return object WPCom_GHF_Markdown_Parser instance.
348
	 */
349
	public function get_parser() {
350
351
		if ( ! self::$parser ) {
352
			jetpack_require_lib( 'markdown' );
353
			self::$parser = new WPCom_GHF_Markdown_Parser;
354
		}
355
356
		return self::$parser;
357
	}
358
359
	/**
360
	 * We don't want Markdown conversion all over the place.
361
	 * @return null
362
	 */
363
	public function add_default_post_type_support() {
364
		add_post_type_support( 'post', self::POST_TYPE_SUPPORT );
365
		add_post_type_support( 'page', self::POST_TYPE_SUPPORT );
366
		add_post_type_support( 'revision', self::POST_TYPE_SUPPORT );
367
	}
368
369
	/**
370
	 * Figure out the post type of the post screen we're on
371
	 * @return string Current post_type
372
	 */
373
	protected function get_post_screen_post_type() {
374
		global $pagenow;
375
		if ( 'post-new.php' === $pagenow )
376
			return ( isset( $_GET['post_type'] ) ) ? $_GET['post_type'] : 'post';
377
		if ( isset( $_GET['post'] ) ) {
378
			$post = get_post( (int) $_GET['post'] );
379
			if ( is_object( $post ) && isset( $post->post_type ) )
380
				return $post->post_type;
381
		}
382
		return 'post';
383
	}
384
385
	/**
386
	 * Swap post_content and post_content_filtered for editing
387
	 * @param  string $content Post content
388
	 * @param  int $id         post ID
389
	 * @return string          Swapped content
390
	 */
391 View Code Duplication
	public function edit_post_content( $content, $id ) {
392
		if ( $this->is_markdown( $id ) ) {
393
			$post = get_post( $id );
394
			if ( $post && ! empty( $post->post_content_filtered ) ) {
395
				$post = $this->swap_for_editing( $post );
396
				return $post->post_content;
397
			}
398
		}
399
		return $content;
400
	}
401
402
	/**
403
	 * Swap post_content_filtered and post_content for editing
404
	 * @param  string $content Post content_filtered
405
	 * @param  int $id         post ID
406
	 * @return string          Swapped content
407
	 */
408 View Code Duplication
	public function edit_post_content_filtered( $content, $id ) {
409
		// if markdown was disabled, let's turn this off
410
		if ( ! $this->is_posting_enabled() && $this->is_markdown( $id ) ) {
411
			$post = get_post( $id );
412
			if ( $post && ! empty( $post->post_content_filtered ) )
413
				$content = '';
414
		}
415
		return $content;
416
	}
417
418
	/**
419
	 * Magic happens here. Markdown is converted and stored on post_content. Original Markdown is stored
420
	 * in post_content_filtered so that we can continue editing as Markdown.
421
	 * @param  array $post_data  The post data that will be inserted into the DB. Slashed.
422
	 * @param  array $postarr    All the stuff that was in $_POST.
423
	 * @return array             $post_data with post_content and post_content_filtered modified
424
	 */
425
	public function wp_insert_post_data( $post_data, $postarr ) {
426
		// $post_data array is slashed!
427
		$post_id = isset( $postarr['ID'] ) ? $postarr['ID'] : false;
428
		// bail early if markdown is disabled or this post type is unsupported.
429
		if ( ! $this->is_posting_enabled() || ! post_type_supports( $post_data['post_type'], self::POST_TYPE_SUPPORT ) ) {
430
			// it's disabled, but maybe this *was* a markdown post before.
431
			if ( $this->is_markdown( $post_id ) && ! empty( $post_data['post_content_filtered'] ) ) {
432
				$post_data['post_content_filtered'] = '';
433
			}
434
			// we have no context to determine supported post types in the `post_content_pre` hook,
435
			// which already ran to sanitize code blocks. Undo that.
436
			$post_data['post_content'] = $this->get_parser()->codeblock_restore( $post_data['post_content'] );
437
			return $post_data;
438
		}
439
		// rejigger post_content and post_content_filtered
440
		// revisions are already in the right place, except when we're restoring, but that's taken care of elsewhere
441
		// also prevent quick edit feature from overriding already-saved markdown (issue https://github.com/Automattic/jetpack/issues/636)
442
		if ( 'revision' !== $post_data['post_type'] && ! isset( $_POST['_inline_edit'] ) ) {
443
			/**
444
			 * Filter the original post content passed to Markdown.
445
			 *
446
			 * @module markdown
447
			 *
448
			 * @since 2.8.0
449
			 *
450
			 * @param string $post_data['post_content'] Untransformed post content.
451
			 */
452
			$post_data['post_content_filtered'] = apply_filters( 'wpcom_untransformed_content', $post_data['post_content'] );
453
			$post_data['post_content'] = $this->transform( $post_data['post_content'], array( 'id' => $post_id ) );
454
			/** This filter is already documented in core/wp-includes/default-filters.php */
455
			$post_data['post_content'] = apply_filters( 'content_save_pre', $post_data['post_content'] );
456
		} elseif ( 0 === strpos( $post_data['post_name'], $post_data['post_parent'] . '-autosave' ) ) {
457
			// autosaves for previews are weird
458
			/** This filter is already documented in modules/markdown/easy-markdown.php */
459
			$post_data['post_content_filtered'] = apply_filters( 'wpcom_untransformed_content', $post_data['post_content'] );
460
			$post_data['post_content'] = $this->transform( $post_data['post_content'], array( 'id' => $post_data['post_parent'] ) );
461
			/** This filter is already documented in core/wp-includes/default-filters.php */
462
			$post_data['post_content'] = apply_filters( 'content_save_pre', $post_data['post_content'] );
463
		}
464
465
		// set as markdown on the wp_insert_post hook later
466
		if ( $post_id )
467
			$this->monitoring['post'][ $post_id ] = true;
468
		else
469
			$this->monitoring['content'] = wp_unslash( $post_data['post_content'] );
470
		if ( 'revision' === $postarr['post_type'] && $this->is_markdown( $postarr['post_parent'] ) )
471
			$this->monitoring['parent'][ $postarr['post_parent'] ] = true;
472
473
		return $post_data;
474
	}
475
476
	/**
477
	 * Calls on wp_insert_post action, after wp_insert_post_data. This way we can
478
	 * still set postmeta on our revisions after it's all been deleted.
479
	 * @param  int $post_id The post ID that has just been added/updated
480
	 * @return null
481
	 */
482
	public function wp_insert_post( $post_id ) {
483
		$post_parent = get_post_field( 'post_parent', $post_id );
484
		// this didn't have an ID yet. Compare the content that was just saved.
485
		if ( isset( $this->monitoring['content'] ) && $this->monitoring['content'] === get_post_field( 'post_content', $post_id ) ) {
486
			unset( $this->monitoring['content'] );
487
			$this->set_as_markdown( $post_id );
488
		}
489
		if ( isset( $this->monitoring['post'][$post_id] ) ) {
490
			unset( $this->monitoring['post'][$post_id] );
491
			$this->set_as_markdown( $post_id );
492
		} elseif ( isset( $this->monitoring['parent'][$post_parent] ) ) {
493
			unset( $this->monitoring['parent'][$post_parent] );
494
			$this->set_as_markdown( $post_id );
495
		}
496
	}
497
498
	/**
499
	 * Run a comment through Markdown. Easy peasy.
500
	 * @param  string $content
501
	 * @return string
502
	 */
503
	public function pre_comment_content( $content ) {
504
		return $this->transform( $content, array(
505
			'id' => $this->comment_hash( $content ),
506
		) );
507
	}
508
509
	protected function comment_hash( $content ) {
510
		return 'c-' . substr( md5( $content ), 0, 8 );
511
	}
512
513
	/**
514
	 * Markdown conversion. Some DRYness for repetitive tasks.
515
	 * @param  string $text  Content to be run through Markdown
516
	 * @param  array  $args  Arguments, with keys:
517
	 *                       id: provide a string to prefix footnotes with a unique identifier
518
	 *                       unslash: when true, expects and returns slashed data
519
	 *                       decode_code_blocks: when true, assume that text in fenced code blocks is already
520
	 *                         HTML encoded and should be decoded before being passed to Markdown, which does
521
	 *                         its own encoding.
522
	 * @return string        Markdown-processed content
523
	 */
524
	public function transform( $text, $args = array() ) {
525
		$args = wp_parse_args( $args, array(
526
			'id' => false,
527
			'unslash' => true,
528
			'decode_code_blocks' => ! $this->get_parser()->use_code_shortcode
529
		) );
530
		// probably need to unslash
531
		if ( $args['unslash'] )
532
			$text = wp_unslash( $text );
533
534
		/**
535
		 * Filter the content to be run through Markdown, before it's transformed by Markdown.
536
		 *
537
		 * @module markdown
538
		 *
539
		 * @since 2.8.0
540
		 *
541
		 * @param string $text Content to be run through Markdown
542
		 * @param array $args Array of Markdown options.
543
		 */
544
		$text = apply_filters( 'wpcom_markdown_transform_pre', $text, $args );
545
		// ensure our paragraphs are separated
546
		$text = str_replace( array( '</p><p>', "</p>\n<p>" ), "</p>\n\n<p>", $text );
547
		// visual editor likes to add <p>s. Buh-bye.
548
		$text = $this->get_parser()->unp( $text );
549
		// sometimes we get an encoded > at start of line, breaking blockquotes
550
		$text = preg_replace( '/^&gt;/m', '>', $text );
551
		// prefixes are because we need to namespace footnotes by post_id
552
		$this->get_parser()->fn_id_prefix = $args['id'] ? $args['id'] . '-' : '';
553
		// If we're not using the code shortcode, prevent over-encoding.
554
		if ( $args['decode_code_blocks'] ) {
555
			$text = $this->get_parser()->codeblock_restore( $text );
556
		}
557
		// Transform it!
558
		$text = $this->get_parser()->transform( $text );
559
		// Fix footnotes - kses doesn't like the : IDs it supplies
560
		$text = preg_replace( '/((id|href)="#?fn(ref)?):/', "$1-", $text );
561
		// Markdown inserts extra spaces to make itself work. Buh-bye.
562
		$text = rtrim( $text );
563
		/**
564
		 * Filter the content to be run through Markdown, after it was transformed by Markdown.
565
		 *
566
		 * @module markdown
567
		 *
568
		 * @since 2.8.0
569
		 *
570
		 * @param string $text Content to be run through Markdown
571
		 * @param array $args Array of Markdown options.
572
		 */
573
		$text = apply_filters( 'wpcom_markdown_transform_post', $text, $args );
574
575
		// probably need to re-slash
576
		if ( $args['unslash'] )
577
			$text = wp_slash( $text );
578
579
		return $text;
580
	}
581
582
	/**
583
	 * Shows Markdown in the Revisions screen, and ensures that post_content_filtered
584
	 * is maintained on revisions
585
	 * @param  array $fields  Post fields pertinent to revisions
586
	 * @return array          Modified array to include post_content_filtered
587
	 */
588
	public function _wp_post_revision_fields( $fields ) {
589
		$fields['post_content_filtered'] = __( 'Markdown content', 'jetpack' );
590
		return $fields;
591
	}
592
593
	/**
594
	 * Do some song and dance to keep all post_content and post_content_filtered content
595
	 * in the expected place when a post revision is restored.
596
	 * @param  int $post_id        The post ID have a restore done to it
597
	 * @param  int $revision_id    The revision ID being restored
598
	 * @return null
599
	 */
600
	public function wp_restore_post_revision( $post_id, $revision_id ) {
601
		if ( $this->is_markdown( $revision_id ) ) {
602
			$revision = get_post( $revision_id, ARRAY_A );
603
			$post = get_post( $post_id, ARRAY_A );
604
			$post['post_content'] = $revision['post_content_filtered']; // Yes, we put it in post_content, because our wp_insert_post_data() expects that
605
			// set this flag so we can restore the post_content_filtered on the last revision later
606
			$this->monitoring['restore'] = true;
607
			// let's not make a revision of our fixing update
608
			add_filter( 'wp_revisions_to_keep', '__return_false', 99 );
609
			wp_update_post( $post );
610
			$this->fix_latest_revision_on_restore( $post_id );
611
			remove_filter( 'wp_revisions_to_keep', '__return_false', 99 );
612
		}
613
	}
614
615
	/**
616
	 * We need to ensure the last revision has Markdown, not HTML in its post_content_filtered
617
	 * column after a restore.
618
	 * @param  int $post_id The post ID that was just restored.
619
	 * @return null
620
	 */
621
	protected function fix_latest_revision_on_restore( $post_id ) {
622
		global $wpdb;
623
		$post = get_post( $post_id );
624
		$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 ) );
625
		$last_revision->post_content_filtered = $post->post_content_filtered;
626
		wp_insert_post( (array) $last_revision );
627
	}
628
629
	/**
630
	 * Kicks off magic for an XML-RPC session. We want to keep editing Markdown
631
	 * and publishing HTML.
632
	 * @param  string $xmlrpc_method The current XML-RPC method
633
	 * @return null
634
	 */
635
	public function xmlrpc_actions( $xmlrpc_method ) {
636
		switch ( $xmlrpc_method ) {
637
			case 'metaWeblog.getRecentPosts':
638
			case 'wp.getPosts':
639
			case 'wp.getPages':
640
				add_action( 'parse_query', array( $this, 'make_filterable' ), 10, 1 );
641
				break;
642
			case 'wp.getPost':
643
				$this->prime_post_cache();
644
				break;
645
		}
646
	}
647
648
	/**
649
	 * metaWeblog.getPost and wp.getPage fire xmlrpc_call action *after* get_post() is called.
650
	 * So, we have to detect those methods and prime the post cache early.
651
	 * @return null
652
	 */
653
	protected function check_for_early_methods() {
654
		global $HTTP_RAW_POST_DATA;
655
		if ( false === strpos( $HTTP_RAW_POST_DATA, 'metaWeblog.getPost' )
656
			&& false === strpos( $HTTP_RAW_POST_DATA, 'wp.getPage' ) ) {
657
			return;
658
		}
659
		include_once( ABSPATH . WPINC . '/class-IXR.php' );
660
		$message = new IXR_Message( $HTTP_RAW_POST_DATA );
661
		$message->parse();
662
		$post_id_position = 'metaWeblog.getPost' === $message->methodName ?  0 : 1;
663
		$this->prime_post_cache( $message->params[ $post_id_position ] );
664
	}
665
666
	/**
667
	 * Prime the post cache with swapped post_content. This is a sneaky way of getting around
668
	 * the fact that there are no good hooks to call on the *.getPost xmlrpc methods.
669
	 *
670
	 * @return null
671
	 */
672
	private function prime_post_cache( $post_id = false ) {
673
		global $wp_xmlrpc_server;
674
		if ( ! $post_id ) {
675
			$post_id = $wp_xmlrpc_server->message->params[3];
676
		}
677
678
		// prime the post cache
679
		if ( $this->is_markdown( $post_id ) ) {
680
			$post = get_post( $post_id );
681
			if ( ! empty( $post->post_content_filtered ) ) {
682
				wp_cache_delete( $post->ID, 'posts' );
683
				$post = $this->swap_for_editing( $post );
684
				wp_cache_add( $post->ID, $post, 'posts' );
685
				$this->posts_to_uncache[] = $post_id;
686
			}
687
		}
688
		// uncache munged posts if using a persistent object cache
689
		if ( wp_using_ext_object_cache() ) {
690
			add_action( 'shutdown', array( $this, 'uncache_munged_posts' ) );
691
		}
692
	}
693
694
	/**
695
	 * Swaps `post_content_filtered` back to `post_content` for editing purposes.
696
	 * @param  object $post WP_Post object
697
	 * @return object       WP_Post object with swapped `post_content_filtered` and `post_content`
698
	 */
699
	protected function swap_for_editing( $post ) {
700
		$markdown = $post->post_content_filtered;
701
		// unencode encoded code blocks
702
		$markdown = $this->get_parser()->codeblock_restore( $markdown );
703
		// restore beginning of line blockquotes
704
		$markdown = preg_replace( '/^&gt; /m', '> ', $markdown );
705
		$post->post_content_filtered = $post->post_content;
706
		$post->post_content = $markdown;
707
		return $post;
708
	}
709
710
711
	/**
712
	 * We munge the post cache to serve proper markdown content to XML-RPC clients.
713
	 * Uncache these after the XML-RPC session ends.
714
	 * @return null
715
	 */
716
	public function uncache_munged_posts() {
717
		// $this context gets lost in testing sometimes. Weird.
718
		foreach( WPCom_Markdown::get_instance()->posts_to_uncache as $post_id ) {
719
			wp_cache_delete( $post_id, 'posts' );
720
		}
721
	}
722
723
	/**
724
	 * Since *.(get)?[Rr]ecentPosts calls get_posts with suppress filters on, we need to
725
	 * turn them back on so that we can swap things for editing.
726
	 * @param  object $wp_query WP_Query object
727
	 * @return null
728
	 */
729
	public function make_filterable( $wp_query ) {
730
		$wp_query->set( 'suppress_filters', false );
731
		add_action( 'the_posts', array( $this, 'the_posts' ), 10, 2 );
732
	}
733
734
	/**
735
	 * Swaps post_content and post_content_filtered for editing.
736
	 * @param  array  $posts     Posts returned by the just-completed query
737
	 * @param  object $wp_query  Current WP_Query object
738
	 * @return array             Modified $posts
739
	 */
740
	public function the_posts( $posts, $wp_query ) {
741
		foreach ( $posts as $key => $post ) {
742
			if ( $this->is_markdown( $post->ID ) && ! empty( $posts[ $key ]->post_content_filtered ) ) {
743
				$markdown = $posts[ $key ]->post_content_filtered;
744
				$posts[ $key ]->post_content_filtered = $posts[ $key ]->post_content;
745
				$posts[ $key ]->post_content = $markdown;
746
			}
747
		}
748
		return $posts;
749
	}
750
751
	/**
752
	 * Singleton silence is golden
753
	 */
754
	private function __construct() {}
755
}
756
757
add_action( 'init', array( WPCom_Markdown::get_instance(), 'load' ) );
758