Completed
Push — update/remove-omni-search-test... ( 6db49e...ed8043 )
by
unknown
30:51 queued 20:39
created

WPCom_Markdown::after_wp_tiny_mce()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 0
dl 0
loc 15
rs 9.4285
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
tinymce.on( 'AddEditor', function( event ) {
454
	event.editor.on( 'BeforeSetContent', function( event ) {
455
		var editor = event.target;
456
		Object.keys( editor.schema.elements ).forEach( function( key, index ) {
457
			editor.schema.elements[ key ].attributes['markdown'] = {};
458
			editor.schema.elements[ key ].attributesOrder.push( 'markdown' );
459
		} );
460
	} );
461
}, true );
462
</script>
463
<?php
464
	}
465
466
	/**
467
	 * Magic happens here. Markdown is converted and stored on post_content. Original Markdown is stored
468
	 * in post_content_filtered so that we can continue editing as Markdown.
469
	 * @param  array $post_data  The post data that will be inserted into the DB. Slashed.
470
	 * @param  array $postarr    All the stuff that was in $_POST.
471
	 * @return array             $post_data with post_content and post_content_filtered modified
472
	 */
473
	public function wp_insert_post_data( $post_data, $postarr ) {
474
		// $post_data array is slashed!
475
		$post_id = isset( $postarr['ID'] ) ? $postarr['ID'] : false;
476
		// bail early if markdown is disabled or this post type is unsupported.
477
		if ( ! $this->is_posting_enabled() || ! post_type_supports( $post_data['post_type'], self::POST_TYPE_SUPPORT ) ) {
478
			// it's disabled, but maybe this *was* a markdown post before.
479
			if ( $this->is_markdown( $post_id ) && ! empty( $post_data['post_content_filtered'] ) ) {
480
				$post_data['post_content_filtered'] = '';
481
			}
482
			// we have no context to determine supported post types in the `post_content_pre` hook,
483
			// which already ran to sanitize code blocks. Undo that.
484
			$post_data['post_content'] = $this->get_parser()->codeblock_restore( $post_data['post_content'] );
485
			return $post_data;
486
		}
487
		// rejigger post_content and post_content_filtered
488
		// revisions are already in the right place, except when we're restoring, but that's taken care of elsewhere
489
		// also prevent quick edit feature from overriding already-saved markdown (issue https://github.com/Automattic/jetpack/issues/636)
490
		if ( 'revision' !== $post_data['post_type'] && ! isset( $_POST['_inline_edit'] ) ) {
491
			/**
492
			 * Filter the original post content passed to Markdown.
493
			 *
494
			 * @module markdown
495
			 *
496
			 * @since 2.8.0
497
			 *
498
			 * @param string $post_data['post_content'] Untransformed post content.
499
			 */
500
			$post_data['post_content_filtered'] = apply_filters( 'wpcom_untransformed_content', $post_data['post_content'] );
501
			$post_data['post_content'] = $this->transform( $post_data['post_content'], array( 'id' => $post_id ) );
502
			/** This filter is already documented in core/wp-includes/default-filters.php */
503
			$post_data['post_content'] = apply_filters( 'content_save_pre', $post_data['post_content'] );
504
		} elseif ( 0 === strpos( $post_data['post_name'], $post_data['post_parent'] . '-autosave' ) ) {
505
			// autosaves for previews are weird
506
			/** This filter is already documented in modules/markdown/easy-markdown.php */
507
			$post_data['post_content_filtered'] = apply_filters( 'wpcom_untransformed_content', $post_data['post_content'] );
508
			$post_data['post_content'] = $this->transform( $post_data['post_content'], array( 'id' => $post_data['post_parent'] ) );
509
			/** This filter is already documented in core/wp-includes/default-filters.php */
510
			$post_data['post_content'] = apply_filters( 'content_save_pre', $post_data['post_content'] );
511
		}
512
513
		// set as markdown on the wp_insert_post hook later
514
		if ( $post_id )
515
			$this->monitoring['post'][ $post_id ] = true;
516
		else
517
			$this->monitoring['content'] = wp_unslash( $post_data['post_content'] );
518
		if ( 'revision' === $postarr['post_type'] && $this->is_markdown( $postarr['post_parent'] ) )
519
			$this->monitoring['parent'][ $postarr['post_parent'] ] = true;
520
521
		return $post_data;
522
	}
