[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ -> revision.php (source)

   1  <?php
   2  /**
   3   * Post revision functions.
   4   *
   5   * @package WordPress
   6   * @subpackage Post_Revisions
   7   */
   8  
   9  /**
  10   * Determines which fields of posts are to be saved in revisions.
  11   *
  12   * @since 2.6.0
  13   * @since 4.5.0 A `WP_Post` object can now be passed to the `$post` parameter.
  14   * @since 4.5.0 The optional `$autosave` parameter was deprecated and renamed to `$deprecated`.
  15   * @access private
  16   *
  17   * @param array|WP_Post $post       Optional. A post array or a WP_Post object being processed
  18   *                                  for insertion as a post revision. Default empty array.
  19   * @param bool          $deprecated Not used.
  20   * @return string[] Array of fields that can be versioned.
  21   *
  22   * @phpstan-param false $deprecated
  23   */
  24  function _wp_post_revision_fields( $post = array(), $deprecated = false ) {
  25      static $fields = null;
  26  
  27      if ( ! is_array( $post ) ) {
  28          $post = get_post( $post, ARRAY_A );
  29      }
  30  
  31      if ( is_null( $fields ) ) {
  32          // Allow these to be versioned.
  33          $fields = array(
  34              'post_title'   => __( 'Title' ),
  35              'post_content' => __( 'Content' ),
  36              'post_excerpt' => __( 'Excerpt' ),
  37          );
  38      }
  39  
  40      /**
  41       * Filters the list of fields saved in post revisions.
  42       *
  43       * Included by default: 'post_title', 'post_content' and 'post_excerpt'.
  44       *
  45       * Disallowed fields: 'ID', 'post_name', 'post_parent', 'post_date',
  46       * 'post_date_gmt', 'post_status', 'post_type', 'comment_count',
  47       * and 'post_author'.
  48       *
  49       * @since 2.6.0
  50       * @since 4.5.0 The `$post` parameter was added.
  51       *
  52       * @param string[] $fields List of fields to revision. Contains 'post_title',
  53       *                         'post_content', and 'post_excerpt' by default.
  54       * @param array    $post   A post array being processed for insertion as a post revision.
  55       */
  56      $fields = apply_filters( '_wp_post_revision_fields', $fields, $post );
  57  
  58      // WP uses these internally either in versioning or elsewhere - they cannot be versioned.
  59      foreach ( array( 'ID', 'post_name', 'post_parent', 'post_date', 'post_date_gmt', 'post_status', 'post_type', 'comment_count', 'post_author' ) as $protect ) {
  60          unset( $fields[ $protect ] );
  61      }
  62  
  63      return $fields;
  64  }
  65  
  66  /**
  67   * Returns a post array ready to be inserted into the posts table as a post revision.
  68   *
  69   * @since 4.5.0
  70   * @access private
  71   *
  72   * @param array|WP_Post $post     Optional. A post array or a WP_Post object to be processed
  73   *                                for insertion as a post revision. Default empty array.
  74   * @param bool          $autosave Optional. Is the revision an autosave? Default false.
  75   * @return array Post array ready to be inserted as a post revision.
  76   */
  77  function _wp_post_revision_data( $post = array(), $autosave = false ) {
  78      if ( ! is_array( $post ) ) {
  79          $post = get_post( $post, ARRAY_A );
  80      }
  81  
  82      $fields = _wp_post_revision_fields( $post );
  83  
  84      $revision_data = array();
  85  
  86      foreach ( array_intersect( array_keys( $post ), array_keys( $fields ) ) as $field ) {
  87          $revision_data[ $field ] = $post[ $field ];
  88      }
  89  
  90      $revision_data['post_parent']   = $post['ID'];
  91      $revision_data['post_status']   = 'inherit';
  92      $revision_data['post_type']     = 'revision';
  93      $revision_data['post_name']     = $autosave ? "$post[ID]-autosave-v1" : "$post[ID]-revision-v1"; // "1" is the revisioning system version.
  94      $revision_data['post_date']     = $post['post_modified'] ?? '';
  95      $revision_data['post_date_gmt'] = $post['post_modified_gmt'] ?? '';
  96  
  97      return $revision_data;
  98  }
  99  
 100  /**
 101   * Saves revisions for a post after all changes have been made.
 102   *
 103   * @since 6.4.0
 104   *
 105   * @param int     $post_id The post id that was inserted.
 106   * @param WP_Post $post    The post object that was inserted.
 107   * @param bool    $update  Whether this insert is updating an existing post.
 108   */
 109  function wp_save_post_revision_on_insert( $post_id, $post, $update ) {
 110      if ( ! $update ) {
 111          return;
 112      }
 113  
 114      if ( ! has_action( 'post_updated', 'wp_save_post_revision' ) ) {
 115          return;
 116      }
 117  
 118      wp_save_post_revision( $post_id );
 119  }
 120  
 121  /**
 122   * Creates a revision for the current version of a post.
 123   *
 124   * Typically used immediately after a post update, as every update is a revision,
 125   * and the most recent revision always matches the current post.
 126   *
 127   * @since 2.6.0
 128   *
 129   * @param int $post_id The ID of the post to save as a revision.
 130   * @return int|WP_Error|null Null or 0 if error, new revision ID, if success.
 131   */
 132  function wp_save_post_revision( $post_id ) {
 133      if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
 134          return null;
 135      }
 136  
 137      // Prevent saving post revisions if revisions should be saved on wp_after_insert_post.
 138      if ( doing_action( 'post_updated' ) && has_action( 'wp_after_insert_post', 'wp_save_post_revision_on_insert' ) ) {
 139          return null;
 140      }
 141  
 142      $post = get_post( $post_id );
 143  
 144      if ( ! $post ) {
 145          return null;
 146      }
 147  
 148      if ( ! post_type_supports( $post->post_type, 'revisions' ) ) {
 149          return null;
 150      }
 151  
 152      if ( 'auto-draft' === $post->post_status ) {
 153          return null;
 154      }
 155  
 156      if ( ! wp_revisions_enabled( $post ) ) {
 157          return null;
 158      }
 159  
 160      /*
 161       * Compare the proposed update with the last stored revision verifying that
 162       * they are different, unless a plugin tells us to always save regardless.
 163       * If no previous revisions, save one.
 164       */
 165      $revisions = wp_get_post_revisions( $post_id );
 166      if ( $revisions ) {
 167          // Grab the latest revision, but not an autosave.
 168          foreach ( $revisions as $revision ) {
 169              if ( str_contains( $revision->post_name, "{$revision->post_parent}-revision" ) ) {
 170                  $latest_revision = $revision;
 171                  break;
 172              }
 173          }
 174  
 175          /**
 176           * Filters whether the post has changed since the latest revision.
 177           *
 178           * By default a revision is saved only if one of the revisioned fields has changed.
 179           * This filter can override that so a revision is saved even if nothing has changed.
 180           *
 181           * @since 3.6.0
 182           *
 183           * @param bool    $check_for_changes Whether to check for changes before saving a new revision.
 184           *                                   Default true.
 185           * @param WP_Post $latest_revision   The latest revision post object.
 186           * @param WP_Post $post              The post object.
 187           */
 188          if ( isset( $latest_revision ) && apply_filters( 'wp_save_post_revision_check_for_changes', true, $latest_revision, $post ) ) {
 189              $post_has_changed = false;
 190  
 191              foreach ( array_keys( _wp_post_revision_fields( $post ) ) as $field ) {
 192                  if ( normalize_whitespace( maybe_serialize( $post->$field ) ) !== normalize_whitespace( maybe_serialize( $latest_revision->$field ) ) ) {
 193                      $post_has_changed = true;
 194                      break;
 195                  }
 196              }
 197  
 198              /**
 199               * Filters whether a post has changed.
 200               *
 201               * By default a revision is saved only if one of the revisioned fields has changed.
 202               * This filter allows for additional checks to determine if there were changes.
 203               *
 204               * @since 4.1.0
 205               *
 206               * @param bool    $post_has_changed Whether the post has changed.
 207               * @param WP_Post $latest_revision  The latest revision post object.
 208               * @param WP_Post $post             The post object.
 209               */
 210              $post_has_changed = (bool) apply_filters( 'wp_save_post_revision_post_has_changed', $post_has_changed, $latest_revision, $post );
 211  
 212              // Don't save revision if post unchanged.
 213              if ( ! $post_has_changed ) {
 214                  return null;
 215              }
 216          }
 217      }
 218  
 219      $return = _wp_put_post_revision( $post );
 220  
 221      /*
 222       * If a limit for the number of revisions to keep has been set,
 223       * delete the oldest ones.
 224       */
 225      $revisions_to_keep = wp_revisions_to_keep( $post );
 226  
 227      if ( $revisions_to_keep < 0 ) {
 228          return $return;
 229      }
 230  
 231      $revisions = wp_get_post_revisions( $post_id, array( 'order' => 'ASC' ) );
 232  
 233      /**
 234       * Filters the revisions to be considered for deletion.
 235       *
 236       * @since 6.2.0
 237       *
 238       * @param WP_Post[] $revisions Array of revisions, or an empty array if none.
 239       * @param int       $post_id   The ID of the post to save as a revision.
 240       */
 241      $revisions = apply_filters(
 242          'wp_save_post_revision_revisions_before_deletion',
 243          $revisions,
 244          $post_id
 245      );
 246  
 247      $delete = count( $revisions ) - $revisions_to_keep;
 248  
 249      if ( $delete < 1 ) {
 250          return $return;
 251      }
 252  
 253      $revisions = array_slice( $revisions, 0, $delete );
 254  
 255      for ( $i = 0; isset( $revisions[ $i ] ); $i++ ) {
 256          if ( str_contains( $revisions[ $i ]->post_name, 'autosave' ) ) {
 257              continue;
 258          }
 259  
 260          wp_delete_post_revision( $revisions[ $i ]->ID );
 261      }
 262  
 263      return $return;
 264  }
 265  
 266  /**
 267   * Retrieves the autosaved data of the specified post.
 268   *
 269   * Returns a post object with the information that was autosaved for the specified post.
 270   * If the optional $user_id is passed, returns the autosave for that user, otherwise
 271   * returns the latest autosave.
 272   *
 273   * @since 2.6.0
 274   *
 275   * @param int $post_id The post ID.
 276   * @param int $user_id Optional. The post author ID. Default 0.
 277   * @return WP_Post|false The autosaved data or false on failure or when no autosave exists.
 278   */
 279  function wp_get_post_autosave( $post_id, $user_id = 0 ) {
 280      $args = array(
 281          'post_type'      => 'revision',
 282          'post_status'    => 'inherit',
 283          'post_parent'    => $post_id,
 284          'name'           => $post_id . '-autosave-v1',
 285          'posts_per_page' => 1,
 286          'orderby'        => 'date',
 287          'order'          => 'DESC',
 288          'fields'         => 'ids',
 289          'no_found_rows'  => true,
 290      );
 291  
 292      if ( 0 !== $user_id ) {
 293          $args['author'] = $user_id;
 294      }
 295  
 296      $query = new WP_Query( $args );
 297  
 298      if ( ! $query->have_posts() ) {
 299          return false;
 300      }
 301  
 302      return get_post( $query->posts[0] );
 303  }
 304  
 305  /**
 306   * Determines if the specified post is a revision.
 307   *
 308   * @since 2.6.0
 309   *
 310   * @param int|WP_Post $post Post ID or post object.
 311   * @return int|false ID of revision's parent on success, false if not a revision.
 312   *
 313   * @phpstan-return ( $post is int<min, -1> ? false : false|int<0, max> )
 314   */
 315  function wp_is_post_revision( $post ) {
 316      $post = wp_get_post_revision( $post );
 317  
 318      if ( ! $post ) {
 319          return false;
 320      }
 321  
 322      return (int) $post->post_parent;
 323  }
 324  
 325  /**
 326   * Determines if the specified post is an autosave.
 327   *
 328   * @since 2.6.0
 329   *
 330   * @param int|WP_Post $post Post ID or post object.
 331   * @return int|false ID of autosave's parent on success, false if not a revision.
 332   */
 333  function wp_is_post_autosave( $post ) {
 334      $post = wp_get_post_revision( $post );
 335  
 336      if ( ! $post ) {
 337          return false;
 338      }
 339  
 340      if ( str_contains( $post->post_name, "{$post->post_parent}-autosave" ) ) {
 341          return (int) $post->post_parent;
 342      }
 343  
 344      return false;
 345  }
 346  
 347  /**
 348   * Inserts post data into the posts table as a post revision.
 349   *
 350   * @since 2.6.0
 351   * @access private
 352   *
 353   * @param int|WP_Post|array|null $post     Post ID, post object OR post array.
 354   * @param bool                   $autosave Optional. Whether the revision is an autosave or not.
 355   *                                         Default false.
 356   * @return int|WP_Error WP_Error or 0 if error, new revision ID if success.
 357   */
 358  function _wp_put_post_revision( $post = null, $autosave = false ) {
 359      if ( is_object( $post ) ) {
 360          $post = get_object_vars( $post );
 361      } elseif ( ! is_array( $post ) ) {
 362          $post = get_post( $post, ARRAY_A );
 363      }
 364  
 365      if ( ! $post || empty( $post['ID'] ) ) {
 366          return new WP_Error( 'invalid_post', __( 'Invalid post ID.' ) );
 367      }
 368  
 369      if ( isset( $post['post_type'] ) && 'revision' === $post['post_type'] ) {
 370          return new WP_Error( 'post_type', __( 'Cannot create a revision of a revision' ) );
 371      }
 372  
 373      $post = _wp_post_revision_data( $post, $autosave );
 374      $post = wp_slash( $post ); // Since data is from DB.
 375  
 376      $revision_id = wp_insert_post( $post, true );
 377      if ( is_wp_error( $revision_id ) ) {
 378          return $revision_id;
 379      }
 380  
 381      if ( $revision_id ) {
 382          /**
 383           * Fires once a revision has been saved.
 384           *
 385           * @since 2.6.0
 386           * @since 6.4.0 The `$post_id` parameter was added.
 387           *
 388           * @param int $revision_id Post revision ID.
 389           * @param int $post_id     Post ID.
 390           */
 391          do_action( '_wp_put_post_revision', $revision_id, $post['post_parent'] );
 392      }
 393  
 394      return $revision_id;
 395  }
 396  
 397  
 398  /**
 399   * Save the revisioned meta fields.
 400   *
 401   * @since 6.4.0
 402   *
 403   * @param int $revision_id The ID of the revision to save the meta to.
 404   * @param int $post_id     The ID of the post the revision is associated with.
 405   */
 406  function wp_save_revisioned_meta_fields( $revision_id, $post_id ) {
 407      $post_type = get_post_type( $post_id );
 408      if ( ! $post_type ) {
 409          return;
 410      }
 411  
 412      foreach ( wp_post_revision_meta_keys( $post_type ) as $meta_key ) {
 413          if ( metadata_exists( 'post', $post_id, $meta_key ) ) {
 414              _wp_copy_post_meta( $post_id, $revision_id, $meta_key );
 415          }
 416      }
 417  }
 418  
 419  /**
 420   * Gets a post revision.
 421   *
 422   * @since 2.6.0
 423   *
 424   * @param int|WP_Post $post   Post ID or post object.
 425   * @param string      $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which
 426   *                            correspond to a WP_Post object, an associative array, or a numeric array,
 427   *                            respectively. Default OBJECT.
 428   * @param string      $filter Optional sanitization filter. See sanitize_post(). Default 'raw'.
 429   * @return WP_Post|array|null WP_Post (or array) on success, or null on failure.
 430   *
 431   * @phpstan-param int|WP_Post $post
 432   * @phpstan-param 'OBJECT'|'ARRAY_A'|'ARRAY_N' $output
 433   * @phpstan-param 'raw'|'edit'|'db'|'display' $filter
 434   * @phpstan-return (
 435   *     $output is 'ARRAY_A' ? non-empty-array<string, mixed>|null : (
 436   *         $output is 'ARRAY_N' ? non-empty-array<int, mixed>|null : (
 437   *             WP_Post|null
 438   *         )
 439   *     )
 440   * )
 441   */
 442  function wp_get_post_revision( &$post, $output = OBJECT, $filter = 'raw' ) {
 443      $revision = get_post( $post, OBJECT, $filter );
 444  
 445      if ( ! $revision ) {
 446          return $revision;
 447      }
 448  
 449      if ( 'revision' !== $revision->post_type ) {
 450          return null;
 451      }
 452  
 453      if ( OBJECT === $output ) {
 454          return $revision;
 455      } elseif ( ARRAY_A === $output ) {
 456          /** @var non-empty-array<string, mixed> $_revision */
 457          $_revision = get_object_vars( $revision );
 458          return $_revision;
 459      } elseif ( ARRAY_N === $output ) {
 460          /** @var non-empty-array<string, mixed> $vars */
 461          $vars      = get_object_vars( $revision );
 462          $_revision = array_values( $vars );
 463          return $_revision;
 464      }
 465  
 466      return $revision;
 467  }
 468  
 469  /**
 470   * Restores a post to the specified revision.
 471   *
 472   * Can restore a past revision using all fields of the post revision, or only selected fields.
 473   *
 474   * @since 2.6.0
 475   *
 476   * @param int|WP_Post $revision Revision ID or revision object.
 477   * @param array       $fields   Optional. What fields to restore from. Defaults to all.
 478   * @return int|false|null Null if error, false if no fields to restore, (int) post ID if success.
 479   */
 480  function wp_restore_post_revision( $revision, $fields = null ) {
 481      $revision = wp_get_post_revision( $revision, ARRAY_A );
 482  
 483      if ( ! $revision ) {
 484          return $revision;
 485      }
 486  
 487      if ( ! is_array( $fields ) ) {
 488          $fields = array_keys( _wp_post_revision_fields( $revision ) );
 489      }
 490  
 491      $update = array();
 492      foreach ( array_intersect( array_keys( $revision ), $fields ) as $field ) {
 493          $update[ $field ] = $revision[ $field ];
 494      }
 495  
 496      if ( ! $update ) {
 497          return false;
 498      }
 499  
 500      $update['ID'] = $revision['post_parent'];
 501  
 502      $update = wp_slash( $update ); // Since data is from DB.
 503  
 504      $post_id = wp_update_post( $update );
 505  
 506      if ( ! $post_id || is_wp_error( $post_id ) ) {
 507          return $post_id;
 508      }
 509  
 510      // Update last edit user.
 511      update_post_meta( $post_id, '_edit_last', get_current_user_id() );
 512  
 513      /**
 514       * Fires after a post revision has been restored.
 515       *
 516       * @since 2.6.0
 517       *
 518       * @param int $post_id     Post ID.
 519       * @param int $revision_id Post revision ID.
 520       */
 521      do_action( 'wp_restore_post_revision', $post_id, $revision['ID'] );
 522  
 523      return $post_id;
 524  }
 525  
 526  /**
 527   * Restore the revisioned meta values for a post.
 528   *
 529   * @since 6.4.0
 530   *
 531   * @param int $post_id     The ID of the post to restore the meta to.
 532   * @param int $revision_id The ID of the revision to restore the meta from.
 533   */
 534  function wp_restore_post_revision_meta( $post_id, $revision_id ) {
 535      $post_type = get_post_type( $post_id );
 536      if ( ! $post_type ) {
 537          return;
 538      }
 539  
 540      // Restore revisioned meta fields.
 541      foreach ( wp_post_revision_meta_keys( $post_type ) as $meta_key ) {
 542  
 543          // Clear any existing meta.
 544          delete_post_meta( $post_id, $meta_key );
 545  
 546          _wp_copy_post_meta( $revision_id, $post_id, $meta_key );
 547      }
 548  }
 549  
 550  /**
 551   * Copy post meta for the given key from one post to another.
 552   *
 553   * @since 6.4.0
 554   *
 555   * @param int    $source_post_id Post ID to copy meta value(s) from.
 556   * @param int    $target_post_id Post ID to copy meta value(s) to.
 557   * @param string $meta_key       Meta key to copy.
 558   */
 559  function _wp_copy_post_meta( $source_post_id, $target_post_id, $meta_key ) {
 560  
 561      foreach ( get_post_meta( $source_post_id, $meta_key ) as $meta_value ) {
 562          /**
 563           * We use add_metadata() function vs add_post_meta() here
 564           * to allow for a revision post target OR regular post.
 565           */
 566          add_metadata( 'post', $target_post_id, $meta_key, wp_slash( $meta_value ) );
 567      }
 568  }
 569  
 570  /**
 571   * Determine which post meta fields should be revisioned.
 572   *
 573   * @since 6.4.0
 574   *
 575   * @param string $post_type The post type being revisioned.
 576   * @return array An array of meta keys to be revisioned.
 577   */
 578  function wp_post_revision_meta_keys( $post_type ) {
 579      $registered_meta = array_merge(
 580          get_registered_meta_keys( 'post' ),
 581          get_registered_meta_keys( 'post', $post_type )
 582      );
 583  
 584      $wp_revisioned_meta_keys = array();
 585  
 586      foreach ( $registered_meta as $name => $args ) {
 587          if ( $args['revisions_enabled'] ) {
 588              $wp_revisioned_meta_keys[ $name ] = true;
 589          }
 590      }
 591  
 592      $wp_revisioned_meta_keys = array_keys( $wp_revisioned_meta_keys );
 593  
 594      /**
 595       * Filter the list of post meta keys to be revisioned.
 596       *
 597       * @since 6.4.0
 598       *
 599       * @param array  $keys      An array of meta fields to be revisioned.
 600       * @param string $post_type The post type being revisioned.
 601       */
 602      return apply_filters( 'wp_post_revision_meta_keys', $wp_revisioned_meta_keys, $post_type );
 603  }
 604  
 605  /**
 606   * Check whether revisioned post meta fields have changed.
 607   *
 608   * @since 6.4.0
 609   *
 610   * @param bool    $post_has_changed Whether the post has changed.
 611   * @param WP_Post $last_revision    The last revision post object.
 612   * @param WP_Post $post             The post object.
 613   * @return bool Whether the post has changed.
 614   */
 615  function wp_check_revisioned_meta_fields_have_changed( $post_has_changed, WP_Post $last_revision, WP_Post $post ) {
 616      foreach ( wp_post_revision_meta_keys( $post->post_type ) as $meta_key ) {
 617          if ( get_post_meta( $post->ID, $meta_key ) !== get_post_meta( $last_revision->ID, $meta_key ) ) {
 618              $post_has_changed = true;
 619              break;
 620          }
 621      }
 622      return $post_has_changed;
 623  }
 624  
 625  /**
 626   * Deletes a revision.
 627   *
 628   * Deletes the row from the posts table corresponding to the specified revision.
 629   *
 630   * @since 2.6.0
 631   *
 632   * @param int|WP_Post $revision Revision ID or revision object.
 633   * @return WP_Post|false|null Null or false if error, deleted post object if success.
 634   */
 635  function wp_delete_post_revision( $revision ) {
 636      $revision = wp_get_post_revision( $revision );
 637  
 638      if ( ! $revision ) {
 639          return $revision;
 640      }
 641  
 642      $delete = wp_delete_post( $revision->ID );
 643  
 644      if ( $delete ) {
 645          /**
 646           * Fires once a post revision has been deleted.
 647           *
 648           * @since 2.6.0
 649           *
 650           * @param int     $revision_id Post revision ID.
 651           * @param WP_Post $revision    Post revision object.
 652           */
 653          do_action( 'wp_delete_post_revision', $revision->ID, $revision );
 654      }
 655  
 656      return $delete;
 657  }
 658  
 659  /**
 660   * Returns all revisions of specified post.
 661   *
 662   * @since 2.6.0
 663   *
 664   * @see get_children()
 665   *
 666   * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global `$post`.
 667   * @param array|null  $args Optional. Arguments for retrieving post revisions. Default null.
 668   * @return WP_Post[]|int[] Array of revision objects or IDs, or an empty array if none.
 669   */
 670  function wp_get_post_revisions( $post = 0, $args = null ) {
 671      $post = get_post( $post );
 672  
 673      if ( ! $post || empty( $post->ID ) ) {
 674          return array();
 675      }
 676  
 677      $defaults = array(
 678          'order'         => 'DESC',
 679          'orderby'       => 'date ID',
 680          'check_enabled' => true,
 681      );
 682      $args     = wp_parse_args( $args, $defaults );
 683  
 684      if ( $args['check_enabled'] && ! wp_revisions_enabled( $post ) ) {
 685          return array();
 686      }
 687  
 688      $args = array_merge(
 689          $args,
 690          array(
 691              'post_parent' => $post->ID,
 692              'post_type'   => 'revision',
 693              'post_status' => 'inherit',
 694          )
 695      );
 696  
 697      $revisions = get_children( $args );
 698  
 699      if ( ! $revisions ) {
 700          return array();
 701      }
 702  
 703      return $revisions;
 704  }
 705  
 706  /**
 707   * Returns the latest revision ID and count of revisions for a post.
 708   *
 709   * @since 6.1.0
 710   *
 711   * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post.
 712   * @return array|WP_Error {
 713   *     Returns associative array with latest revision ID and total count,
 714   *     or a WP_Error if the post does not exist or revisions are not enabled.
 715   *
 716   *     @type int $latest_id The latest revision post ID or 0 if no revisions exist.
 717   *     @type int $count     The total count of revisions for the given post.
 718   * }
 719   */
 720  function wp_get_latest_revision_id_and_total_count( $post = 0 ) {
 721      $post = get_post( $post );
 722  
 723      if ( ! $post ) {
 724          return new WP_Error( 'invalid_post', __( 'Invalid post.' ) );
 725      }
 726  
 727      if ( ! wp_revisions_enabled( $post ) ) {
 728          return new WP_Error( 'revisions_not_enabled', __( 'Revisions not enabled.' ) );
 729      }
 730  
 731      $args = array(
 732          'post_parent'         => $post->ID,
 733          'fields'              => 'ids',
 734          'post_type'           => 'revision',
 735          'post_status'         => 'inherit',
 736          'order'               => 'DESC',
 737          'orderby'             => 'date ID',
 738          'posts_per_page'      => 1,
 739          'ignore_sticky_posts' => true,
 740      );
 741  
 742      $revision_query = new WP_Query();
 743      $revisions      = $revision_query->query( $args );
 744  
 745      if ( ! $revisions ) {
 746          return array(
 747              'latest_id' => 0,
 748              'count'     => 0,
 749          );
 750      }
 751  
 752      return array(
 753          'latest_id' => $revisions[0],
 754          'count'     => $revision_query->found_posts,
 755      );
 756  }
 757  
 758  /**
 759   * Returns the url for viewing and potentially restoring revisions of a given post.
 760   *
 761   * @since 5.9.0
 762   *
 763   * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global `$post`.
 764   * @return string|null The URL for editing revisions on the given post, otherwise null.
 765   */
 766  function wp_get_post_revisions_url( $post = 0 ) {
 767      $post = get_post( $post );
 768  
 769      if ( ! $post instanceof WP_Post ) {
 770          return null;
 771      }
 772  
 773      // If the post is a revision, return early.
 774      if ( 'revision' === $post->post_type ) {
 775          return get_edit_post_link( $post );
 776      }
 777  
 778      if ( ! wp_revisions_enabled( $post ) ) {
 779          return null;
 780      }
 781  
 782      $revisions = wp_get_latest_revision_id_and_total_count( $post->ID );
 783  
 784      if ( is_wp_error( $revisions ) || 0 === $revisions['count'] ) {
 785          return null;
 786      }
 787  
 788      return get_edit_post_link( $revisions['latest_id'] );
 789  }
 790  
 791  /**
 792   * Determines whether revisions are enabled for a given post.
 793   *
 794   * @since 3.6.0
 795   *
 796   * @param WP_Post $post The post object.
 797   * @return bool True if number of revisions to keep isn't zero, false otherwise.
 798   */
 799  function wp_revisions_enabled( $post ) {
 800      return wp_revisions_to_keep( $post ) !== 0;
 801  }
 802  
 803  /**
 804   * Determines how many revisions to retain for a given post.
 805   *
 806   * By default, an infinite number of revisions are kept.
 807   *
 808   * The constant WP_POST_REVISIONS can be set in wp-config to specify the limit
 809   * of revisions to keep.
 810   *
 811   * @since 3.6.0
 812   *
 813   * @param WP_Post $post The post object.
 814   * @return int The number of revisions to keep.
 815   */
 816  function wp_revisions_to_keep( $post ) {
 817      $num = WP_POST_REVISIONS;
 818  
 819      if ( true === $num ) {
 820          $num = -1;
 821      } else {
 822          $num = (int) $num;
 823      }
 824  
 825      if ( ! post_type_supports( $post->post_type, 'revisions' ) ) {
 826          $num = 0;
 827      }
 828  
 829      /**
 830       * Filters the number of revisions to save for the given post.
 831       *
 832       * Overrides the value of WP_POST_REVISIONS.
 833       *
 834       * @since 3.6.0
 835       *
 836       * @param int     $num  Number of revisions to store.
 837       * @param WP_Post $post Post object.
 838       */
 839      $num = apply_filters( 'wp_revisions_to_keep', $num, $post );
 840  
 841      /**
 842       * Filters the number of revisions to save for the given post by its post type.
 843       *
 844       * Overrides both the value of WP_POST_REVISIONS and the {@see 'wp_revisions_to_keep'} filter.
 845       *
 846       * The dynamic portion of the hook name, `$post->post_type`, refers to
 847       * the post type slug.
 848       *
 849       * Possible hook names include:
 850       *
 851       *  - `wp_post_revisions_to_keep`
 852       *  - `wp_page_revisions_to_keep`
 853       *
 854       * @since 5.8.0
 855       *
 856       * @param int     $num  Number of revisions to store.
 857       * @param WP_Post $post Post object.
 858       */
 859      $num = apply_filters( "wp_{$post->post_type}_revisions_to_keep", $num, $post );
 860  
 861      return (int) $num;
 862  }
 863  
 864  /**
 865   * Sets up the post object for preview based on the post autosave.
 866   *
 867   * @since 2.7.0
 868   * @access private
 869   *
 870   * @param WP_Post $post
 871   * @return WP_Post|false
 872   */
 873  function _set_preview( $post ) {
 874      if ( ! is_object( $post ) ) {
 875          return $post;
 876      }
 877  
 878      $preview = wp_get_post_autosave( $post->ID );
 879  
 880      if ( is_object( $preview ) ) {
 881          $preview = sanitize_post( $preview );
 882  
 883          $post->post_content = $preview->post_content;
 884          $post->post_title   = $preview->post_title;
 885          $post->post_excerpt = $preview->post_excerpt;
 886      }
 887  
 888      add_filter( 'get_the_terms', '_wp_preview_terms_filter', 10, 3 );
 889      add_filter( 'get_post_metadata', '_wp_preview_post_thumbnail_filter', 10, 3 );
 890      add_filter( 'get_post_metadata', '_wp_preview_meta_filter', 10, 4 );
 891  
 892      return $post;
 893  }
 894  
 895  /**
 896   * Filters the latest content for preview from the post autosave.
 897   *
 898   * @since 2.7.0
 899   * @access private
 900   */
 901  function _show_post_preview() {
 902      if ( isset( $_GET['preview_id'] ) && isset( $_GET['preview_nonce'] ) ) {
 903          $id = (int) $_GET['preview_id'];
 904  
 905          if ( false === wp_verify_nonce( $_GET['preview_nonce'], 'post_preview_' . $id ) ) {
 906              wp_die( __( 'Sorry, you are not allowed to preview drafts.' ), 403 );
 907          }
 908  
 909          add_filter( 'the_preview', '_set_preview' );
 910      }
 911  }
 912  
 913  /**
 914   * Filters terms lookup to set the post format.
 915   *
 916   * @since 3.6.0
 917   * @access private
 918   *
 919   * @param array  $terms
 920   * @param int    $post_id
 921   * @param string $taxonomy
 922   * @return array
 923   */
 924  function _wp_preview_terms_filter( $terms, $post_id, $taxonomy ) {
 925      $post = get_post();
 926  
 927      if ( ! $post ) {
 928          return $terms;
 929      }
 930  
 931      if ( empty( $_REQUEST['post_format'] ) || $post->ID !== $post_id
 932          || 'post_format' !== $taxonomy || 'revision' === $post->post_type
 933      ) {
 934          return $terms;
 935      }
 936  
 937      if ( 'standard' === $_REQUEST['post_format'] ) {
 938          $terms = array();
 939      } else {
 940          $term = get_term_by( 'slug', 'post-format-' . sanitize_key( $_REQUEST['post_format'] ), 'post_format' );
 941  
 942          if ( $term ) {
 943              $terms = array( $term ); // Can only have one post format.
 944          }
 945      }
 946  
 947      return $terms;
 948  }
 949  
 950  /**
 951   * Filters post thumbnail lookup to set the post thumbnail.
 952   *
 953   * @since 4.6.0
 954   * @access private
 955   *
 956   * @param null|array|string $value    The value to return - a single metadata value, or an array of values.
 957   * @param int               $post_id  Post ID.
 958   * @param string            $meta_key Meta key.
 959   * @return null|array The default return value or the post thumbnail meta array.
 960   */
 961  function _wp_preview_post_thumbnail_filter( $value, $post_id, $meta_key ) {
 962      $post = get_post();
 963  
 964      if ( ! $post ) {
 965          return $value;
 966      }
 967  
 968      if ( empty( $_REQUEST['_thumbnail_id'] ) || empty( $_REQUEST['preview_id'] )
 969          || $post->ID !== $post_id || $post_id !== (int) $_REQUEST['preview_id']
 970          || '_thumbnail_id' !== $meta_key || 'revision' === $post->post_type
 971      ) {
 972          return $value;
 973      }
 974  
 975      $thumbnail_id = (int) $_REQUEST['_thumbnail_id'];
 976  
 977      if ( $thumbnail_id <= 0 ) {
 978          return '';
 979      }
 980  
 981      return (string) $thumbnail_id;
 982  }
 983  
 984  /**
 985   * Gets the post revision version.
 986   *
 987   * @since 3.6.0
 988   * @access private
 989   *
 990   * @param WP_Post $revision
 991   * @return int|false
 992   */
 993  function _wp_get_post_revision_version( $revision ) {
 994      if ( is_object( $revision ) ) {
 995          $revision = get_object_vars( $revision );
 996      } elseif ( ! is_array( $revision ) ) {
 997          return false;
 998      }
 999  
1000      if ( preg_match( '/^\d+-(?:autosave|revision)-v(\d+)$/', $revision['post_name'], $matches ) ) {
1001          return (int) $matches[1];
1002      }
1003  
1004      return 0;
1005  }
1006  
1007  /**
1008   * Upgrades the revisions author, adds the current post as a revision and sets the revisions version to 1.
1009   *
1010   * @since 3.6.0
1011   * @access private
1012   *
1013   * @global wpdb $wpdb WordPress database abstraction object.
1014   *
1015   * @param WP_Post $post      Post object.
1016   * @param array   $revisions Current revisions of the post.
1017   * @return bool true if the revisions were upgraded, false if problems.
1018   */
1019  function _wp_upgrade_revisions_of_post( $post, $revisions ) {
1020      global $wpdb;
1021  
1022      // Add post option exclusively.
1023      $lock   = "revision-upgrade-{$post->ID}";
1024      $now    = time();
1025      $result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, 'off') /* LOCK */", $lock, $now ) );
1026  
1027      if ( ! $result ) {
1028          // If we couldn't get a lock, see how old the previous lock is.
1029          $locked = get_option( $lock );
1030  
1031          if ( ! $locked ) {
1032              /*
1033               * Can't write to the lock, and can't read the lock.
1034               * Something broken has happened.
1035               */
1036              return false;
1037          }
1038  
1039          if ( $locked > $now - HOUR_IN_SECONDS ) {
1040              // Lock is not too old: some other process may be upgrading this post. Bail.
1041              return false;
1042          }
1043  
1044          // Lock is too old - update it (below) and continue.
1045      }
1046  
1047      // If we could get a lock, re-"add" the option to fire all the correct filters.
1048      update_option( $lock, $now );
1049  
1050      reset( $revisions );
1051      $add_last = true;
1052  
1053      do {
1054          $this_revision = current( $revisions );
1055          $prev_revision = next( $revisions );
1056  
1057          $this_revision_version = _wp_get_post_revision_version( $this_revision );
1058  
1059          // Something terrible happened.
1060          if ( false === $this_revision_version ) {
1061              continue;
1062          }
1063  
1064          /*
1065           * 1 is the latest revision version, so we're already up to date.
1066           * No need to add a copy of the post as latest revision.
1067           */
1068          if ( 0 < $this_revision_version ) {
1069              $add_last = false;
1070              continue;
1071          }
1072  
1073          // Always update the revision version.
1074          $update = array(
1075              'post_name' => preg_replace( '/^(\d+-(?:autosave|revision))[\d-]*$/', '$1-v1', $this_revision->post_name ),
1076          );
1077  
1078          /*
1079           * If this revision is the oldest revision of the post, i.e. no $prev_revision,
1080           * the correct post_author is probably $post->post_author, but that's only a good guess.
1081           * Update the revision version only and Leave the author as-is.
1082           */
1083          if ( $prev_revision ) {
1084              $prev_revision_version = _wp_get_post_revision_version( $prev_revision );
1085  
1086              // If the previous revision is already up to date, it no longer has the information we need :(
1087              if ( $prev_revision_version < 1 ) {
1088                  $update['post_author'] = $prev_revision->post_author;
1089              }
1090          }
1091  
1092          // Upgrade this revision.
1093          $result = $wpdb->update( $wpdb->posts, $update, array( 'ID' => $this_revision->ID ) );
1094  
1095          if ( $result ) {
1096              wp_cache_delete( $this_revision->ID, 'posts' );
1097          }
1098      } while ( $prev_revision );
1099  
1100      delete_option( $lock );
1101  
1102      // Add a copy of the post as latest revision.
1103      if ( $add_last ) {
1104          wp_save_post_revision( $post->ID );
1105      }
1106  
1107      return true;
1108  }
1109  
1110  /**
1111   * Filters preview post meta retrieval to get values from the autosave.
1112   *
1113   * Filters revisioned meta keys only.
1114   *
1115   * @since 6.4.0
1116   *
1117   * @param mixed  $value     Meta value to filter.
1118   * @param int    $object_id Object ID.
1119   * @param string $meta_key  Meta key to filter a value for.
1120   * @param bool   $single    Whether to return a single value.
1121   * @return mixed Original meta value if the meta key isn't revisioned, the object doesn't exist,
1122   *               the post type is a revision or the post ID doesn't match the object ID.
1123   *               Otherwise, the revisioned meta value is returned for the preview.
1124   */
1125  function _wp_preview_meta_filter( $value, $object_id, $meta_key, $single ) {
1126      $post = get_post();
1127  
1128      if ( empty( $post )
1129          || $post->ID !== $object_id
1130          || ! in_array( $meta_key, wp_post_revision_meta_keys( $post->post_type ), true )
1131          || 'revision' === $post->post_type
1132      ) {
1133          return $value;
1134      }
1135  
1136      $preview = wp_get_post_autosave( $post->ID );
1137  
1138      if ( false === $preview ) {
1139          return $value;
1140      }
1141  
1142      return get_post_meta( $preview->ID, $meta_key, $single );
1143  }


Generated : Tue Oct 13 08:20:33 2026 Cross-referenced by PHPXref