easy-markdown.php 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807
  1. <?php
  2. /*
  3. Plugin Name: Easy Markdown
  4. Plugin URI: http://automattic.com/
  5. Description: Write in Markdown, publish in WordPress
  6. Version: 0.1
  7. Author: Matt Wiebe
  8. Author URI: http://automattic.com/
  9. */
  10. /**
  11. * Copyright (c) Automattic. All rights reserved.
  12. *
  13. * Released under the GPL license
  14. * http://www.opensource.org/licenses/gpl-license.php
  15. *
  16. * This is an add-on for WordPress
  17. * https://wordpress.org/
  18. *
  19. * **********************************************************************
  20. * This program is free software; you can redistribute it and/or modify
  21. * it under the terms of the GNU General Public License as published by
  22. * the Free Software Foundation; either version 2 of the License, or
  23. * (at your option) any later version.
  24. *
  25. * This program is distributed in the hope that it will be useful,
  26. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  27. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  28. * GNU General Public License for more details.
  29. * **********************************************************************
  30. */
  31. class WPCom_Markdown {
  32. const POST_OPTION = 'wpcom_publish_posts_with_markdown';
  33. const COMMENT_OPTION = 'wpcom_publish_comments_with_markdown';
  34. const POST_TYPE_SUPPORT = 'wpcom-markdown';
  35. const IS_MD_META = '_wpcom_is_markdown';
  36. private static $parser;
  37. private static $instance;
  38. // to ensure that our munged posts over xml-rpc are removed from the cache
  39. public $posts_to_uncache = array();
  40. private $monitoring = array( 'post' => array(), 'parent' => array() );
  41. /**
  42. * Yay singletons!
  43. * @return object WPCom_Markdown instance
  44. */
  45. public static function get_instance() {
  46. if ( ! self::$instance )
  47. self::$instance = new self();
  48. return self::$instance;
  49. }
  50. /**
  51. * Kicks things off on `init` action
  52. * @return null
  53. */
  54. public function load() {
  55. $this->add_default_post_type_support();
  56. $this->maybe_load_actions_and_filters();
  57. if ( defined( 'REST_API_REQUEST' ) && REST_API_REQUEST ) {
  58. add_action( 'switch_blog', array( $this, 'maybe_load_actions_and_filters' ), 10, 2 );
  59. }
  60. add_action( 'admin_init', array( $this, 'register_setting' ) );
  61. add_action( 'admin_init', array( $this, 'maybe_unload_for_bulk_edit' ) );
  62. if ( current_theme_supports( 'o2' ) || class_exists( 'P2' ) ) {
  63. $this->add_o2_helpers();
  64. }
  65. }
  66. /**
  67. * If we're in a bulk edit session, unload so that we don't lose our markdown metadata
  68. * @return null
  69. */
  70. public function maybe_unload_for_bulk_edit() {
  71. if ( isset( $_REQUEST['bulk_edit'] ) && $this->is_posting_enabled() ) {
  72. $this->unload_markdown_for_posts();
  73. }
  74. }
  75. /**
  76. * Called on init and fires on switch_blog to decide if our actions and filters
  77. * should be running.
  78. * @param int|null $new_blog_id New blog ID
  79. * @param int|null $old_blog_id Old blog ID
  80. * @return null
  81. */
  82. public function maybe_load_actions_and_filters( $new_blog_id = null, $old_blog_id = null ) {
  83. // If this is a switch_to_blog call, and the blog isn't changing, we'll already be loaded
  84. if ( $new_blog_id && $new_blog_id === $old_blog_id ) {
  85. return;
  86. }
  87. if ( $this->is_posting_enabled() ) {
  88. $this->load_markdown_for_posts();
  89. } else {
  90. $this->unload_markdown_for_posts();
  91. }
  92. if ( $this->is_commenting_enabled() ) {
  93. $this->load_markdown_for_comments();
  94. } else {
  95. $this->unload_markdown_for_comments();
  96. }
  97. }
  98. /**
  99. * Set up hooks for enabling Markdown conversion on posts
  100. * @return null
  101. */
  102. public function load_markdown_for_posts() {
  103. add_filter( 'wp_kses_allowed_html', array( $this, 'wp_kses_allowed_html' ), 10, 2 );
  104. add_action( 'after_wp_tiny_mce', array( $this, 'after_wp_tiny_mce' ) );
  105. add_action( 'wp_insert_post', array( $this, 'wp_insert_post' ) );
  106. add_filter( 'wp_insert_post_data', array( $this, 'wp_insert_post_data' ), 10, 2 );
  107. add_filter( 'edit_post_content', array( $this, 'edit_post_content' ), 10, 2 );
  108. add_filter( 'edit_post_content_filtered', array( $this, 'edit_post_content_filtered' ), 10, 2 );
  109. add_action( 'wp_restore_post_revision', array( $this, 'wp_restore_post_revision' ), 10, 2 );
  110. add_filter( '_wp_post_revision_fields', array( $this, '_wp_post_revision_fields' ) );
  111. add_action( 'xmlrpc_call', array( $this, 'xmlrpc_actions' ) );
  112. add_filter( 'content_save_pre', array( $this, 'preserve_code_blocks' ), 1 );
  113. if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
  114. $this->check_for_early_methods();
  115. }
  116. }
  117. /**
  118. * Removes hooks to disable Markdown conversion on posts
  119. * @return null
  120. */
  121. public function unload_markdown_for_posts() {
  122. remove_filter( 'wp_kses_allowed_html', array( $this, 'wp_kses_allowed_html' ) );
  123. remove_action( 'after_wp_tiny_mce', array( $this, 'after_wp_tiny_mce' ) );
  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. add_filter( 'wpcom_markdown_transform_pre', array( $this, 'o2_unescape_lists' ) );
  160. add_filter( 'wpcom_untransformed_content', array( $this, 'o2_unescape_lists' ) );
  161. }
  162. /**
  163. * If Markdown is enabled for posts on this blog, filter the text for o2 previews
  164. * @param string $text Post text
  165. * @return string Post text transformed through the magic of Markdown
  166. */
  167. public function o2_preview_post( $text ) {
  168. if ( $this->is_posting_enabled() ) {
  169. $text = $this->transform( $text, array( 'unslash' => false ) );
  170. }
  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. }
  182. return $text;
  183. }
  184. /**
  185. * Escapes lists so that o2 doesn't trounce them
  186. * @param string $text Post/comment text
  187. * @return string Text escaped with HTML entity for asterisk
  188. */
  189. public function o2_escape_lists( $text ) {
  190. return preg_replace( '/^\\* /um', '&#42; ', $text );
  191. }
  192. /**
  193. * Unescapes the token we inserted on o2_escape_lists
  194. * @param string $text Post/comment text with HTML entities for asterisks
  195. * @return string Text with the HTML entity removed
  196. */
  197. public function o2_unescape_lists( $text ) {
  198. return preg_replace( '/^[&]\#042; /um', '* ', $text );
  199. }
  200. /**
  201. * Preserve code blocks from being munged by KSES before they have a chance
  202. * @param string $text post content
  203. * @return string post content with code blocks escaped
  204. */
  205. public function preserve_code_blocks( $text ) {
  206. return $this->get_parser()->codeblock_preserve( $text );
  207. }
  208. /**
  209. * Remove KSES if it's there. Store the result to manually invoke later if needed.
  210. * @return null
  211. */
  212. public function maybe_remove_kses() {
  213. // Filters return true if they existed before you removed them
  214. if ( $this->is_posting_enabled() )
  215. $this->kses = remove_filter( 'content_filtered_save_pre', 'wp_filter_post_kses' ) && remove_filter( 'content_save_pre', 'wp_filter_post_kses' );
  216. }
  217. /**
  218. * Add our Writing and Discussion settings.
  219. * @return null
  220. */
  221. public function register_setting() {
  222. add_settings_field( self::POST_OPTION, __( 'Markdown', 'jetpack' ), array( $this, 'post_field' ), 'writing' );
  223. register_setting( 'writing', self::POST_OPTION, array( $this, 'sanitize_setting') );
  224. add_settings_field( self::COMMENT_OPTION, __( 'Markdown', 'jetpack' ), array( $this, 'comment_field' ), 'discussion' );
  225. register_setting( 'discussion', self::COMMENT_OPTION, array( $this, 'sanitize_setting') );
  226. }
  227. /**
  228. * Sanitize setting. Don't really want to store "on" value, so we'll store "1" instead!
  229. * @param string $input Value received by settings API via $_POST
  230. * @return bool Cast to boolean.
  231. */
  232. public function sanitize_setting( $input ) {
  233. return (bool) $input;
  234. }
  235. /**
  236. * Prints HTML for the Writing setting
  237. * @return null
  238. */
  239. public function post_field() {
  240. printf(
  241. '<label><input name="%s" id="%s" type="checkbox"%s /> %s</label><p class="description">%s</p>',
  242. self::POST_OPTION,
  243. self::POST_OPTION,
  244. checked( $this->is_posting_enabled(), true, false ),
  245. esc_html__( 'Use Markdown for posts and pages.', 'jetpack' ),
  246. sprintf( '<a href="%s">%s</a>', esc_url( $this->get_support_url() ), esc_html__( 'Learn more about Markdown.', 'jetpack' ) )
  247. );
  248. }
  249. /**
  250. * Prints HTML for the Discussion setting
  251. * @return null
  252. */
  253. public function comment_field() {
  254. printf(
  255. '<label><input name="%s" id="%s" type="checkbox"%s /> %s</label><p class="description">%s</p>',
  256. self::COMMENT_OPTION,
  257. self::COMMENT_OPTION,
  258. checked( $this->is_commenting_enabled(), true, false ),
  259. esc_html__( 'Use Markdown for comments.', 'jetpack' ),
  260. sprintf( '<a href="%s">%s</a>', esc_url( $this->get_support_url() ), esc_html__( 'Learn more about Markdown.', 'jetpack' ) )
  261. );
  262. }
  263. /**
  264. * Get the support url for Markdown
  265. * @uses apply_filters
  266. * @return string support url
  267. */
  268. protected function get_support_url() {
  269. /**
  270. * Filter the Markdown support URL.
  271. *
  272. * @module markdown
  273. *
  274. * @since 2.8.0
  275. *
  276. * @param string $url Markdown support URL.
  277. */
  278. return apply_filters( 'easy_markdown_support_url', 'http://en.support.wordpress.com/markdown-quick-reference/' );
  279. }
  280. /**
  281. * Is Mardown conversion for posts enabled?
  282. * @return boolean
  283. */
  284. public function is_posting_enabled() {
  285. return (bool) Jetpack_Options::get_option_and_ensure_autoload( self::POST_OPTION, '' );
  286. }
  287. /**
  288. * Is Markdown conversion for comments enabled?
  289. * @return boolean
  290. */
  291. public function is_commenting_enabled() {
  292. return (bool) Jetpack_Options::get_option_and_ensure_autoload( self::COMMENT_OPTION, '' );
  293. }
  294. /**
  295. * Check if a $post_id has Markdown enabled
  296. * @param int $post_id A post ID.
  297. * @return boolean
  298. */
  299. public function is_markdown( $post_id ) {
  300. return get_metadata( 'post', $post_id, self::IS_MD_META, true );
  301. }
  302. /**
  303. * Set Markdown as enabled on a post_id. We skip over update_postmeta so we
  304. * can sneakily set metadata on post revisions, which we need.
  305. * @param int $post_id A post ID.
  306. * @return bool The metadata was successfully set.
  307. */
  308. protected function set_as_markdown( $post_id ) {
  309. return update_metadata( 'post', $post_id, self::IS_MD_META, true );
  310. }
  311. /**
  312. * Get our Markdown parser object, optionally requiring all of our needed classes and
  313. * instantiating our parser.
  314. * @return object WPCom_GHF_Markdown_Parser instance.
  315. */
  316. public function get_parser() {
  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. }
  345. return 'post';
  346. }
  347. /**
  348. * Swap post_content and post_content_filtered for editing
  349. * @param string $content Post content
  350. * @param int $id post ID
  351. * @return string Swapped content
  352. */
  353. public function edit_post_content( $content, $id ) {
  354. if ( $this->is_markdown( $id ) ) {
  355. $post = get_post( $id );
  356. if ( $post && ! empty( $post->post_content_filtered ) ) {
  357. $post = $this->swap_for_editing( $post );
  358. return $post->post_content;
  359. }
  360. }
  361. return $content;
  362. }
  363. /**
  364. * Swap post_content_filtered and post_content for editing
  365. * @param string $content Post content_filtered
  366. * @param int $id post ID
  367. * @return string Swapped content
  368. */
  369. public function edit_post_content_filtered( $content, $id ) {
  370. // if markdown was disabled, let's turn this off
  371. if ( ! $this->is_posting_enabled() && $this->is_markdown( $id ) ) {
  372. $post = get_post( $id );
  373. if ( $post && ! empty( $post->post_content_filtered ) )
  374. $content = '';
  375. }
  376. return $content;
  377. }
  378. /**
  379. * Some tags are allowed to have a 'markdown' attribute, allowing them to contain Markdown.
  380. * We need to tell KSES about those tags.
  381. * @param array $tags List of tags that KSES allows.
  382. * @param string $context The context that KSES is allowing these tags.
  383. * @return array The tags that KSES allows, with our extra 'markdown' parameter where necessary.
  384. */
  385. public function wp_kses_allowed_html( $tags, $context ) {
  386. if ( 'post' !== $context ) {
  387. return $tags;
  388. }
  389. $re = '/' . $this->get_parser()->contain_span_tags_re . '/';
  390. foreach ( $tags as $tag => $attributes ) {
  391. if ( preg_match( $re, $tag ) ) {
  392. $attributes['markdown'] = true;
  393. $tags[ $tag ] = $attributes;
  394. }
  395. }
  396. return $tags;
  397. }
  398. /**
  399. * TinyMCE needs to know not to strip the 'markdown' attribute. Unfortunately, it doesn't
  400. * really offer a nice API for whitelisting attributes, so we have to manually add it
  401. * to the schema instead.
  402. */
  403. public function after_wp_tiny_mce() {
  404. ?>
  405. <script type="text/javascript">
  406. jQuery( function() {
  407. ( 'undefined' !== typeof tinymce ) && tinymce.on( 'AddEditor', function( event ) {
  408. event.editor.on( 'BeforeSetContent', function( event ) {
  409. var editor = event.target;
  410. Object.keys( editor.schema.elements ).forEach( function( key, index ) {
  411. editor.schema.elements[ key ].attributes['markdown'] = {};
  412. editor.schema.elements[ key ].attributesOrder.push( 'markdown' );
  413. } );
  414. } );
  415. }, true );
  416. } );
  417. </script>
  418. <?php
  419. }
  420. /**
  421. * Magic happens here. Markdown is converted and stored on post_content. Original Markdown is stored
  422. * in post_content_filtered so that we can continue editing as Markdown.
  423. * @param array $post_data The post data that will be inserted into the DB. Slashed.
  424. * @param array $postarr All the stuff that was in $_POST.
  425. * @return array $post_data with post_content and post_content_filtered modified
  426. */
  427. public function wp_insert_post_data( $post_data, $postarr ) {
  428. // $post_data array is slashed!
  429. $post_id = isset( $postarr['ID'] ) ? $postarr['ID'] : false;
  430. // bail early if markdown is disabled or this post type is unsupported.
  431. if ( ! $this->is_posting_enabled() || ! post_type_supports( $post_data['post_type'], self::POST_TYPE_SUPPORT ) ) {
  432. // it's disabled, but maybe this *was* a markdown post before.
  433. if ( $this->is_markdown( $post_id ) && ! empty( $post_data['post_content_filtered'] ) ) {
  434. $post_data['post_content_filtered'] = '';
  435. }
  436. // we have no context to determine supported post types in the `post_content_pre` hook,
  437. // which already ran to sanitize code blocks. Undo that.
  438. $post_data['post_content'] = $this->get_parser()->codeblock_restore( $post_data['post_content'] );
  439. return $post_data;
  440. }
  441. // rejigger post_content and post_content_filtered
  442. // revisions are already in the right place, except when we're restoring, but that's taken care of elsewhere
  443. // also prevent quick edit feature from overriding already-saved markdown (issue https://github.com/Automattic/jetpack/issues/636)
  444. if ( 'revision' !== $post_data['post_type'] && ! isset( $_POST['_inline_edit'] ) ) {
  445. /**
  446. * Filter the original post content passed to Markdown.
  447. *
  448. * @module markdown
  449. *
  450. * @since 2.8.0
  451. *
  452. * @param string $post_data['post_content'] Untransformed post content.
  453. */
  454. $post_data['post_content_filtered'] = apply_filters( 'wpcom_untransformed_content', $post_data['post_content'] );
  455. $post_data['post_content'] = $this->transform( $post_data['post_content'], array( 'id' => $post_id ) );
  456. /** This filter is already documented in core/wp-includes/default-filters.php */
  457. $post_data['post_content'] = apply_filters( 'content_save_pre', $post_data['post_content'] );
  458. } elseif ( 0 === strpos( $post_data['post_name'], $post_data['post_parent'] . '-autosave' ) ) {
  459. // autosaves for previews are weird
  460. /** This filter is already documented in modules/markdown/easy-markdown.php */
  461. $post_data['post_content_filtered'] = apply_filters( 'wpcom_untransformed_content', $post_data['post_content'] );
  462. $post_data['post_content'] = $this->transform( $post_data['post_content'], array( 'id' => $post_data['post_parent'] ) );
  463. /** This filter is already documented in core/wp-includes/default-filters.php */
  464. $post_data['post_content'] = apply_filters( 'content_save_pre', $post_data['post_content'] );
  465. }
  466. // set as markdown on the wp_insert_post hook later
  467. if ( $post_id )
  468. $this->monitoring['post'][ $post_id ] = true;
  469. else
  470. $this->monitoring['content'] = wp_unslash( $post_data['post_content'] );
  471. if ( 'revision' === $postarr['post_type'] && $this->is_markdown( $postarr['post_parent'] ) )
  472. $this->monitoring['parent'][ $postarr['post_parent'] ] = true;
  473. return $post_data;
  474. }
  475. /**
  476. * Calls on wp_insert_post action, after wp_insert_post_data. This way we can
  477. * still set postmeta on our revisions after it's all been deleted.
  478. * @param int $post_id The post ID that has just been added/updated
  479. * @return null
  480. */
  481. public function wp_insert_post( $post_id ) {
  482. $post_parent = get_post_field( 'post_parent', $post_id );
  483. // this didn't have an ID yet. Compare the content that was just saved.
  484. if ( isset( $this->monitoring['content'] ) && $this->monitoring['content'] === get_post_field( 'post_content', $post_id ) ) {
  485. unset( $this->monitoring['content'] );
  486. $this->set_as_markdown( $post_id );
  487. }
  488. if ( isset( $this->monitoring['post'][$post_id] ) ) {
  489. unset( $this->monitoring['post'][$post_id] );
  490. $this->set_as_markdown( $post_id );
  491. } elseif ( isset( $this->monitoring['parent'][$post_parent] ) ) {
  492. unset( $this->monitoring['parent'][$post_parent] );
  493. $this->set_as_markdown( $post_id );
  494. }
  495. }
  496. /**
  497. * Run a comment through Markdown. Easy peasy.
  498. * @param string $content
  499. * @return string
  500. */
  501. public function pre_comment_content( $content ) {
  502. return $this->transform( $content, array(
  503. 'id' => $this->comment_hash( $content ),
  504. ) );
  505. }
  506. protected function comment_hash( $content ) {
  507. return 'c-' . substr( md5( $content ), 0, 8 );
  508. }
  509. /**
  510. * Markdown conversion. Some DRYness for repetitive tasks.
  511. * @param string $text Content to be run through Markdown
  512. * @param array $args Arguments, with keys:
  513. * id: provide a string to prefix footnotes with a unique identifier
  514. * unslash: when true, expects and returns slashed data
  515. * decode_code_blocks: when true, assume that text in fenced code blocks is already
  516. * HTML encoded and should be decoded before being passed to Markdown, which does
  517. * its own encoding.
  518. * @return string Markdown-processed content
  519. */
  520. public function transform( $text, $args = array() ) {
  521. $args = wp_parse_args( $args, array(
  522. 'id' => false,
  523. 'unslash' => true,
  524. 'decode_code_blocks' => ! $this->get_parser()->use_code_shortcode
  525. ) );
  526. // probably need to unslash
  527. if ( $args['unslash'] )
  528. $text = wp_unslash( $text );
  529. /**
  530. * Filter the content to be run through Markdown, before it's transformed by Markdown.
  531. *
  532. * @module markdown
  533. *
  534. * @since 2.8.0
  535. *
  536. * @param string $text Content to be run through Markdown
  537. * @param array $args Array of Markdown options.
  538. */
  539. $text = apply_filters( 'wpcom_markdown_transform_pre', $text, $args );
  540. // ensure our paragraphs are separated
  541. $text = str_replace( array( '</p><p>', "</p>\n<p>" ), "</p>\n\n<p>", $text );
  542. // visual editor likes to add <p>s. Buh-bye.
  543. $text = $this->get_parser()->unp( $text );
  544. // sometimes we get an encoded > at start of line, breaking blockquotes
  545. $text = preg_replace( '/^&gt;/m', '>', $text );
  546. // prefixes are because we need to namespace footnotes by post_id
  547. $this->get_parser()->fn_id_prefix = $args['id'] ? $args['id'] . '-' : '';
  548. // If we're not using the code shortcode, prevent over-encoding.
  549. if ( $args['decode_code_blocks'] ) {
  550. $text = $this->get_parser()->codeblock_restore( $text );
  551. }
  552. // Transform it!
  553. $text = $this->get_parser()->transform( $text );
  554. // Fix footnotes - kses doesn't like the : IDs it supplies
  555. $text = preg_replace( '/((id|href)="#?fn(ref)?):/', "$1-", $text );
  556. // Markdown inserts extra spaces to make itself work. Buh-bye.
  557. $text = rtrim( $text );
  558. /**
  559. * Filter the content to be run through Markdown, after it was transformed by Markdown.
  560. *
  561. * @module markdown
  562. *
  563. * @since 2.8.0
  564. *
  565. * @param string $text Content to be run through Markdown
  566. * @param array $args Array of Markdown options.
  567. */
  568. $text = apply_filters( 'wpcom_markdown_transform_post', $text, $args );
  569. // probably need to re-slash
  570. if ( $args['unslash'] )
  571. $text = wp_slash( $text );
  572. return $text;
  573. }
  574. /**
  575. * Shows Markdown in the Revisions screen, and ensures that post_content_filtered
  576. * is maintained on revisions
  577. * @param array $fields Post fields pertinent to revisions
  578. * @return array Modified array to include post_content_filtered
  579. */
  580. public function _wp_post_revision_fields( $fields ) {
  581. $fields['post_content_filtered'] = __( 'Markdown content', 'jetpack' );
  582. return $fields;
  583. }
  584. /**
  585. * Do some song and dance to keep all post_content and post_content_filtered content
  586. * in the expected place when a post revision is restored.
  587. * @param int $post_id The post ID have a restore done to it
  588. * @param int $revision_id The revision ID being restored
  589. * @return null
  590. */
  591. public function wp_restore_post_revision( $post_id, $revision_id ) {
  592. if ( $this->is_markdown( $revision_id ) ) {
  593. $revision = get_post( $revision_id, ARRAY_A );
  594. $post = get_post( $post_id, ARRAY_A );
  595. $post['post_content'] = $revision['post_content_filtered']; // Yes, we put it in post_content, because our wp_insert_post_data() expects that
  596. // set this flag so we can restore the post_content_filtered on the last revision later
  597. $this->monitoring['restore'] = true;
  598. // let's not make a revision of our fixing update
  599. add_filter( 'wp_revisions_to_keep', '__return_false', 99 );
  600. wp_update_post( $post );
  601. $this->fix_latest_revision_on_restore( $post_id );
  602. remove_filter( 'wp_revisions_to_keep', '__return_false', 99 );
  603. }
  604. }
  605. /**
  606. * We need to ensure the last revision has Markdown, not HTML in its post_content_filtered
  607. * column after a restore.
  608. * @param int $post_id The post ID that was just restored.
  609. * @return null
  610. */
  611. protected function fix_latest_revision_on_restore( $post_id ) {
  612. global $wpdb;
  613. $post = get_post( $post_id );
  614. $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 ) );
  615. $last_revision->post_content_filtered = $post->post_content_filtered;
  616. wp_insert_post( (array) $last_revision );
  617. }
  618. /**
  619. * Kicks off magic for an XML-RPC session. We want to keep editing Markdown
  620. * and publishing HTML.
  621. * @param string $xmlrpc_method The current XML-RPC method
  622. * @return null
  623. */
  624. public function xmlrpc_actions( $xmlrpc_method ) {
  625. switch ( $xmlrpc_method ) {
  626. case 'metaWeblog.getRecentPosts':
  627. case 'wp.getPosts':
  628. case 'wp.getPages':
  629. add_action( 'parse_query', array( $this, 'make_filterable' ), 10, 1 );
  630. break;
  631. case 'wp.getPost':
  632. $this->prime_post_cache();
  633. break;
  634. }
  635. }
  636. /**
  637. * metaWeblog.getPost and wp.getPage fire xmlrpc_call action *after* get_post() is called.
  638. * So, we have to detect those methods and prime the post cache early.
  639. * @return null
  640. */
  641. protected function check_for_early_methods() {
  642. $raw_post_data = file_get_contents( "php://input" );
  643. if ( false === strpos( $raw_post_data, 'metaWeblog.getPost' )
  644. && false === strpos( $raw_post_data, 'wp.getPage' ) ) {
  645. return;
  646. }
  647. include_once( ABSPATH . WPINC . '/class-IXR.php' );
  648. $message = new IXR_Message( $raw_post_data );
  649. $message->parse();
  650. $post_id_position = 'metaWeblog.getPost' === $message->methodName ? 0 : 1;
  651. $this->prime_post_cache( $message->params[ $post_id_position ] );
  652. }
  653. /**
  654. * Prime the post cache with swapped post_content. This is a sneaky way of getting around
  655. * the fact that there are no good hooks to call on the *.getPost xmlrpc methods.
  656. *
  657. * @return null
  658. */
  659. private function prime_post_cache( $post_id = false ) {
  660. global $wp_xmlrpc_server;
  661. if ( ! $post_id ) {
  662. $post_id = $wp_xmlrpc_server->message->params[3];
  663. }
  664. // prime the post cache
  665. if ( $this->is_markdown( $post_id ) ) {
  666. $post = get_post( $post_id );
  667. if ( ! empty( $post->post_content_filtered ) ) {
  668. wp_cache_delete( $post->ID, 'posts' );
  669. $post = $this->swap_for_editing( $post );
  670. wp_cache_add( $post->ID, $post, 'posts' );
  671. $this->posts_to_uncache[] = $post_id;
  672. }
  673. }
  674. // uncache munged posts if using a persistent object cache
  675. if ( wp_using_ext_object_cache() ) {
  676. add_action( 'shutdown', array( $this, 'uncache_munged_posts' ) );
  677. }
  678. }
  679. /**
  680. * Swaps `post_content_filtered` back to `post_content` for editing purposes.
  681. * @param object $post WP_Post object
  682. * @return object WP_Post object with swapped `post_content_filtered` and `post_content`
  683. */
  684. protected function swap_for_editing( $post ) {
  685. $markdown = $post->post_content_filtered;
  686. // unencode encoded code blocks
  687. $markdown = $this->get_parser()->codeblock_restore( $markdown );
  688. // restore beginning of line blockquotes
  689. $markdown = preg_replace( '/^&gt; /m', '> ', $markdown );
  690. $post->post_content_filtered = $post->post_content;
  691. $post->post_content = $markdown;
  692. return $post;
  693. }
  694. /**
  695. * We munge the post cache to serve proper markdown content to XML-RPC clients.
  696. * Uncache these after the XML-RPC session ends.
  697. * @return null
  698. */
  699. public function uncache_munged_posts() {
  700. // $this context gets lost in testing sometimes. Weird.
  701. foreach( WPCom_Markdown::get_instance()->posts_to_uncache as $post_id ) {
  702. wp_cache_delete( $post_id, 'posts' );
  703. }
  704. }
  705. /**
  706. * Since *.(get)?[Rr]ecentPosts calls get_posts with suppress filters on, we need to
  707. * turn them back on so that we can swap things for editing.
  708. * @param object $wp_query WP_Query object
  709. * @return null
  710. */
  711. public function make_filterable( $wp_query ) {
  712. $wp_query->set( 'suppress_filters', false );
  713. add_action( 'the_posts', array( $this, 'the_posts' ), 10, 2 );
  714. }
  715. /**
  716. * Swaps post_content and post_content_filtered for editing.
  717. * @param array $posts Posts returned by the just-completed query
  718. * @param object $wp_query Current WP_Query object
  719. * @return array Modified $posts
  720. */
  721. public function the_posts( $posts, $wp_query ) {
  722. foreach ( $posts as $key => $post ) {
  723. if ( $this->is_markdown( $post->ID ) && ! empty( $posts[ $key ]->post_content_filtered ) ) {
  724. $markdown = $posts[ $key ]->post_content_filtered;
  725. $posts[ $key ]->post_content_filtered = $posts[ $key ]->post_content;
  726. $posts[ $key ]->post_content = $markdown;
  727. }
  728. }
  729. return $posts;
  730. }
  731. /**
  732. * Singleton silence is golden
  733. */
  734. private function __construct() {}
  735. }
  736. add_action( 'init', array( WPCom_Markdown::get_instance(), 'load' ) );