Completed
Push — add/geo-location-support ( 007f25...6296b5 )
by Brad
29:34 queued 17:46
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_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' );
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...
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 View Code Duplication
	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 View Code Duplication
	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 View Code Duplication
	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 View Code Duplication
	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
	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
		$args = wp_parse_args( $args, array(
576
			'id' => false,
577
			'unslash' => true,
578
			'decode_code_blocks' => ! $this->get_parser()->use_code_shortcode
579
		) );
580
		// probably need to unslash
581
		if ( $args['unslash'] )
582
			$text = wp_unslash( $text );
583
584
		/**
585
		 * Filter the content to be run through Markdown, before it's transformed by Markdown.
586
		 *
587
		 * @module markdown
588
		 *
589
		 * @since 2.8.0
590
		 *
591
		 * @param string $text Content to be run through Markdown
592
		 * @param array $args Array of Markdown options.
593
		 */
594
		$text = apply_filters( 'wpcom_markdown_transform_pre', $text, $args );
595
		// ensure our paragraphs are separated
596
		$text = str_replace( array( '</p><p>', "</p>\n<p>" ), "</p>\n\n<p>", $text );
597
		// visual editor likes to add <p>s. Buh-bye.
598
		$text = $this->get_parser()->unp( $text );
599
		// sometimes we get an encoded > at start of line, breaking blockquotes
600
		$text = preg_replace( '/^&gt;/m', '>', $text );
601
		// prefixes are because we need to namespace footnotes by post_id
602
		$this->get_parser()->fn_id_prefix = $args['id'] ? $args['id'] . '-' : '';
603
		// If we're not using the code shortcode, prevent over-encoding.
604
		if ( $args['decode_code_blocks'] ) {
605
			$text = $this->get_parser()->codeblock_restore( $text );
606
		}
607
		// Transform it!
608
		$text = $this->get_parser()->transform( $text );
609
		// Fix footnotes - kses doesn't like the : IDs it supplies
610
		$text = preg_replace( '/((id|href)="#?fn(ref)?):/', "$1-", $text );
611
		// Markdown inserts extra spaces to make itself work. Buh-bye.
612
		$text = rtrim( $text );
613
		/**
614
		 * Filter the content to be run through Markdown, after it was transformed by Markdown.
615
		 *
616
		 * @module markdown
617
		 *
618
		 * @since 2.8.0
619
		 *
620
		 * @param string $text Content to be run through Markdown
621
		 * @param array $args Array of Markdown options.
622
		 */
623
		$text = apply_filters( 'wpcom_markdown_transform_post', $text, $args );
624
625
		// probably need to re-slash
626
		if ( $args['unslash'] )
627
			$text = wp_slash( $text );
628
629
		return $text;
630
	}
631
632
	/**
633
	 * Shows Markdown in the Revisions screen, and ensures that post_content_filtered
634
	 * is maintained on revisions
635
	 * @param  array $fields  Post fields pertinent to revisions
636
	 * @return array          Modified array to include post_content_filtered
637
	 */
638
	public function _wp_post_revision_fields( $fields ) {
639
		$fields['post_content_filtered'] = __( 'Markdown content', 'jetpack' );
640
		return $fields;
641
	}
642
643
	/**
644
	 * Do some song and dance to keep all post_content and post_content_filtered content
645
	 * in the expected place when a post revision is restored.
646
	 * @param  int $post_id        The post ID have a restore done to it
647
	 * @param  int $revision_id    The revision ID being restored
648
	 * @return null
649
	 */
650
	public function wp_restore_post_revision( $post_id, $revision_id ) {
651
		if ( $this->is_markdown( $revision_id ) ) {
652
			$revision = get_post( $revision_id, ARRAY_A );
653
			$post = get_post( $post_id, ARRAY_A );
654
			$post['post_content'] = $revision['post_content_filtered']; // Yes, we put it in post_content, because our wp_insert_post_data() expects that
655
			// set this flag so we can restore the post_content_filtered on the last revision later
656
			$this->monitoring['restore'] = true;
657
			// let's not make a revision of our fixing update
658
			add_filter( 'wp_revisions_to_keep', '__return_false', 99 );
659
			wp_update_post( $post );
660
			$this->fix_latest_revision_on_restore( $post_id );
661
			remove_filter( 'wp_revisions_to_keep', '__return_false', 99 );
662
		}
663
	}
664
665
	/**
666
	 * We need to ensure the last revision has Markdown, not HTML in its post_content_filtered
667
	 * column after a restore.
668
	 * @param  int $post_id The post ID that was just restored.
669
	 * @return null
670
	 */
671
	protected function fix_latest_revision_on_restore( $post_id ) {
672
		global $wpdb;
673
		$post = get_post( $post_id );
674
		$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 ) );
675
		$last_revision->post_content_filtered = $post->post_content_filtered;
676
		wp_insert_post( (array) $last_revision );
677
	}
678
679
	/**
680
	 * Kicks off magic for an XML-RPC session. We want to keep editing Markdown
681
	 * and publishing HTML.
682
	 * @param  string $xmlrpc_method The current XML-RPC method
683
	 * @return null
684
	 */