523
524
	/**
525
	 * Calls on wp_insert_post action, after wp_insert_post_data. This way we can
526
	 * still set postmeta on our revisions after it's all been deleted.
527
	 * @param  int $post_id The post ID that has just been added/updated
528
	 * @return null
529
	 */
530
	public function wp_insert_post( $post_id ) {
531
		$post_parent = get_post_field( 'post_parent', $post_id );
532
		// this didn't have an ID yet. Compare the content that was just saved.
533
		if ( isset( $this->monitoring['content'] ) && $this->monitoring['content'] === get_post_field( 'post_content', $post_id ) ) {
534
			unset( $this->monitoring['content'] );
535
			$this->set_as_markdown( $post_id );
536
		}
537
		if ( isset( $this->monitoring['post'][$post_id] ) ) {
538
			unset( $this->monitoring['post'][$post_id] );
539
			$this->set_as_markdown( $post_id );
540
		} elseif ( isset( $this->monitoring['parent'][$post_parent] ) ) {
541
			unset( $this->monitoring['parent'][$post_parent] );
542
			$this->set_as_markdown( $post_id );
543
		}
544
	}
545
546
	/**
547
	 * Run a comment through Markdown. Easy peasy.
548
	 * @param  string $content
549
	 * @return string
550
	 */
551
	public function pre_comment_content( $content ) {
552
		return $this->transform( $content, array(
553
			'id' => $this->comment_hash( $content ),
554
		) );
555
	}
556
557
	protected function comment_hash( $content ) {
558
		return 'c-' . substr( md5( $content ), 0, 8 );
559
	}
560
561
	/**
562
	 * Markdown conversion. Some DRYness for repetitive tasks.
563
	 * @param  string $text  Content to be run through Markdown
564
	 * @param  array  $args  Arguments, with keys:
565
	 *                       id: provide a string to prefix footnotes with a unique identifier
566
	 *                       unslash: when true, expects and returns slashed data
567
	 *                       decode_code_blocks: when true, assume that text in fenced code blocks is already
568
	 *                         HTML encoded and should be decoded before being passed to Markdown, which does
569
	 *                         its own encoding.
570
	 * @return string        Markdown-processed content
571
	 */
572
	public function transform( $text, $args = array() ) {
573
		$args = wp_parse_args( $args, array(
574
			'id' => false,
575
			'unslash' => true,
576
			'decode_code_blocks' => ! $this->get_parser()->use_code_shortcode
577
		) );
578
		// probably need to unslash
579
		if ( $args['unslash'] )
580
			$text = wp_unslash( $text );
581
582
		/**
583
		 * Filter the content to be run through Markdown, before it's transformed by Markdown.
584
		 *
585
		 * @module markdown
586
		 *
587
		 * @since 2.8.0
588
		 *
589
		 * @param string $text Content to be run through Markdown
590
		 * @param array $args Array of Markdown options.
591
		 */
592
		$text = apply_filters( 'wpcom_markdown_transform_pre', $text, $args );
593
		// ensure our paragraphs are separated
594
		$text = str_replace( array( '</p><p>', "</p>\n<p>" ), "</p>\n\n<p>", $text );
595
		// visual editor likes to add <p>s. Buh-bye.
596
		$text = $this->get_parser()->unp( $text );
597
		// sometimes we get an encoded > at start of line, breaking blockquotes
598
		$text = preg_replace( '/^&gt;/m', '>', $text );
599
		// prefixes are because we need to namespace footnotes by post_id
600
		$this->get_parser()->fn_id_prefix = $args['id'] ? $args['id'] . '-' : '';
601
		// If we're not using the code shortcode, prevent over-encoding.
602
		if ( $args['decode_code_blocks'] ) {
603
			$text = $this->get_parser()->codeblock_restore( $text );
604
		}
605
		// Transform it!
606
		$text = $this->get_parser()->transform( $text );
607
		// Fix footnotes - kses doesn't like the : IDs it supplies
608
		$text = preg_replace( '/((id|href)="#?fn(ref)?):/', "$1-", $text );
609
		// Markdown inserts extra spaces to make itself work. Buh-bye.
610
		$text = rtrim( $text );
611
		/**
612
		 * Filter the content to be run through Markdown, after it was transformed by Markdown.
613
		 *
614
		 * @module markdown
615
		 *
616
		 * @since 2.8.0
617
		 *
618
		 * @param string $text Content to be run through Markdown
619
		 * @param array $args Array of Markdown options.
620
		 */
621
		$text = apply_filters( 'wpcom_markdown_transform_post', $text, $args );
622
623
		// probably need to re-slash
624
		if ( $args['unslash'] )
625
			$text = wp_slash( $text );
626
627
		return $text;
628
	}
