: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* $children contains parent-children relations
* @see _page_traverse_name()
* @param int $page_id Page ID.
* @param array $children Parent-children relations (passed by reference).
* @param string[] $result Array of page names keyed by ID (passed by reference).
function _page_traverse_name( $page_id, &$children, &$result ) {
if ( isset( $children[ $page_id ] ) ) {
foreach ( (array) $children[ $page_id ] as $child ) {
$result[ $child->ID ] = $child->post_name;
_page_traverse_name( $child->ID, $children, $result );
* Builds the URI path for a page.
* Sub pages will be in the "directory" under the parent page post name.
* @since 4.6.0 The `$page` parameter was made optional.
* @param WP_Post|object|int $page Optional. Page ID or WP_Post object. Default is global $post.
* @return string|false Page URI, false on error.
function get_page_uri( $page = 0 ) {
if ( ! $page instanceof WP_Post ) {
$page = get_post( $page );
foreach ( $page->ancestors as $parent ) {
$parent = get_post( $parent );
if ( $parent && $parent->post_name ) {
$uri = $parent->post_name . '/' . $uri;
* Filters the URI for a page.
* @param string $uri Page URI.
* @param WP_Post $page Page object.
return apply_filters( 'get_page_uri', $uri, $page );
* Retrieves an array of pages (or hierarchical post type items).
* @since 6.3.0 Use WP_Query internally.
* @param array|string $args {
* Optional. Array or string of arguments to retrieve pages.
* @type int $child_of Page ID to return child and grandchild pages of. Note: The value
* of `$hierarchical` has no bearing on whether `$child_of` returns
* hierarchical results. Default 0, or no restriction.
* @type string $sort_order How to sort retrieved pages. Accepts 'ASC', 'DESC'. Default 'ASC'.
* @type string $sort_column What columns to sort pages by, comma-separated. Accepts 'post_author',
* 'post_date', 'post_title', 'post_name', 'post_modified', 'menu_order',
* 'post_modified_gmt', 'post_parent', 'ID', 'rand', 'comment_count'.
* 'post_' can be omitted for any values that start with it.
* @type bool $hierarchical Whether to return pages hierarchically. If false in conjunction with
* `$child_of` also being false, both arguments will be disregarded.
* @type int[] $exclude Array of page IDs to exclude. Default empty array.
* @type int[] $include Array of page IDs to include. Cannot be used with `$child_of`,
* `$parent`, `$exclude`, `$meta_key`, `$meta_value`, or `$hierarchical`.
* @type string $meta_key Only include pages with this meta key. Default empty.
* @type string $meta_value Only include pages with this meta value. Requires `$meta_key`.
* @type string $authors A comma-separated list of author IDs. Default empty.
* @type int $parent Page ID to return direct children of. Default -1, or no restriction.
* @type string|int[] $exclude_tree Comma-separated string or array of page IDs to exclude.
* @type int $number The number of pages to return. Default 0, or all pages.
* @type int $offset The number of pages to skip before returning. Requires `$number`.
* @type string $post_type The post type to query. Default 'page'.
* @type string|array $post_status A comma-separated list or array of post statuses to include.
* @return WP_Post[]|false Array of pages (or hierarchical post type items). Boolean false if the
* specified post type is not hierarchical or the specified status is not
* supported by the post type.
function get_pages( $args = array() ) {
'sort_column' => 'post_title',
'exclude_tree' => array(),
'post_status' => 'publish',
$parsed_args = wp_parse_args( $args, $defaults );
$number = (int) $parsed_args['number'];
$offset = (int) $parsed_args['offset'];
$child_of = (int) $parsed_args['child_of'];
$hierarchical = $parsed_args['hierarchical'];
$exclude = $parsed_args['exclude'];
$meta_key = $parsed_args['meta_key'];
$meta_value = $parsed_args['meta_value'];
$parent = $parsed_args['parent'];
$post_status = $parsed_args['post_status'];
// Make sure the post type is hierarchical.
$hierarchical_post_types = get_post_types( array( 'hierarchical' => true ) );
if ( ! in_array( $parsed_args['post_type'], $hierarchical_post_types, true ) ) {
if ( $parent > 0 && ! $child_of ) {
// Make sure we have a valid post status.
if ( ! is_array( $post_status ) ) {
$post_status = explode( ',', $post_status );
if ( array_diff( $post_status, get_post_stati() ) ) {
'orderby' => 'post_title',
'post__not_in' => wp_parse_id_list( $exclude ),
'meta_value' => $meta_value,
'post_type' => $parsed_args['post_type'],
'post_status' => $post_status,
'update_post_term_cache' => false,
'update_post_meta_cache' => false,
'ignore_sticky_posts' => true,
if ( ! empty( $parsed_args['include'] ) ) {
$child_of = 0; // Ignore child_of, parent, exclude, meta_key, and meta_value params if using include.
unset( $query_args['post__not_in'], $query_args['meta_key'], $query_args['meta_value'] );
$query_args['post__in'] = wp_parse_id_list( $parsed_args['include'] );
if ( ! empty( $parsed_args['authors'] ) ) {
$post_authors = wp_parse_list( $parsed_args['authors'] );
if ( ! empty( $post_authors ) ) {
$query_args['author__in'] = array();
foreach ( $post_authors as $post_author ) {
// Do we have an author id or an author login?
if ( 0 == (int) $post_author ) {
$post_author = get_user_by( 'login', $post_author );
if ( empty( $post_author ) ) {
if ( empty( $post_author->ID ) ) {
$post_author = $post_author->ID;
$query_args['author__in'][] = (int) $post_author;
if ( is_array( $parent ) ) {
$post_parent__in = array_map( 'absint', (array) $parent );
if ( ! empty( $post_parent__in ) ) {
$query_args['post_parent__in'] = $post_parent__in;
} elseif ( $parent >= 0 ) {
$query_args['post_parent'] = $parent;
* Maintain backward compatibility for `sort_column` key.
* Additionally to `WP_Query`, it has been supporting the `post_modified_gmt` field, so this logic will translate
* it to `post_modified` which should result in the same order given the two dates in the fields match.
$orderby = wp_parse_list( $parsed_args['sort_column'] );
static function ( $orderby_field ) {
$orderby_field = trim( $orderby_field );
if ( 'post_modified_gmt' === $orderby_field || 'modified_gmt' === $orderby_field ) {
$orderby_field = str_replace( '_gmt', '', $orderby_field );
$query_args['orderby'] = array_fill_keys( $orderby, $parsed_args['sort_order'] );
$order = $parsed_args['sort_order'];
$query_args['order'] = $order;
if ( ! empty( $number ) ) {
$query_args['posts_per_page'] = $number;
* Filters query arguments passed to WP_Query in get_pages.
* @param array $query_args Array of arguments passed to WP_Query.
* @param array $parsed_args Array of get_pages() arguments.
$query_args = apply_filters( 'get_pages_query_args', $query_args, $parsed_args );
$pages = $pages->query( $query_args );
if ( $child_of || $hierarchical ) {
$pages = get_page_children( $child_of, $pages );
if ( ! empty( $parsed_args['exclude_tree'] ) ) {
$exclude = wp_parse_id_list( $parsed_args['exclude_tree'] );
foreach ( $exclude as $id ) {
$children = get_page_children( $id, $pages );
foreach ( $children as $child ) {
$num_pages = count( $pages );
for ( $i = 0; $i < $num_pages; $i++ ) {
if ( in_array( $pages[ $i ]->ID, $exclude, true ) ) {
* Filters the retrieved list of pages.
* @param WP_Post[] $pages Array of page objects.
* @param array $parsed_args Array of get_pages() arguments.
return apply_filters( 'get_pages', $pages, $parsed_args );
* Determines whether an attachment URI is local and really an attachment.
* For more information on this and similar theme functions, check out
* the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
* Conditional Tags} article in the Theme Developer Handbook.
* @param string $url URL to check
* @return bool True on success, false on failure.
function is_local_attachment( $url ) {
if ( ! str_contains( $url, home_url() ) ) {
if ( str_contains( $url, home_url( '/?attachment_id=' ) ) ) {
$id = url_to_postid( $url );
if ( 'attachment' === $post->post_type ) {
* If you set the 'ID' in the $args parameter, it will mean that you are
* updating and attempt to update the attachment. You can also set the
* attachment name or title by setting the key 'post_name' or 'post_title'.
* You can set the dates for the attachment manually by setting the 'post_date'
* and 'post_date_gmt' keys' values.
* By default, the comments will use the default settings for whether the
* comments are allowed. You can close them manually or keep them open by
* setting the value for the 'comment_status' key.
* @since 4.7.0 Added the `$wp_error` parameter to allow a WP_Error to be returned on failure.
* @since 5.6.0 Added the `$fire_after_hooks` parameter.
* @param string|array $args Arguments for inserting an attachment.
* @param string|false $file Optional. Filename. Default false.
* @param int $parent_post_id Optional. Parent post ID or 0 for no parent. Default 0.
* @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 attachment ID on success. The value 0 or WP_Error on failure.
function wp_insert_attachment( $args, $file = false, $parent_post_id = 0, $wp_error = false, $fire_after_hooks = true ) {
$data = wp_parse_args( $args, $defaults );
if ( ! empty( $parent_post_id ) ) {
$data['post_parent'] = $parent_post_id;
$data['post_type'] = 'attachment';
return wp_insert_post( $data, $wp_error, $fire_after_hooks );
* Trashes or deletes an attachment.
* When an attachment is permanently deleted, the file will also be removed.
* Deletion removes all post meta fields, taxonomy, comments, etc. associated
* with the attachment (except the main post).
* The attachment is moved to the Trash instead of permanently deleted unless Trash
* for media is disabled, item is already in the Trash, or $force_delete is true.
* @global wpdb $wpdb WordPress database abstraction object.
* @param int $post_id Attachment ID.
* @param bool $force_delete Optional. Whether to bypass Trash and force deletion.
* @return WP_Post|false|null Post data on success, false or null on failure.
function wp_delete_attachment( $post_id, $force_delete = false ) {
$post = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE ID = %d", $post_id ) );
$post = get_post( $post );
if ( 'attachment' !== $post->post_type ) {
if ( ! $force_delete && EMPTY_TRASH_DAYS && MEDIA_TRASH && 'trash' !== $post->post_status ) {
return wp_trash_post( $post_id );
* Filters whether an attachment deletion should take place.
* @param WP_Post|false|null $delete Whether to go forward with deletion.
* @param WP_Post $post Post object.
* @param bool $force_delete Whether to bypass the Trash.
$check = apply_filters( 'pre_delete_attachment', null, $post, $force_delete );
delete_post_meta( $post_id, '_wp_trash_meta_status' );
delete_post_meta( $post_id, '_wp_trash_meta_time' );
$meta = wp_get_attachment_metadata( $post_id );
$backup_sizes = get_post_meta( $post->ID, '_wp_attachment_backup_sizes', true );
$file = get_attached_file( $post_id );
if ( is_multisite() && is_string( $file ) && ! empty( $file ) ) {
clean_dirsize_cache( $file );
* Fires before an attachment is deleted, at the start of wp_delete_attachment().
* @since 5.5.0 Added the `$post` parameter.
* @param int $post_id Attachment ID.
* @param WP_Post $post Post object.
do_action( 'delete_attachment', $post_id, $post );
wp_delete_object_term_relationships( $post_id, array( 'category', 'post_tag' ) );
wp_delete_object_term_relationships( $post_id, get_object_taxonomies( $post->post_type ) );
// Delete all for any posts.
delete_metadata( 'post', null, '_thumbnail_id', $post_id, true );
wp_defer_comment_counting( true );
$comment_ids = $wpdb->get_col( $wpdb->prepare( "SELECT comment_ID FROM $wpdb->comments WHERE comment_post_ID = %d ORDER BY comment_ID DESC", $post_id ) );
foreach ( $comment_ids as $comment_id ) {
wp_delete_comment( $comment_id, true );
wp_defer_comment_counting( false );
$post_meta_ids = $wpdb->get_col( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE post_id = %d ", $post_id ) );
foreach ( $post_meta_ids as $mid ) {
delete_metadata_by_mid( 'post', $mid );
/** This action is documented in wp-includes/post.php */
do_action( 'delete_post', $post_id, $post );
$result = $wpdb->delete( $wpdb->posts, array( 'ID' => $post_id ) );
/** This action is documented in wp-includes/post.php */
do_action( 'deleted_post', $post_id, $post );
wp_delete_attachment_files( $post_id, $meta, $backup_sizes, $file );
clean_post_cache( $post );
* Deletes all files that belong to the given attachment.
* @global wpdb $wpdb WordPress database abstraction object.
* @param int $post_id Attachment ID.
* @param array $meta The attachment's meta data.
* @param array $backup_sizes The meta data for the attachment's backup images.
* @param string $file Absolute path to the attachment's file.
* @return bool True on success, false on failure.
function wp_delete_attachment_files( $post_id, $meta, $backup_sizes, $file ) {
$uploadpath = wp_get_upload_dir();
if ( ! empty( $meta['thumb'] ) ) {
// Don't delete the thumb if another attachment uses it.
if ( ! $wpdb->get_row( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE meta_key = '_wp_attachment_metadata' AND meta_value LIKE %s AND post_id <> %d", '%' . $wpdb->esc_like( $meta['thumb'] ) . '%', $post_id ) ) ) {
$thumbfile = str_replace( wp_basename( $file ), $meta['thumb'], $file );
if ( ! empty( $thumbfile ) ) {
$thumbfile = path_join( $uploadpath['basedir'], $thumbfile );
$thumbdir = path_join( $uploadpath['basedir'], dirname( $file ) );
if ( ! wp_delete_file_from_directory( $thumbfile, $thumbdir ) ) {