revision.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. <?php
  2. /**
  3. * WordPress Administration Revisions API
  4. *
  5. * @package WordPress
  6. * @subpackage Administration
  7. * @since 3.6.0
  8. */
  9. /**
  10. * Get the revision UI diff.
  11. *
  12. * @since 3.6.0
  13. *
  14. * @param WP_Post|int $post The post object or post ID.
  15. * @param int $compare_from The revision ID to compare from.
  16. * @param int $compare_to The revision ID to come to.
  17. * @return array|false Associative array of a post's revisioned fields and their diffs.
  18. * Or, false on failure.
  19. */
  20. function wp_get_revision_ui_diff( $post, $compare_from, $compare_to ) {
  21. $post = get_post( $post );
  22. if ( ! $post ) {
  23. return false;
  24. }
  25. if ( $compare_from ) {
  26. $compare_from = get_post( $compare_from );
  27. if ( ! $compare_from ) {
  28. return false;
  29. }
  30. } else {
  31. // If we're dealing with the first revision...
  32. $compare_from = false;
  33. }
  34. $compare_to = get_post( $compare_to );
  35. if ( ! $compare_to ) {
  36. return false;
  37. }
  38. // If comparing revisions, make sure we're dealing with the right post parent.
  39. // The parent post may be a 'revision' when revisions are disabled and we're looking at autosaves.
  40. if ( $compare_from && $compare_from->post_parent !== $post->ID && $compare_from->ID !== $post->ID ) {
  41. return false;
  42. }
  43. if ( $compare_to->post_parent !== $post->ID && $compare_to->ID !== $post->ID ) {
  44. return false;
  45. }
  46. if ( $compare_from && strtotime( $compare_from->post_date_gmt ) > strtotime( $compare_to->post_date_gmt ) ) {
  47. $temp = $compare_from;
  48. $compare_from = $compare_to;
  49. $compare_to = $temp;
  50. }
  51. // Add default title if title field is empty.
  52. if ( $compare_from && empty( $compare_from->post_title ) ) {
  53. $compare_from->post_title = __( '(no title)' );
  54. }
  55. if ( empty( $compare_to->post_title ) ) {
  56. $compare_to->post_title = __( '(no title)' );
  57. }
  58. $return = array();
  59. foreach ( _wp_post_revision_fields( $post ) as $field => $name ) {
  60. /**
  61. * Contextually filter a post revision field.
  62. *
  63. * The dynamic portion of the hook name, `$field`, corresponds to a name of a
  64. * field of the revision object.
  65. *
  66. * Possible hook names include:
  67. *
  68. * - `_wp_post_revision_field_post_title`
  69. * - `_wp_post_revision_field_post_content`
  70. * - `_wp_post_revision_field_post_excerpt`
  71. *
  72. * @since 3.6.0
  73. *
  74. * @param string $revision_field The current revision field to compare to or from.
  75. * @param string $field The current revision field.
  76. * @param WP_Post $compare_from The revision post object to compare to or from.
  77. * @param string $context The context of whether the current revision is the old
  78. * or the new one. Values are 'to' or 'from'.
  79. */
  80. $content_from = $compare_from ? apply_filters( "_wp_post_revision_field_{$field}", $compare_from->$field, $field, $compare_from, 'from' ) : '';
  81. /** This filter is documented in wp-admin/includes/revision.php */
  82. $content_to = apply_filters( "_wp_post_revision_field_{$field}", $compare_to->$field, $field, $compare_to, 'to' );
  83. $args = array(
  84. 'show_split_view' => true,
  85. 'title_left' => __( 'Removed' ),
  86. 'title_right' => __( 'Added' ),
  87. );
  88. /**
  89. * Filters revisions text diff options.
  90. *
  91. * Filters the options passed to wp_text_diff() when viewing a post revision.
  92. *
  93. * @since 4.1.0
  94. *
  95. * @param array $args {
  96. * Associative array of options to pass to wp_text_diff().
  97. *
  98. * @type bool $show_split_view True for split view (two columns), false for
  99. * un-split view (single column). Default true.
  100. * }
  101. * @param string $field The current revision field.
  102. * @param WP_Post $compare_from The revision post to compare from.
  103. * @param WP_Post $compare_to The revision post to compare to.
  104. */
  105. $args = apply_filters( 'revision_text_diff_options', $args, $field, $compare_from, $compare_to );
  106. $diff = wp_text_diff( $content_from, $content_to, $args );
  107. if ( ! $diff && 'post_title' === $field ) {
  108. // It's a better user experience to still show the Title, even if it didn't change.
  109. // No, you didn't see this.
  110. $diff = '<table class="diff"><colgroup><col class="content diffsplit left"><col class="content diffsplit middle"><col class="content diffsplit right"></colgroup><tbody><tr>';
  111. // In split screen mode, show the title before/after side by side.
  112. if ( true === $args['show_split_view'] ) {
  113. $diff .= '<td>' . esc_html( $compare_from->post_title ) . '</td><td></td><td>' . esc_html( $compare_to->post_title ) . '</td>';
  114. } else {
  115. $diff .= '<td>' . esc_html( $compare_from->post_title ) . '</td>';
  116. // In single column mode, only show the title once if unchanged.
  117. if ( $compare_from->post_title !== $compare_to->post_title ) {
  118. $diff .= '</tr><tr><td>' . esc_html( $compare_to->post_title ) . '</td>';
  119. }
  120. }
  121. $diff .= '</tr></tbody>';
  122. $diff .= '</table>';
  123. }
  124. if ( $diff ) {
  125. $return[] = array(
  126. 'id' => $field,
  127. 'name' => $name,
  128. 'diff' => $diff,
  129. );
  130. }
  131. }
  132. /**
  133. * Filters the fields displayed in the post revision diff UI.
  134. *
  135. * @since 4.1.0
  136. *
  137. * @param array[] $return Array of revision UI fields. Each item is an array of id, name, and diff.
  138. * @param WP_Post $compare_from The revision post to compare from.
  139. * @param WP_Post $compare_to The revision post to compare to.
  140. */
  141. return apply_filters( 'wp_get_revision_ui_diff', $return, $compare_from, $compare_to );
  142. }
  143. /**
  144. * Prepare revisions for JavaScript.
  145. *
  146. * @since 3.6.0
  147. *
  148. * @param WP_Post|int $post The post object or post ID.
  149. * @param int $selected_revision_id The selected revision ID.
  150. * @param int $from Optional. The revision ID to compare from.
  151. * @return array An associative array of revision data and related settings.
  152. */
  153. function wp_prepare_revisions_for_js( $post, $selected_revision_id, $from = null ) {
  154. $post = get_post( $post );
  155. $authors = array();
  156. $now_gmt = time();
  157. $revisions = wp_get_post_revisions(
  158. $post->ID,
  159. array(
  160. 'order' => 'ASC',
  161. 'check_enabled' => false,
  162. )
  163. );
  164. // If revisions are disabled, we only want autosaves and the current post.
  165. if ( ! wp_revisions_enabled( $post ) ) {
  166. foreach ( $revisions as $revision_id => $revision ) {
  167. if ( ! wp_is_post_autosave( $revision ) ) {
  168. unset( $revisions[ $revision_id ] );
  169. }
  170. }
  171. $revisions = array( $post->ID => $post ) + $revisions;
  172. }
  173. $show_avatars = get_option( 'show_avatars' );
  174. cache_users( wp_list_pluck( $revisions, 'post_author' ) );
  175. $can_restore = current_user_can( 'edit_post', $post->ID );
  176. $current_id = false;
  177. foreach ( $revisions as $revision ) {
  178. $modified = strtotime( $revision->post_modified );
  179. $modified_gmt = strtotime( $revision->post_modified_gmt . ' +0000' );
  180. if ( $can_restore ) {
  181. $restore_link = str_replace(
  182. '&amp;',
  183. '&',
  184. wp_nonce_url(
  185. add_query_arg(
  186. array(
  187. 'revision' => $revision->ID,
  188. 'action' => 'restore',
  189. ),
  190. admin_url( 'revision.php' )
  191. ),
  192. "restore-post_{$revision->ID}"
  193. )
  194. );
  195. }
  196. if ( ! isset( $authors[ $revision->post_author ] ) ) {
  197. $authors[ $revision->post_author ] = array(
  198. 'id' => (int) $revision->post_author,
  199. 'avatar' => $show_avatars ? get_avatar( $revision->post_author, 32 ) : '',
  200. 'name' => get_the_author_meta( 'display_name', $revision->post_author ),
  201. );
  202. }
  203. $autosave = (bool) wp_is_post_autosave( $revision );
  204. $current = ! $autosave && $revision->post_modified_gmt === $post->post_modified_gmt;
  205. if ( $current && ! empty( $current_id ) ) {
  206. // If multiple revisions have the same post_modified_gmt, highest ID is current.
  207. if ( $current_id < $revision->ID ) {
  208. $revisions[ $current_id ]['current'] = false;
  209. $current_id = $revision->ID;
  210. } else {
  211. $current = false;
  212. }
  213. } elseif ( $current ) {
  214. $current_id = $revision->ID;
  215. }
  216. $revisions_data = array(
  217. 'id' => $revision->ID,
  218. 'title' => get_the_title( $post->ID ),
  219. 'author' => $authors[ $revision->post_author ],
  220. 'date' => date_i18n( __( 'M j, Y @ H:i' ), $modified ),
  221. 'dateShort' => date_i18n( _x( 'j M @ H:i', 'revision date short format' ), $modified ),
  222. /* translators: %s: Human-readable time difference. */
  223. 'timeAgo' => sprintf( __( '%s ago' ), human_time_diff( $modified_gmt, $now_gmt ) ),
  224. 'autosave' => $autosave,
  225. 'current' => $current,
  226. 'restoreUrl' => $can_restore ? $restore_link : false,
  227. );
  228. /**
  229. * Filters the array of revisions used on the revisions screen.
  230. *
  231. * @since 4.4.0
  232. *
  233. * @param array $revisions_data {
  234. * The bootstrapped data for the revisions screen.
  235. *
  236. * @type int $id Revision ID.
  237. * @type string $title Title for the revision's parent WP_Post object.
  238. * @type int $author Revision post author ID.
  239. * @type string $date Date the revision was modified.
  240. * @type string $dateShort Short-form version of the date the revision was modified.
  241. * @type string $timeAgo GMT-aware amount of time ago the revision was modified.
  242. * @type bool $autosave Whether the revision is an autosave.
  243. * @type bool $current Whether the revision is both not an autosave and the post
  244. * modified date matches the revision modified date (GMT-aware).
  245. * @type bool|false $restoreUrl URL if the revision can be restored, false otherwise.
  246. * }
  247. * @param WP_Post $revision The revision's WP_Post object.
  248. * @param WP_Post $post The revision's parent WP_Post object.
  249. */
  250. $revisions[ $revision->ID ] = apply_filters( 'wp_prepare_revision_for_js', $revisions_data, $revision, $post );
  251. }
  252. /**
  253. * If we only have one revision, the initial revision is missing; This happens
  254. * when we have an autsosave and the user has clicked 'View the Autosave'
  255. */
  256. if ( 1 === count( $revisions ) ) {
  257. $revisions[ $post->ID ] = array(
  258. 'id' => $post->ID,
  259. 'title' => get_the_title( $post->ID ),
  260. 'author' => $authors[ $revision->post_author ],
  261. 'date' => date_i18n( __( 'M j, Y @ H:i' ), strtotime( $post->post_modified ) ),
  262. 'dateShort' => date_i18n( _x( 'j M @ H:i', 'revision date short format' ), strtotime( $post->post_modified ) ),
  263. /* translators: %s: Human-readable time difference. */
  264. 'timeAgo' => sprintf( __( '%s ago' ), human_time_diff( strtotime( $post->post_modified_gmt ), $now_gmt ) ),
  265. 'autosave' => false,
  266. 'current' => true,
  267. 'restoreUrl' => false,
  268. );
  269. $current_id = $post->ID;
  270. }
  271. /*
  272. * If a post has been saved since the latest revision (no revisioned fields
  273. * were changed), we may not have a "current" revision. Mark the latest
  274. * revision as "current".
  275. */
  276. if ( empty( $current_id ) ) {
  277. if ( $revisions[ $revision->ID ]['autosave'] ) {
  278. $revision = end( $revisions );
  279. while ( $revision['autosave'] ) {
  280. $revision = prev( $revisions );
  281. }
  282. $current_id = $revision['id'];
  283. } else {
  284. $current_id = $revision->ID;
  285. }
  286. $revisions[ $current_id ]['current'] = true;
  287. }
  288. // Now, grab the initial diff.
  289. $compare_two_mode = is_numeric( $from );
  290. if ( ! $compare_two_mode ) {
  291. $found = array_search( $selected_revision_id, array_keys( $revisions ), true );
  292. if ( $found ) {
  293. $from = array_keys( array_slice( $revisions, $found - 1, 1, true ) );
  294. $from = reset( $from );
  295. } else {
  296. $from = 0;
  297. }
  298. }
  299. $from = absint( $from );
  300. $diffs = array(
  301. array(
  302. 'id' => $from . ':' . $selected_revision_id,
  303. 'fields' => wp_get_revision_ui_diff( $post->ID, $from, $selected_revision_id ),
  304. ),
  305. );
  306. return array(
  307. 'postId' => $post->ID,
  308. 'nonce' => wp_create_nonce( 'revisions-ajax-nonce' ),
  309. 'revisionData' => array_values( $revisions ),
  310. 'to' => $selected_revision_id,
  311. 'from' => $from,
  312. 'diffData' => $diffs,
  313. 'baseUrl' => parse_url( admin_url( 'revision.php' ), PHP_URL_PATH ),
  314. 'compareTwoMode' => absint( $compare_two_mode ), // Apparently booleans are not allowed.
  315. 'revisionIds' => array_keys( $revisions ),
  316. );
  317. }
  318. /**
  319. * Print JavaScript templates required for the revisions experience.
  320. *
  321. * @since 4.1.0
  322. *
  323. * @global WP_Post $post Global post object.
  324. */
  325. function wp_print_revision_templates() {
  326. global $post;
  327. ?><script id="tmpl-revisions-frame" type="text/html">
  328. <div class="revisions-control-frame"></div>
  329. <div class="revisions-diff-frame"></div>
  330. </script>
  331. <script id="tmpl-revisions-buttons" type="text/html">
  332. <div class="revisions-previous">
  333. <input class="button" type="button" value="<?php echo esc_attr_x( 'Previous', 'Button label for a previous revision' ); ?>" />
  334. </div>
  335. <div class="revisions-next">
  336. <input class="button" type="button" value="<?php echo esc_attr_x( 'Next', 'Button label for a next revision' ); ?>" />
  337. </div>
  338. </script>
  339. <script id="tmpl-revisions-checkbox" type="text/html">
  340. <div class="revision-toggle-compare-mode">
  341. <label>
  342. <input type="checkbox" class="compare-two-revisions"
  343. <#
  344. if ( 'undefined' !== typeof data && data.model.attributes.compareTwoMode ) {
  345. #> checked="checked"<#
  346. }
  347. #>
  348. />
  349. <?php esc_html_e( 'Compare any two revisions' ); ?>
  350. </label>
  351. </div>
  352. </script>
  353. <script id="tmpl-revisions-meta" type="text/html">
  354. <# if ( ! _.isUndefined( data.attributes ) ) { #>
  355. <div class="diff-title">
  356. <# if ( 'from' === data.type ) { #>
  357. <strong><?php _ex( 'From:', 'Followed by post revision info' ); ?></strong>
  358. <# } else if ( 'to' === data.type ) { #>
  359. <strong><?php _ex( 'To:', 'Followed by post revision info' ); ?></strong>
  360. <# } #>
  361. <div class="author-card<# if ( data.attributes.autosave ) { #> autosave<# } #>">
  362. {{{ data.attributes.author.avatar }}}
  363. <div class="author-info">
  364. <# if ( data.attributes.autosave ) { #>
  365. <span class="byline">
  366. <?php
  367. printf(
  368. /* translators: %s: User's display name. */
  369. __( 'Autosave by %s' ),
  370. '<span class="author-name">{{ data.attributes.author.name }}</span>'
  371. );
  372. ?>
  373. </span>
  374. <# } else if ( data.attributes.current ) { #>
  375. <span class="byline">
  376. <?php
  377. printf(
  378. /* translators: %s: User's display name. */
  379. __( 'Current Revision by %s' ),
  380. '<span class="author-name">{{ data.attributes.author.name }}</span>'
  381. );
  382. ?>
  383. </span>
  384. <# } else { #>
  385. <span class="byline">
  386. <?php
  387. printf(
  388. /* translators: %s: User's display name. */
  389. __( 'Revision by %s' ),
  390. '<span class="author-name">{{ data.attributes.author.name }}</span>'
  391. );
  392. ?>
  393. </span>
  394. <# } #>
  395. <span class="time-ago">{{ data.attributes.timeAgo }}</span>
  396. <span class="date">({{ data.attributes.dateShort }})</span>
  397. </div>
  398. <# if ( 'to' === data.type && data.attributes.restoreUrl ) { #>
  399. <input <?php if ( wp_check_post_lock( $post->ID ) ) { ?>
  400. disabled="disabled"
  401. <?php } else { ?>
  402. <# if ( data.attributes.current ) { #>
  403. disabled="disabled"
  404. <# } #>
  405. <?php } ?>
  406. <# if ( data.attributes.autosave ) { #>
  407. type="button" class="restore-revision button button-primary" value="<?php esc_attr_e( 'Restore This Autosave' ); ?>" />
  408. <# } else { #>
  409. type="button" class="restore-revision button button-primary" value="<?php esc_attr_e( 'Restore This Revision' ); ?>" />
  410. <# } #>
  411. <# } #>
  412. </div>
  413. <# if ( 'tooltip' === data.type ) { #>
  414. <div class="revisions-tooltip-arrow"><span></span></div>
  415. <# } #>
  416. <# } #>
  417. </script>
  418. <script id="tmpl-revisions-diff" type="text/html">
  419. <div class="loading-indicator"><span class="spinner"></span></div>
  420. <div class="diff-error"><?php _e( 'Sorry, something went wrong. The requested comparison could not be loaded.' ); ?></div>
  421. <div class="diff">
  422. <# _.each( data.fields, function( field ) { #>
  423. <h3>{{ field.name }}</h3>
  424. {{{ field.diff }}}
  425. <# }); #>
  426. </div>
  427. </script>
  428. <?php
  429. }