function wp_untrash_post_comments( $post = null ) {
$post = get_post( $post );
$statuses = get_post_meta( $post_id, '_wp_trash_meta_comments_status', true );
* Fires before comments are restored for a post from the Trash.
* @param int $post_id Post ID.
do_action( 'untrash_post_comments', $post_id );
// Restore each comment to its original status.
$group_by_status = array();
foreach ( $statuses as $comment_id => $comment_status ) {
$group_by_status[ $comment_status ][] = $comment_id;
foreach ( $group_by_status as $status => $comments ) {
// Sanity check. This shouldn't happen.
if ( 'post-trashed' === $status ) {
$comments_in = implode( ', ', array_map( 'intval', $comments ) );
$wpdb->query( $wpdb->prepare( "UPDATE $wpdb->comments SET comment_approved = %s WHERE comment_ID IN ($comments_in)", $status ) );
clean_comment_cache( array_keys( $statuses ) );
delete_post_meta( $post_id, '_wp_trash_meta_comments_status' );
* Fires after comments are restored for a post from the Trash.
* @param int $post_id Post ID.
do_action( 'untrashed_post_comments', $post_id );
* Retrieve the list of categories for a post.
* Compatibility layer for themes and plugins. Also an easy layer of abstraction
* away from the complexity of the taxonomy layer.
* @see wp_get_object_terms()
* @param int $post_id Optional. The Post ID. Does not default to the ID of the
* global $post. Default 0.
* @param array $args Optional. Category query parameters. Default empty array.
* See WP_Term_Query::__construct() for supported arguments.
* @return array|WP_Error List of categories. If the `$fields` argument passed via `$args` is 'all' or
* 'all_with_object_id', an array of WP_Term objects will be returned. If `$fields`
* is 'ids', an array of category IDs. If `$fields` is 'names', an array of category names.
* WP_Error object if 'category' taxonomy doesn't exist.
function wp_get_post_categories( $post_id = 0, $args = array() ) {
$post_id = (int) $post_id;
$defaults = array( 'fields' => 'ids' );
$args = wp_parse_args( $args, $defaults );
$cats = wp_get_object_terms( $post_id, 'category', $args );
* Retrieve the tags for a post.
* There is only one default for this function, called 'fields' and by default
* is set to 'all'. There are other defaults that can be overridden in
* @param int $post_id Optional. The Post ID. Does not default to the ID of the
* global $post. Default 0.
* @param array $args Optional. Tag query parameters. Default empty array.
* See WP_Term_Query::__construct() for supported arguments.
* @return array|WP_Error Array of WP_Term objects on success or empty array if no tags were found.
* WP_Error object if 'post_tag' taxonomy doesn't exist.
function wp_get_post_tags( $post_id = 0, $args = array() ) {
return wp_get_post_terms( $post_id, 'post_tag', $args );
* Retrieves the terms for a post.
* @param int $post_id Optional. The Post ID. Does not default to the ID of the
* global $post. Default 0.
* @param string|string[] $taxonomy Optional. The taxonomy slug or array of slugs for which
* to retrieve terms. Default 'post_tag'.
* Optional. Term query parameters. See WP_Term_Query::__construct() for supported arguments.
* @type string $fields Term fields to retrieve. Default 'all'.
* @return array|WP_Error Array of WP_Term objects on success or empty array if no terms were found.
* WP_Error object if `$taxonomy` doesn't exist.
function wp_get_post_terms( $post_id = 0, $taxonomy = 'post_tag', $args = array() ) {
$post_id = (int) $post_id;
$defaults = array( 'fields' => 'all' );
$args = wp_parse_args( $args, $defaults );
$tags = wp_get_object_terms( $post_id, $taxonomy, $args );
* Retrieve a number of recent posts.
* @param array $args Optional. Arguments to retrieve posts. Default empty array.
* @param string $output Optional. The required return type. One of OBJECT or ARRAY_A, which
* correspond to a WP_Post object or an associative array, respectively.
* @return array|false Array of recent posts, where the type of each element is determined
* by the `$output` parameter. Empty array on failure.
function wp_get_recent_posts( $args = array(), $output = ARRAY_A ) {
if ( is_numeric( $args ) ) {
_deprecated_argument( __FUNCTION__, '3.1.0', __( 'Passing an integer number of posts is deprecated. Pass an array of arguments instead.' ) );
$args = array( 'numberposts' => absint( $args ) );
// Set default arguments.
'orderby' => 'post_date',
'post_status' => 'draft, publish, future, pending, private',
'suppress_filters' => true,
$parsed_args = wp_parse_args( $args, $defaults );
$results = get_posts( $parsed_args );
// Backward compatibility. Prior to 3.1 expected posts to be returned in array.
if ( ARRAY_A == $output ) {
foreach ( $results as $key => $result ) {
$results[ $key ] = get_object_vars( $result );
return $results ? $results : array();
return $results ? $results : false;
* Insert or update a post.
* If the $postarr parameter has 'ID' set to a value, then post will be updated.
* You can set the post date manually, by setting the values for 'post_date'
* and 'post_date_gmt' keys. You can close the comments or open the comments by
* setting the value for 'comment_status' key.
* @since 2.6.0 Added the `$wp_error` parameter to allow a WP_Error to be returned on failure.
* @since 4.2.0 Support was added for encoding emoji in the post title, content, and excerpt.
* @since 4.4.0 A 'meta_input' array can now be passed to `$postarr` to add post meta data.
* @since 5.6.0 Added the `$fire_after_hooks` parameter.
* @global wpdb $wpdb WordPress database abstraction object.
* @param array $postarr {
* An array of elements that make up a post to update or insert.
* @type int $ID The post ID. If equal to something other than 0,
* the post with that ID will be updated. Default 0.
* @type int $post_author The ID of the user who added the post. Default is
* @type string $post_date The date of the post. Default is the current time.
* @type string $post_date_gmt The date of the post in the GMT timezone. Default is
* the value of `$post_date`.
* @type mixed $post_content The post content. Default empty.
* @type string $post_content_filtered The filtered post content. Default empty.
* @type string $post_title The post title. Default empty.
* @type string $post_excerpt The post excerpt. Default empty.
* @type string $post_status The post status. Default 'draft'.
* @type string $post_type The post type. Default 'post'.
* @type string $comment_status Whether the post can accept comments. Accepts 'open' or 'closed'.
* Default is the value of 'default_comment_status' option.
* @type string $ping_status Whether the post can accept pings. Accepts 'open' or 'closed'.
* Default is the value of 'default_ping_status' option.
* @type string $post_password The password to access the post. Default empty.
* @type string $post_name The post name. Default is the sanitized post title
* when creating a new post.
* @type string $to_ping Space or carriage return-separated list of URLs to ping.
* @type string $pinged Space or carriage return-separated list of URLs that have
* been pinged. Default empty.
* @type string $post_modified The date when the post was last modified. Default is
* @type string $post_modified_gmt The date when the post was last modified in the GMT
* timezone. Default is the current time.
* @type int $post_parent Set this for the post it belongs to, if any. Default 0.
* @type int $menu_order The order the post should be displayed in. Default 0.
* @type string $post_mime_type The mime type of the post. Default empty.
* @type string $guid Global Unique ID for referencing the post. Default empty.
* @type int[] $post_category Array of category IDs.
* Defaults to value of the 'default_category' option.
* @type array $tags_input Array of tag names, slugs, or IDs. Default empty.
* @type array $tax_input Array of taxonomy terms keyed by their taxonomy name. Default empty.
* @type array $meta_input Array of post meta values keyed by their post meta key. Default empty.
* @param bool $wp_error Optional. Whether to return a WP_Error on failure. Default false.
* @param bool $fire_after_hooks Optional. Whether to fire the after insert hooks. Default true.
* @return int|WP_Error The post ID on success. The value 0 or WP_Error on failure.
function wp_insert_post( $postarr, $wp_error = false, $fire_after_hooks = true ) {
// Capture original pre-sanitized array for passing into filters.
$unsanitized_postarr = $postarr;
$user_id = get_current_user_id();
'post_author' => $user_id,
'post_content_filtered' => '',
'post_status' => 'draft',
$postarr = wp_parse_args( $postarr, $defaults );
unset( $postarr['filter'] );
$postarr = sanitize_post( $postarr, 'db' );
// Are we updating or creating?
$guid = $postarr['guid'];
if ( ! empty( $postarr['ID'] ) ) {
// Get the post ID and GUID.
$post_ID = $postarr['ID'];
$post_before = get_post( $post_ID );
if ( is_null( $post_before ) ) {
return new WP_Error( 'invalid_post', __( 'Invalid post ID.' ) );
$guid = get_post_field( 'guid', $post_ID );
$previous_status = get_post_field( 'post_status', $post_ID );
$previous_status = 'new';
$post_type = empty( $postarr['post_type'] ) ? 'post' : $postarr['post_type'];
$post_title = $postarr['post_title'];
$post_content = $postarr['post_content'];
$post_excerpt = $postarr['post_excerpt'];
if ( isset( $postarr['post_name'] ) ) {
$post_name = $postarr['post_name'];
// For an update, don't modify the post_name if it wasn't supplied as an argument.
$post_name = $post_before->post_name;
$maybe_empty = 'attachment' !== $post_type
&& ! $post_content && ! $post_title && ! $post_excerpt
&& post_type_supports( $post_type, 'editor' )
&& post_type_supports( $post_type, 'title' )
&& post_type_supports( $post_type, 'excerpt' );
* Filters whether the post should be considered "empty".
* The post is considered "empty" if both:
* 1. The post type supports the title, editor, and excerpt fields
* 2. The title, editor, and excerpt fields are all empty
* Returning a truthy value from the filter will effectively short-circuit
* the new post being inserted and return 0. If $wp_error is true, a WP_Error
* will be returned instead.
* @param bool $maybe_empty Whether the post should be considered "empty".
* @param array $postarr Array of post data.
if ( apply_filters( 'wp_insert_post_empty_content', $maybe_empty, $postarr ) ) {
return new WP_Error( 'empty_content', __( 'Content, title, and excerpt are empty.' ) );
$post_status = empty( $postarr['post_status'] ) ? 'draft' : $postarr['post_status'];
if ( 'attachment' === $post_type && ! in_array( $post_status, array( 'inherit', 'private', 'trash', 'auto-draft' ), true ) ) {
$post_status = 'inherit';
if ( ! empty( $postarr['post_category'] ) ) {
// Filter out empty terms.
$post_category = array_filter( $postarr['post_category'] );
// Make sure we set a valid category.
if ( empty( $post_category ) || 0 === count( $post_category ) || ! is_array( $post_category ) ) {
// 'post' requires at least one category.
if ( 'post' === $post_type && 'auto-draft' !== $post_status ) {
$post_category = array( get_option( 'default_category' ) );
$post_category = array();
* Don't allow contributors to set the post slug for pending review posts.
* For new posts check the primitive capability, for updates check the meta capability.
$post_type_object = get_post_type_object( $post_type );
if ( ! $update && 'pending' === $post_status && ! current_user_can( $post_type_object->cap->publish_posts ) ) {
} elseif ( $update && 'pending' === $post_status && ! current_user_can( 'publish_post', $post_ID ) ) {
* Create a valid post name. Drafts and pending posts are allowed to have
if ( empty( $post_name ) ) {
if ( ! in_array( $post_status, array( 'draft', 'pending', 'auto-draft' ), true ) ) {
$post_name = sanitize_title( $post_title );
// On updates, we need to check to see if it's using the old, fixed sanitization context.
$check_name = sanitize_title( $post_name, '', 'old-save' );
if ( $update && strtolower( urlencode( $post_name ) ) == $check_name && get_post_field( 'post_name', $post_ID ) == $check_name ) {
$post_name = $check_name;
} else { // new post, or slug has changed.
$post_name = sanitize_title( $post_name );
* Resolve the post date from any provided post date or post date GMT strings;
* if none are provided, the date will be set to now.
$post_date = wp_resolve_post_date( $postarr['post_date'], $postarr['post_date_gmt'] );
return new WP_Error( 'invalid_date', __( 'Invalid date.' ) );
if ( empty( $postarr['post_date_gmt'] ) || '0000-00-00 00:00:00' === $postarr['post_date_gmt'] ) {
if ( ! in_array( $post_status, get_post_stati( array( 'date_floating' => true ) ), true ) ) {
$post_date_gmt = get_gmt_from_date( $post_date );
$post_date_gmt = '0000-00-00 00:00:00';
$post_date_gmt = $postarr['post_date_gmt'];
if ( $update || '0000-00-00 00:00:00' === $post_date ) {
$post_modified = current_time( 'mysql' );
$post_modified_gmt = current_time( 'mysql', 1 );
$post_modified = $post_date;
$post_modified_gmt = $post_date_gmt;
if ( 'attachment' !== $post_type ) {
$now = gmdate( 'Y-m-d H:i:s' );
if ( 'publish' === $post_status ) {
if ( strtotime( $post_date_gmt ) - strtotime( $now ) >= MINUTE_IN_SECONDS ) {
} elseif ( 'future' === $post_status ) {
if ( strtotime( $post_date_gmt ) - strtotime( $now ) < MINUTE_IN_SECONDS ) {
$post_status = 'publish';
if ( empty( $postarr['comment_status'] ) ) {
$comment_status = 'closed';
$comment_status = get_default_comment_status( $post_type );
$comment_status = $postarr['comment_status'];
// These variables are needed by compact() later.
$post_content_filtered = $postarr['post_content_filtered'];
$post_author = isset( $postarr['post_author'] ) ? $postarr['post_author'] : $user_id;
$ping_status = empty( $postarr['ping_status'] ) ? get_default_comment_status( $post_type, 'pingback' ) : $postarr['ping_status'];
$to_ping = isset( $postarr['to_ping'] ) ? sanitize_trackback_urls( $postarr['to_ping'] ) : '';
$pinged = isset( $postarr['pinged'] ) ? $postarr['pinged'] : '';
$import_id = isset( $postarr['import_id'] ) ? $postarr['import_id'] : 0;
* The 'wp_insert_post_parent' filter expects all variables to be present.
* Previously, these variables would have already been extracted
if ( isset( $postarr['menu_order'] ) ) {
$menu_order = (int) $postarr['menu_order'];
$post_password = isset( $postarr['post_password'] ) ? $postarr['post_password'] : '';
if ( 'private' === $post_status ) {
if ( isset( $postarr['post_parent'] ) ) {
$post_parent = (int) $postarr['post_parent'];
$new_postarr = array_merge(
compact( array_diff( array_keys( $defaults ), array( 'context', 'filter' ) ) )