/modules/markdown/easy-markdown.php

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