WPCom_Markdown

Copyright (c) Automattic.

Defined (1)

The class is defined in the following location(s).

/modules/markdown/easy-markdown.php  
  1. class WPCom_Markdown { 
  2.  
  3.  
  4. const POST_OPTION = 'wpcom_publish_posts_with_markdown'; 
  5. const COMMENT_OPTION = 'wpcom_publish_comments_with_markdown'; 
  6. const POST_TYPE_SUPPORT = 'wpcom-markdown'; 
  7. const IS_MD_META = '_wpcom_is_markdown'; 
  8.  
  9. private static $parser; 
  10. private static $instance; 
  11.  
  12. // to ensure that our munged posts over xml-rpc are removed from the cache 
  13. public $posts_to_uncache = array(); 
  14. private $monitoring = array( 'post' => array(), 'parent' => array() ); 
  15.  
  16.  
  17. /** 
  18. * Yay singletons! 
  19. * @return object WPCom_Markdown instance 
  20. */ 
  21. public static function get_instance() { 
  22. if ( ! self::$instance ) 
  23. self::$instance = new self(); 
  24. return self::$instance; 
  25.  
  26. /** 
  27. * Kicks things off on `init` action 
  28. * @return null 
  29. */ 
  30. public function load() { 
  31. $this->add_default_post_type_support(); 
  32. $this->maybe_load_actions_and_filters(); 
  33. if ( defined( 'REST_API_REQUEST' ) && REST_API_REQUEST ) { 
  34. add_action( 'switch_blog', array( $this, 'maybe_load_actions_and_filters' ), 10, 2 ); 
  35. add_action( 'admin_init', array( $this, 'register_setting' ) ); 
  36. add_action( 'admin_init', array( $this, 'maybe_unload_for_bulk_edit' ) ); 
  37. if ( current_theme_supports( 'o2' ) || class_exists( 'P2' ) ) { 
  38. $this->add_o2_helpers(); 
  39.  
  40. /** 
  41. * If we're in a bulk edit session, unload so that we don't lose our markdown metadata 
  42. * @return null 
  43. */ 
  44. public function maybe_unload_for_bulk_edit() { 
  45. if ( isset( $_REQUEST['bulk_edit'] ) && $this->is_posting_enabled() ) { 
  46. $this->unload_markdown_for_posts(); 
  47.  
  48. /** 
  49. * Called on init and fires on switch_blog to decide if our actions and filters 
  50. * should be running. 
  51. * @param int|null $new_blog_id New blog ID 
  52. * @param int|null $old_blog_id Old blog ID 
  53. * @return null 
  54. */ 
  55. public function maybe_load_actions_and_filters( $new_blog_id = null, $old_blog_id = null ) { 
  56. // If this is a switch_to_blog call, and the blog isn't changing, we'll already be loaded 
  57. if ( $new_blog_id && $new_blog_id === $old_blog_id ) { 
  58. return; 
  59.  
  60. if ( $this->is_posting_enabled() ) { 
  61. $this->load_markdown_for_posts(); 
  62. } else { 
  63. $this->unload_markdown_for_posts(); 
  64.  
  65. if ( $this->is_commenting_enabled() ) { 
  66. $this->load_markdown_for_comments(); 
  67. } else { 
  68. $this->unload_markdown_for_comments(); 
  69.  
  70. /** 
  71. * Set up hooks for enabling Markdown conversion on posts 
  72. * @return null 
  73. */ 
  74. public function load_markdown_for_posts() { 
  75. add_action( 'wp_insert_post', array( $this, 'wp_insert_post' ) ); 
  76. add_filter( 'wp_insert_post_data', array( $this, 'wp_insert_post_data' ), 10, 2 ); 
  77. add_filter( 'edit_post_content', array( $this, 'edit_post_content' ), 10, 2 ); 
  78. add_filter( 'edit_post_content_filtered', array( $this, 'edit_post_content_filtered' ), 10, 2 ); 
  79. add_action( 'wp_restore_post_revision', array( $this, 'wp_restore_post_revision' ), 10, 2 ); 
  80. add_filter( '_wp_post_revision_fields', array( $this, '_wp_post_revision_fields' ) ); 
  81. add_action( 'xmlrpc_call', array( $this, 'xmlrpc_actions' ) ); 
  82. add_filter( 'content_save_pre', array( $this, 'preserve_code_blocks' ), 1 ); 
  83. if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) { 
  84. $this->check_for_early_methods(); 
  85.  
  86. /** 
  87. * Removes hooks to disable Markdown conversion on posts 
  88. * @return null 
  89. */ 
  90. public function unload_markdown_for_posts() { 
  91. remove_action( 'wp_insert_post', array( $this, 'wp_insert_post' ) ); 
  92. remove_filter( 'wp_insert_post_data', array( $this, 'wp_insert_post_data' ), 10, 2 ); 
  93. remove_filter( 'edit_post_content', array( $this, 'edit_post_content' ), 10, 2 ); 
  94. remove_filter( 'edit_post_content_filtered', array( $this, 'edit_post_content_filtered' ), 10, 2 ); 
  95. remove_action( 'wp_restore_post_revision', array( $this, 'wp_restore_post_revision' ), 10, 2 ); 
  96. remove_filter( '_wp_post_revision_fields', array( $this, '_wp_post_revision_fields' ) ); 
  97. remove_action( 'xmlrpc_call', array( $this, 'xmlrpc_actions' ) ); 
  98. remove_filter( 'content_save_pre', array( $this, 'preserve_code_blocks' ), 1 ); 
  99.  
  100. /** 
  101. * Set up hooks for enabling Markdown conversion on comments 
  102. * @return null 
  103. */ 
  104. protected function load_markdown_for_comments() { 
  105. // Use priority 9 so that Markdown runs before KSES, which can clean up 
  106. // any munged HTML. 
  107. add_filter( 'pre_comment_content', array( $this, 'pre_comment_content' ), 9 ); 
  108.  
  109. /** 
  110. * Removes hooks to disable Markdown conversion 
  111. * @return null 
  112. */ 
  113. protected function unload_markdown_for_comments() { 
  114. remove_filter( 'pre_comment_content', array( $this, 'pre_comment_content' ), 9 ); 
  115.  
  116. /** 
  117. * o2 does some of what we do. Let's take precedence. 
  118. * @return null 
  119. */ 
  120. public function add_o2_helpers() { 
  121. if ( $this->is_posting_enabled() ) { 
  122. add_filter( 'content_save_pre', array( $this, 'o2_escape_lists' ), 1 ); 
  123.  
  124. add_filter( 'o2_preview_post', array( $this, 'o2_preview_post' ) ); 
  125. add_filter( 'o2_preview_comment', array( $this, 'o2_preview_comment' ) ); 
  126.  
  127. add_filter( 'wpcom_markdown_transform_pre', array( $this, 'o2_unescape_lists' ) ); 
  128. add_filter( 'wpcom_untransformed_content', array( $this, 'o2_unescape_lists' ) ); 
  129.  
  130. /** 
  131. * If Markdown is enabled for posts on this blog, filter the text for o2 previews 
  132. * @param string $text Post text 
  133. * @return string Post text transformed through the magic of Markdown 
  134. */ 
  135. public function o2_preview_post( $text ) { 
  136. if ( $this->is_posting_enabled() ) { 
  137. $text = $this->transform( $text, array( 'unslash' => false ) ); 
  138. return $text; 
  139.  
  140. /** 
  141. * If Markdown is enabled for comments on this blog, filter the text for o2 previews 
  142. * @param string $text Comment text 
  143. * @return string Comment text transformed through the magic of Markdown 
  144. */ 
  145. public function o2_preview_comment( $text ) { 
  146. if ( $this->is_commenting_enabled() ) { 
  147. $text = $this->transform( $text, array( 'unslash' => false ) ); 
  148. return $text; 
  149.  
  150. /** 
  151. * Escapes lists so that o2 doesn't trounce them 
  152. * @param string $text Post/comment text 
  153. * @return string Text escaped with HTML entity for asterisk 
  154. */ 
  155. public function o2_escape_lists( $text ) { 
  156. return preg_replace( '/^\\* /um', '* ', $text ); 
  157.  
  158. /** 
  159. * Unescapes the token we inserted on o2_escape_lists 
  160. * @param string $text Post/comment text with HTML entities for asterisks 
  161. * @return string Text with the HTML entity removed 
  162. */ 
  163. public function o2_unescape_lists( $text ) { 
  164. return preg_replace( '/^[&]\#042; /um', '* ', $text ); 
  165.  
  166. /** 
  167. * Preserve code blocks from being munged by KSES before they have a chance 
  168. * @param string $text post content 
  169. * @return string post content with code blocks escaped 
  170. */ 
  171. public function preserve_code_blocks( $text ) { 
  172. return $this->get_parser()->codeblock_preserve( $text ); 
  173.  
  174. /** 
  175. * Remove KSES if it's there. Store the result to manually invoke later if needed. 
  176. * @return null 
  177. */ 
  178. public function maybe_remove_kses() { 
  179. // Filters return true if they existed before you removed them 
  180. if ( $this->is_posting_enabled() ) 
  181. $this->kses = remove_filter( 'content_filtered_save_pre', 'wp_filter_post_kses' ) && remove_filter( 'content_save_pre', 'wp_filter_post_kses' ); 
  182.  
  183. /** 
  184. * Add our Writing and Discussion settings. 
  185. * @return null 
  186. */ 
  187. public function register_setting() { 
  188. add_settings_field( self::POST_OPTION, __( 'Markdown', 'jetpack' ), array( $this, 'post_field' ), 'writing' ); 
  189. register_setting( 'writing', self::POST_OPTION, array( $this, 'sanitize_setting') ); 
  190. add_settings_field( self::COMMENT_OPTION, __( 'Markdown', 'jetpack' ), array( $this, 'comment_field' ), 'discussion' ); 
  191. register_setting( 'discussion', self::COMMENT_OPTION, array( $this, 'sanitize_setting') ); 
  192.  
  193. /** 
  194. * Sanitize setting. Don't really want to store "on" value, so we'll store "1" instead! 
  195. * @param string $input Value received by settings API via $_POST 
  196. * @return bool Cast to boolean. 
  197. */ 
  198. public function sanitize_setting( $input ) { 
  199. return (bool) $input; 
  200.  
  201. /** 
  202. * Prints HTML for the Writing setting 
  203. * @return null 
  204. */ 
  205. public function post_field() { 
  206. printf( 
  207. '<label><input name="%s" id="%s" type="checkbox"%s /> %s</label><p class="description">%s</p>',  
  208. self::POST_OPTION,  
  209. self::POST_OPTION,  
  210. checked( $this->is_posting_enabled(), true, false ),  
  211. esc_html__( 'Use Markdown for posts and pages.', 'jetpack' ),  
  212. sprintf( '<a href="%s">%s</a>', esc_url( $this->get_support_url() ), esc_html__( 'Learn more about Markdown.', 'jetpack' ) ) 
  213. ); 
  214.  
  215. /** 
  216. * Prints HTML for the Discussion setting 
  217. * @return null 
  218. */ 
  219. public function comment_field() { 
  220. printf( 
  221. '<label><input name="%s" id="%s" type="checkbox"%s /> %s</label><p class="description">%s</p>',  
  222. self::COMMENT_OPTION,  
  223. self::COMMENT_OPTION,  
  224. checked( $this->is_commenting_enabled(), true, false ),  
  225. esc_html__( 'Use Markdown for comments.', 'jetpack' ),  
  226. sprintf( '<a href="%s">%s</a>', esc_url( $this->get_support_url() ), esc_html__( 'Learn more about Markdown.', 'jetpack' ) ) 
  227. ); 
  228.  
  229. /** 
  230. * Get the support url for Markdown 
  231. * @uses apply_filters 
  232. * @return string support url 
  233. */ 
  234. protected function get_support_url() { 
  235. /** 
  236. * Filter the Markdown support URL. 
  237. * @module markdown 
  238. * @since 2.8.0 
  239. * @param string $url Markdown support URL. 
  240. */ 
  241. return apply_filters( 'easy_markdown_support_url', 'http://en.support.wordpress.com/markdown-quick-reference/' ); 
  242.  
  243. /** 
  244. * Is Mardown conversion for posts enabled? 
  245. * @return boolean 
  246. */ 
  247. public function is_posting_enabled() { 
  248. return (bool) get_option( self::POST_OPTION, '' ); 
  249.  
  250. /** 
  251. * Is Markdown conversion for comments enabled? 
  252. * @return boolean 
  253. */ 
  254. public function is_commenting_enabled() { 
  255. return (bool) get_option( self::COMMENT_OPTION, '' ); 
  256.  
  257. /** 
  258. * Check if a $post_id has Markdown enabled 
  259. * @param int $post_id A post ID. 
  260. * @return boolean 
  261. */ 
  262. public function is_markdown( $post_id ) { 
  263. return get_metadata( 'post', $post_id, self::IS_MD_META, true ); 
  264.  
  265. /** 
  266. * Set Markdown as enabled on a post_id. We skip over update_postmeta so we 
  267. * can sneakily set metadata on post revisions, which we need. 
  268. * @param int $post_id A post ID. 
  269. * @return bool The metadata was successfully set. 
  270. */ 
  271. protected function set_as_markdown( $post_id ) { 
  272. return update_metadata( 'post', $post_id, self::IS_MD_META, true ); 
  273.  
  274. /** 
  275. * Get our Markdown parser object, optionally requiring all of our needed classes and 
  276. * instantiating our parser. 
  277. * @return object WPCom_GHF_Markdown_Parser instance. 
  278. */ 
  279. public function get_parser() { 
  280.  
  281. if ( ! self::$parser ) { 
  282. jetpack_require_lib( 'markdown' ); 
  283. self::$parser = new WPCom_GHF_Markdown_Parser; 
  284.  
  285. return self::$parser; 
  286.  
  287. /** 
  288. * We don't want Markdown conversion all over the place. 
  289. * @return null 
  290. */ 
  291. public function add_default_post_type_support() { 
  292. add_post_type_support( 'post', self::POST_TYPE_SUPPORT ); 
  293. add_post_type_support( 'page', self::POST_TYPE_SUPPORT ); 
  294. add_post_type_support( 'revision', self::POST_TYPE_SUPPORT ); 
  295.  
  296. /** 
  297. * Figure out the post type of the post screen we're on 
  298. * @return string Current post_type 
  299. */ 
  300. protected function get_post_screen_post_type() { 
  301. global $pagenow; 
  302. if ( 'post-new.php' === $pagenow ) 
  303. return ( isset( $_GET['post_type'] ) ) ? $_GET['post_type'] : 'post'; 
  304. if ( isset( $_GET['post'] ) ) { 
  305. $post = get_post( (int) $_GET['post'] ); 
  306. if ( is_object( $post ) && isset( $post->post_type ) ) 
  307. return $post->post_type; 
  308. return 'post'; 
  309.  
  310. /** 
  311. * Swap post_content and post_content_filtered for editing 
  312. * @param string $content Post content 
  313. * @param int $id post ID 
  314. * @return string Swapped content 
  315. */ 
  316. public function edit_post_content( $content, $id ) { 
  317. if ( $this->is_markdown( $id ) ) { 
  318. $post = get_post( $id ); 
  319. if ( $post && ! empty( $post->post_content_filtered ) ) { 
  320. $post = $this->swap_for_editing( $post ); 
  321. return $post->post_content; 
  322. return $content; 
  323.  
  324. /** 
  325. * Swap post_content_filtered and post_content for editing 
  326. * @param string $content Post content_filtered 
  327. * @param int $id post ID 
  328. * @return string Swapped content 
  329. */ 
  330. public function edit_post_content_filtered( $content, $id ) { 
  331. // if markdown was disabled, let's turn this off 
  332. if ( ! $this->is_posting_enabled() && $this->is_markdown( $id ) ) { 
  333. $post = get_post( $id ); 
  334. if ( $post && ! empty( $post->post_content_filtered ) ) 
  335. $content = ''; 
  336. return $content; 
  337.  
  338. /** 
  339. * Magic happens here. Markdown is converted and stored on post_content. Original Markdown is stored 
  340. * in post_content_filtered so that we can continue editing as Markdown. 
  341. * @param array $post_data The post data that will be inserted into the DB. Slashed. 
  342. * @param array $postarr All the stuff that was in $_POST. 
  343. * @return array $post_data with post_content and post_content_filtered modified 
  344. */ 
  345. public function wp_insert_post_data( $post_data, $postarr ) { 
  346. // $post_data array is slashed! 
  347. $post_id = isset( $postarr['ID'] ) ? $postarr['ID'] : false; 
  348. // bail early if markdown is disabled or this post type is unsupported. 
  349. if ( ! $this->is_posting_enabled() || ! post_type_supports( $post_data['post_type'], self::POST_TYPE_SUPPORT ) ) { 
  350. // it's disabled, but maybe this *was* a markdown post before. 
  351. if ( $this->is_markdown( $post_id ) && ! empty( $post_data['post_content_filtered'] ) ) { 
  352. $post_data['post_content_filtered'] = ''; 
  353. // we have no context to determine supported post types in the `post_content_pre` hook,  
  354. // which already ran to sanitize code blocks. Undo that. 
  355. $post_data['post_content'] = $this->get_parser()->codeblock_restore( $post_data['post_content'] ); 
  356. return $post_data; 
  357. // rejigger post_content and post_content_filtered 
  358. // revisions are already in the right place, except when we're restoring, but that's taken care of elsewhere 
  359. if ( 'revision' !== $post_data['post_type'] ) { 
  360. /** 
  361. * Filter the original post content passed to Markdown. 
  362. * @module markdown 
  363. * @since 2.8.0 
  364. * @param string $post_data['post_content'] Untransformed post content. 
  365. */ 
  366. $post_data['post_content_filtered'] = apply_filters( 'wpcom_untransformed_content', $post_data['post_content'] ); 
  367. $post_data['post_content'] = $this->transform( $post_data['post_content'], array( 'id' => $post_id ) ); 
  368. /** This filter is already documented in core/wp-includes/default-filters.php */ 
  369. $post_data['post_content'] = apply_filters( 'content_save_pre', $post_data['post_content'] ); 
  370. } elseif ( 0 === strpos( $post_data['post_name'], $post_data['post_parent'] . '-autosave' ) ) { 
  371. // autosaves for previews are weird 
  372. $post_data['post_content'] = $this->transform( $post_data['post_content'], array( 'id' => $post_data['post_parent'] ) ); 
  373. /** This filter is already documented in core/wp-includes/default-filters.php */ 
  374. $post_data['post_content'] = apply_filters( 'content_save_pre', $post_data['post_content'] ); 
  375.  
  376. // set as markdown on the wp_insert_post hook later 
  377. if ( $post_id ) 
  378. $this->monitoring['post'][ $post_id ] = true; 
  379. else 
  380. $this->monitoring['content'] = wp_unslash( $post_data['post_content'] ); 
  381. if ( 'revision' === $postarr['post_type'] && $this->is_markdown( $postarr['post_parent'] ) ) 
  382. $this->monitoring['parent'][ $postarr['post_parent'] ] = true; 
  383.  
  384. return $post_data; 
  385.  
  386. /** 
  387. * Calls on wp_insert_post action, after wp_insert_post_data. This way we can 
  388. * still set postmeta on our revisions after it's all been deleted. 
  389. * @param int $post_id The post ID that has just been added/updated 
  390. * @return null 
  391. */ 
  392. public function wp_insert_post( $post_id ) { 
  393. $post_parent = get_post_field( 'post_parent', $post_id ); 
  394. // this didn't have an ID yet. Compare the content that was just saved. 
  395. if ( isset( $this->monitoring['content'] ) && $this->monitoring['content'] === get_post_field( 'post_content', $post_id ) ) { 
  396. unset( $this->monitoring['content'] ); 
  397. $this->set_as_markdown( $post_id ); 
  398. if ( isset( $this->monitoring['post'][$post_id] ) ) { 
  399. unset( $this->monitoring['post'][$post_id] ); 
  400. $this->set_as_markdown( $post_id ); 
  401. } elseif ( isset( $this->monitoring['parent'][$post_parent] ) ) { 
  402. unset( $this->monitoring['parent'][$post_parent] ); 
  403. $this->set_as_markdown( $post_id ); 
  404.  
  405. /** 
  406. * Run a comment through Markdown. Easy peasy. 
  407. * @param string $content 
  408. * @return string 
  409. */ 
  410. public function pre_comment_content( $content ) { 
  411. return $this->transform( $content, array( 
  412. 'id' => $this->comment_hash( $content ),  
  413. ) ); 
  414.  
  415. protected function comment_hash( $content ) { 
  416. return 'c-' . substr( md5( $content ), 0, 8 ); 
  417.  
  418. /** 
  419. * Markdown conversion. Some DRYness for repetitive tasks. 
  420. * @param string $text Content to be run through Markdown 
  421. * @param array $args Arguments, with keys: 
  422. * id: provide a string to prefix footnotes with a unique identifier 
  423. * unslash: when true, expects and returns slashed data 
  424. * decode_code_blocks: when true, assume that text in fenced code blocks is already 
  425. * HTML encoded and should be decoded before being passed to Markdown, which does 
  426. * its own encoding. 
  427. * @return string Markdown-processed content 
  428. */ 
  429. public function transform( $text, $args = array() ) { 
  430. $args = wp_parse_args( $args, array( 
  431. 'id' => false,  
  432. 'unslash' => true,  
  433. 'decode_code_blocks' => ! $this->get_parser()->use_code_shortcode 
  434. ) ); 
  435. // probably need to unslash 
  436. if ( $args['unslash'] ) 
  437. $text = wp_unslash( $text ); 
  438.  
  439. /** 
  440. * Filter the content to be run through Markdown, before it's transformed by Markdown. 
  441. * @module markdown 
  442. * @since 2.8.0 
  443. * @param string $text Content to be run through Markdown 
  444. * @param array $args Array of Markdown options. 
  445. */ 
  446. $text = apply_filters( 'wpcom_markdown_transform_pre', $text, $args ); 
  447. // ensure our paragraphs are separated 
  448. $text = str_replace( array( '</p><p>', "</p>\n<p>" ), "</p>\n\n<p>", $text ); 
  449. // visual editor likes to add <p>s. Buh-bye. 
  450. $text = $this->get_parser()->unp( $text ); 
  451. // sometimes we get an encoded > at start of line, breaking blockquotes 
  452. $text = preg_replace( '/^>/m', '>', $text ); 
  453. // prefixes are because we need to namespace footnotes by post_id 
  454. $this->get_parser()->fn_id_prefix = $args['id'] ? $args['id'] . '-' : ''; 
  455. // If we're not using the code shortcode, prevent over-encoding. 
  456. if ( $args['decode_code_blocks'] ) { 
  457. $text = $this->get_parser()->codeblock_restore( $text ); 
  458. // Transform it! 
  459. $text = $this->get_parser()->transform( $text ); 
  460. // Fix footnotes - kses doesn't like the : IDs it supplies 
  461. $text = preg_replace( '/((id|href)="#?fn(ref)?):/', "$1-", $text ); 
  462. // Markdown inserts extra spaces to make itself work. Buh-bye. 
  463. $text = rtrim( $text ); 
  464. /** 
  465. * Filter the content to be run through Markdown, after it was transformed by Markdown. 
  466. * @module markdown 
  467. * @since 2.8.0 
  468. * @param string $text Content to be run through Markdown 
  469. * @param array $args Array of Markdown options. 
  470. */ 
  471. $text = apply_filters( 'wpcom_markdown_transform_post', $text, $args ); 
  472.  
  473. // probably need to re-slash 
  474. if ( $args['unslash'] ) 
  475. $text = wp_slash( $text ); 
  476.  
  477. return $text; 
  478.  
  479. /** 
  480. * Shows Markdown in the Revisions screen, and ensures that post_content_filtered 
  481. * is maintained on revisions 
  482. * @param array $fields Post fields pertinent to revisions 
  483. * @return array Modified array to include post_content_filtered 
  484. */ 
  485. public function _wp_post_revision_fields( $fields ) { 
  486. $fields['post_content_filtered'] = __( 'Markdown content', 'jetpack' ); 
  487. return $fields; 
  488.  
  489. /** 
  490. * Do some song and dance to keep all post_content and post_content_filtered content 
  491. * in the expected place when a post revision is restored. 
  492. * @param int $post_id The post ID have a restore done to it 
  493. * @param int $revision_id The revision ID being restored 
  494. * @return null 
  495. */ 
  496. public function wp_restore_post_revision( $post_id, $revision_id ) { 
  497. if ( $this->is_markdown( $revision_id ) ) { 
  498. $revision = get_post( $revision_id, ARRAY_A ); 
  499. $post = get_post( $post_id, ARRAY_A ); 
  500. $post['post_content'] = $revision['post_content_filtered']; // Yes, we put it in post_content, because our wp_insert_post_data() expects that 
  501. // set this flag so we can restore the post_content_filtered on the last revision later 
  502. $this->monitoring['restore'] = true; 
  503. // let's not make a revision of our fixing update 
  504. add_filter( 'wp_revisions_to_keep', '__return_false', 99 ); 
  505. wp_update_post( $post ); 
  506. $this->fix_latest_revision_on_restore( $post_id ); 
  507. remove_filter( 'wp_revisions_to_keep', '__return_false', 99 ); 
  508.  
  509. /** 
  510. * We need to ensure the last revision has Markdown, not HTML in its post_content_filtered 
  511. * column after a restore. 
  512. * @param int $post_id The post ID that was just restored. 
  513. * @return null 
  514. */ 
  515. protected function fix_latest_revision_on_restore( $post_id ) { 
  516. global $wpdb; 
  517. $post = get_post( $post_id ); 
  518. $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 ) ); 
  519. $last_revision->post_content_filtered = $post->post_content_filtered; 
  520. wp_insert_post( (array) $last_revision ); 
  521.  
  522. /** 
  523. * Kicks off magic for an XML-RPC session. We want to keep editing Markdown 
  524. * and publishing HTML. 
  525. * @param string $xmlrpc_method The current XML-RPC method 
  526. * @return null 
  527. */ 
  528. public function xmlrpc_actions( $xmlrpc_method ) { 
  529. switch ( $xmlrpc_method ) { 
  530. case 'metaWeblog.getRecentPosts': 
  531. case 'wp.getPosts': 
  532. case 'wp.getPages': 
  533. add_action( 'parse_query', array( $this, 'make_filterable' ), 10, 1 ); 
  534. break; 
  535. case 'wp.getPost': 
  536. $this->prime_post_cache(); 
  537. break; 
  538.  
  539. /** 
  540. * metaWeblog.getPost and wp.getPage fire xmlrpc_call action *after* get_post() is called. 
  541. * So, we have to detect those methods and prime the post cache early. 
  542. * @return null 
  543. */ 
  544. protected function check_for_early_methods() { 
  545. global $HTTP_RAW_POST_DATA; 
  546. if ( false === strpos( $HTTP_RAW_POST_DATA, 'metaWeblog.getPost' ) 
  547. && false === strpos( $HTTP_RAW_POST_DATA, 'wp.getPage' ) ) { 
  548. return; 
  549. include_once( ABSPATH . WPINC . '/class-IXR.php' ); 
  550. $message = new IXR_Message( $HTTP_RAW_POST_DATA ); 
  551. $message->parse(); 
  552. $post_id_position = 'metaWeblog.getPost' === $message->methodName ? 0 : 1; 
  553. $this->prime_post_cache( $message->params[ $post_id_position ] ); 
  554.  
  555. /** 
  556. * Prime the post cache with swapped post_content. This is a sneaky way of getting around 
  557. * the fact that there are no good hooks to call on the *.getPost xmlrpc methods. 
  558. * @return null 
  559. */ 
  560. private function prime_post_cache( $post_id = false ) { 
  561. global $wp_xmlrpc_server; 
  562. if ( ! $post_id ) { 
  563. $post_id = $wp_xmlrpc_server->message->params[3]; 
  564.  
  565. // prime the post cache 
  566. if ( $this->is_markdown( $post_id ) ) { 
  567. $post = get_post( $post_id ); 
  568. if ( ! empty( $post->post_content_filtered ) ) { 
  569. wp_cache_delete( $post->ID, 'posts' ); 
  570. $post = $this->swap_for_editing( $post ); 
  571. wp_cache_add( $post->ID, $post, 'posts' ); 
  572. $this->posts_to_uncache[] = $post_id; 
  573. // uncache munged posts if using a persistent object cache 
  574. if ( wp_using_ext_object_cache() ) { 
  575. add_action( 'shutdown', array( $this, 'uncache_munged_posts' ) ); 
  576.  
  577. /** 
  578. * Swaps `post_content_filtered` back to `post_content` for editing purposes. 
  579. * @param object $post WP_Post object 
  580. * @return object WP_Post object with swapped `post_content_filtered` and `post_content` 
  581. */ 
  582. protected function swap_for_editing( $post ) { 
  583. $markdown = $post->post_content_filtered; 
  584. // unencode encoded code blocks 
  585. $markdown = $this->get_parser()->codeblock_restore( $markdown ); 
  586. // restore beginning of line blockquotes 
  587. $markdown = preg_replace( '/^> /m', '> ', $markdown ); 
  588. $post->post_content_filtered = $post->post_content; 
  589. $post->post_content = $markdown; 
  590. return $post; 
  591.  
  592.  
  593. /** 
  594. * We munge the post cache to serve proper markdown content to XML-RPC clients. 
  595. * Uncache these after the XML-RPC session ends. 
  596. * @return null 
  597. */ 
  598. public function uncache_munged_posts() { 
  599. // $this context gets lost in testing sometimes. Weird. 
  600. foreach( WPCom_Markdown::get_instance()->posts_to_uncache as $post_id ) { 
  601. wp_cache_delete( $post_id, 'posts' ); 
  602.  
  603. /** 
  604. * Since *.(get)?[Rr]ecentPosts calls get_posts with suppress filters on, we need to 
  605. * turn them back on so that we can swap things for editing. 
  606. * @param object $wp_query WP_Query object 
  607. * @return null 
  608. */ 
  609. public function make_filterable( $wp_query ) { 
  610. $wp_query->set( 'suppress_filters', false ); 
  611. add_action( 'the_posts', array( $this, 'the_posts' ), 10, 2 ); 
  612.  
  613. /** 
  614. * Swaps post_content and post_content_filtered for editing. 
  615. * @param array $posts Posts returned by the just-completed query 
  616. * @param object $wp_query Current WP_Query object 
  617. * @return array Modified $posts 
  618. */ 
  619. public function the_posts( $posts, $wp_query ) { 
  620. foreach ( $posts as $key => $post ) { 
  621. if ( $this->is_markdown( $post->ID ) && ! empty( $posts[ $key ]->post_content_filtered ) ) { 
  622. $markdown = $posts[ $key ]->post_content_filtered; 
  623. $posts[ $key ]->post_content_filtered = $posts[ $key ]->post_content; 
  624. $posts[ $key ]->post_content = $markdown; 
  625. return $posts; 
  626.  
  627. /** 
  628. * Singleton silence is golden 
  629. */ 
  630. private function __construct() {}