685
	public function xmlrpc_actions( $xmlrpc_method ) {
686
		switch ( $xmlrpc_method ) {
687
			case 'metaWeblog.getRecentPosts':
688
			case 'wp.getPosts':
689
			case 'wp.getPages':
690
				add_action( 'parse_query', array( $this, 'make_filterable' ), 10, 1 );
691
				break;
692
			case 'wp.getPost':
693
				$this->prime_post_cache();
694
				break;
695
		}
696
	}
697
698
	/**
699
	 * metaWeblog.getPost and wp.getPage fire xmlrpc_call action *after* get_post() is called.
700
	 * So, we have to detect those methods and prime the post cache early.
701
	 * @return null
702
	 */
703
	protected function check_for_early_methods() {
704
		global $HTTP_RAW_POST_DATA;
705
		if ( false === strpos( $HTTP_RAW_POST_DATA, 'metaWeblog.getPost' )
706
			&& false === strpos( $HTTP_RAW_POST_DATA, 'wp.getPage' ) ) {
707
			return;
708
		}
709
		include_once( ABSPATH . WPINC . '/class-IXR.php' );
710
		$message = new IXR_Message( $HTTP_RAW_POST_DATA );
711
		$message->parse();
712
		$post_id_position = 'metaWeblog.getPost' === $message->methodName ?  0 : 1;
713
		$this->prime_post_cache( $message->params[ $post_id_position ] );
714
	}
715
716
	/**
717
	 * Prime the post cache with swapped post_content. This is a sneaky way of getting around
718
	 * the fact that there are no good hooks to call on the *.getPost xmlrpc methods.
719
	 *
720
	 * @return null
721
	 */
722
	private function prime_post_cache( $post_id = false ) {
723
		global $wp_xmlrpc_server;
724
		if ( ! $post_id ) {
725
			$post_id = $wp_xmlrpc_server->message->params[3];
726
		}
727
728
		// prime the post cache
729
		if ( $this->is_markdown( $post_id ) ) {
730
			$post = get_post( $post_id );
731
			if ( ! empty( $post->post_content_filtered ) ) {
732
				wp_cache_delete( $post->ID, 'posts' );
733
				$post = $this->swap_for_editing( $post );
734
				wp_cache_add( $post->ID, $post, 'posts' );
735
				$this->posts_to_uncache[] = $post_id;
736
			}
737
		}
738
		// uncache munged posts if using a persistent object cache
739
		if ( wp_using_ext_object_cache() ) {
740
			add_action( 'shutdown', array( $this, 'uncache_munged_posts' ) );
741
		}
742
	}
743
744
	/**
745
	 * Swaps `post_content_filtered` back to `post_content` for editing purposes.
746
	 * @param  object $post WP_Post object
747
	 * @return object       WP_Post object with swapped `post_content_filtered` and `post_content`
748
	 */
749
	protected function swap_for_editing( $post ) {
750
		$markdown = $post->post_content_filtered;
751
		// unencode encoded code blocks
752
		$markdown = $this->get_parser()->codeblock_restore( $markdown );
753
		// restore beginning of line blockquotes
754
		$markdown = preg_replace( '/^&gt; /m', '> ', $markdown );
755
		$post->post_content_filtered = $post->post_content;
756
		$post->post_content = $markdown;
757
		return $post;
758
	}
759
760
761
	/**
762
	 * We munge the post cache to serve proper markdown content to XML-RPC clients.
763
	 * Uncache these after the XML-RPC session ends.
764
	 * @return null
765
	 */
766
	public function uncache_munged_posts() {
767
		// $this context gets lost in testing sometimes. Weird.
768
		foreach( WPCom_Markdown::get_instance()->posts_to_uncache as $post_id ) {
769
			wp_cache_delete( $post_id, 'posts' );
770
		}
771
	}
772
773
	/**
774
	 * Since *.(get)?[Rr]ecentPosts calls get_posts with suppress filters on, we need to
775
	 * turn them back on so that we can swap things for editing.
776
	 * @param  object $wp_query WP_Query object
777
	 * @return null
778
	 */
779
	public function make_filterable( $wp_query ) {
780
		$wp_query->set( 'suppress_filters', false );
781
		add_action( 'the_posts', array( $this, 'the_posts' ), 10, 2 );
782
	}
783
784
	/**
785
	 * Swaps post_content and post_content_filtered for editing.
786
	 * @param  array  $posts     Posts returned by the just-completed query
787
	 * @param  object $wp_query  Current WP_Query object
788
	 * @return array             Modified $posts
789
	 */
790
	public function the_posts( $posts, $wp_query ) {
791
		foreach ( $posts as $key => $post ) {
792
			if ( $this->is_markdown( $post->ID ) && ! empty( $posts[ $key ]->post_content_filtered ) ) {
793
				$markdown = $posts[ $key ]->post_content_filtered;
794
				$posts[ $key ]->post_content_filtered = $posts[ $key ]->post_content;
795
				$posts[ $key ]->post_content = $markdown;
796
			}
797
		}
798
		return $posts;
799
	}
800
801
	/**
802
	 * Singleton silence is golden
803
	 */
804
	private function __construct() {}
805
}
806
807
add_action( 'init', array( WPCom_Markdown::get_instance(), 'load' ) );
808