629
630
	/**
631
	 * Shows Markdown in the Revisions screen, and ensures that post_content_filtered
632
	 * is maintained on revisions
633
	 * @param  array $fields  Post fields pertinent to revisions
634
	 * @return array          Modified array to include post_content_filtered
635
	 */
636
	public function _wp_post_revision_fields( $fields ) {
637
		$fields['post_content_filtered'] = __( 'Markdown content', 'jetpack' );
638
		return $fields;
639
	}
640
641
	/**
642
	 * Do some song and dance to keep all post_content and post_content_filtered content
643
	 * in the expected place when a post revision is restored.
644
	 * @param  int $post_id        The post ID have a restore done to it
645
	 * @param  int $revision_id    The revision ID being restored
646
	 * @return null
647
	 */
648
	public function wp_restore_post_revision( $post_id, $revision_id ) {
649
		if ( $this->is_markdown( $revision_id ) ) {
650
			$revision = get_post( $revision_id, ARRAY_A );
651
			$post = get_post( $post_id, ARRAY_A );
652
			$post['post_content'] = $revision['post_content_filtered']; // Yes, we put it in post_content, because our wp_insert_post_data() expects that
653
			// set this flag so we can restore the post_content_filtered on the last revision later
654
			$this->monitoring['restore'] = true;
655
			// let's not make a revision of our fixing update
656
			add_filter( 'wp_revisions_to_keep', '__return_false', 99 );
657
			wp_update_post( $post );
658
			$this->fix_latest_revision_on_restore( $post_id );
659
			remove_filter( 'wp_revisions_to_keep', '__return_false', 99 );
660
		}
661
	}
662
663
	/**
664
	 * We need to ensure the last revision has Markdown, not HTML in its post_content_filtered
665
	 * column after a restore.
666
	 * @param  int $post_id The post ID that was just restored.
667
	 * @return null
668
	 */
669
	protected function fix_latest_revision_on_restore( $post_id ) {
670
		global $wpdb;
671
		$post = get_post( $post_id );
672
		$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 ) );
673
		$last_revision->post_content_filtered = $post->post_content_filtered;
674
		wp_insert_post( (array) $last_revision );
675
	}
676
677
	/**
678
	 * Kicks off magic for an XML-RPC session. We want to keep editing Markdown
679
	 * and publishing HTML.
680
	 * @param  string $xmlrpc_method The current XML-RPC method
681
	 * @return null
682
	 */
683
	public function xmlrpc_actions( $xmlrpc_method ) {
684
		switch ( $xmlrpc_method ) {
685
			case 'metaWeblog.getRecentPosts':
686
			case 'wp.getPosts':
687
			case 'wp.getPages':
688
				add_action( 'parse_query', array( $this, 'make_filterable' ), 10, 1 );
689
				break;
690
			case 'wp.getPost':
691
				$this->prime_post_cache();
692
				break;
693
		}
694
	}
695
696
	/**
697
	 * metaWeblog.getPost and wp.getPage fire xmlrpc_call action *after* get_post() is called.
698
	 * So, we have to detect those methods and prime the post cache early.
699
	 * @return null
700
	 */
701
	protected function check_for_early_methods() {
702
		global $HTTP_RAW_POST_DATA;
703
		if ( false === strpos( $HTTP_RAW_POST_DATA, 'metaWeblog.getPost' )
704
			&& false === strpos( $HTTP_RAW_POST_DATA, 'wp.getPage' ) ) {
705
			return;
706
		}
707
		include_once( ABSPATH . WPINC . '/class-IXR.php' );
708
		$message = new IXR_Message( $HTTP_RAW_POST_DATA );
709
		$message->parse();
710
		$post_id_position = 'metaWeblog.getPost' === $message->methodName ?  0 : 1;
711
		$this->prime_post_cache( $message->params[ $post_id_position ] );
712
	}
713
714
	/**
715
	 * Prime the post cache with swapped post_content. This is a sneaky way of getting around
716
	 * the fact that there are no good hooks to call on the *.getPost xmlrpc methods.
717
	 *
718
	 * @return null
719
	 */
720
	private function prime_post_cache( $post_id = false ) {
721
		global $wp_xmlrpc_server;
722
		if ( ! $post_id ) {
723
			$post_id = $wp_xmlrpc_server->message->params[3];
724
		}
725
726
		// prime the post cache
727
		if ( $this->is_markdown( $post_id ) ) {
728
			$post = get_post( $post_id );
729
			if ( ! empty( $post->post_content_filtered ) ) {
730
				wp_cache_delete( $post->ID, 'posts' );
731
				$post = $this->swap_for_editing( $post );
732
				wp_cache_add( $post->ID, $post, 'posts' );
733
				$this->posts_to_uncache[] = $post_id;
734
			}
735
		}
736
		// uncache munged posts if using a persistent object cache
737
		if ( wp_using_ext_object_cache() ) {
738
			add_action( 'shutdown', array( $this, 'uncache_munged_posts' ) );
739
		}
740
	}
741
742
	/**
743
	 * Swaps `post_content_filtered` back to `post_content` for editing purposes.
744
	 * @param  object $post WP_Post object
745
	 * @return object       WP_Post object with swapped `post_content_filtered` and `post_content`
746
	 */
747
	protected function swap_for_editing( $post ) {
748
		$markdown = $post->post_content_filtered;
749
		// unencode encoded code blocks
750
		$markdown = $this->get_parser()->codeblock_restore( $markdown );
751
		// restore beginning of line blockquotes
752
		$markdown = preg_replace( '/^&gt; /m', '> ', $markdown );
753
		$post->post_content_filtered = $post->post_content;
754
		$post->post_content = $markdown;
755
		return $post;
756
	}
757
758
759
	/**
760
	 * We munge the post cache to serve proper markdown content to XML-RPC clients.
761
	 * Uncache these after the XML-RPC session ends.
762
	 * @return null
763
	 */
764
	public function uncache_munged_posts() {
765
		// $this context gets lost in testing sometimes. Weird.
766
		foreach( WPCom_Markdown::get_instance()->posts_to_uncache as $post_id ) {
767
			wp_cache_delete( $post_id, 'posts' );
768
		}
769
	}
770
771
	/**
772
	 * Since *.(get)?[Rr]ecentPosts calls get_posts with suppress filters on, we need to
773
	 * turn them back on so that we can swap things for editing.
774
	 * @param  object $wp_query WP_Query object
775
	 * @return null
776
	 */
777
	public function make_filterable( $wp_query ) {
778
		$wp_query->set( 'suppress_filters', false );
779
		add_action( 'the_posts', array( $this, 'the_posts' ), 10, 2 );
780
	}
781
782
	/**
783
	 * Swaps post_content and post_content_filtered for editing.
784
	 * @param  array  $posts     Posts returned by the just-completed query
785
	 * @param  object $wp_query  Current WP_Query object
786
	 * @return array             Modified $posts
787
	 */
788
	public function the_posts( $posts, $wp_query ) {
789
		foreach ( $posts as $key => $post ) {
790
			if ( $this->is_markdown( $post->ID ) && ! empty( $posts[ $key ]->post_content_filtered ) ) {
791
				$markdown = $posts[ $key ]->post_content_filtered;
792
				$posts[ $key ]->post_content_filtered = $posts[ $key ]->post_content;
793
				$posts[ $key ]->post_content = $markdown;
794
			}
795
		}
796
		return $posts;
797
	}
798
799
	/**
800
	 * Singleton silence is golden
801
	 */
802
	private function __construct() {}
803
}
804
805
add_action( 'init', array( WPCom_Markdown::get_instance(), 'load' ) );
806