} } $this->queried_object = get_userdata( $this->queried_object_id ); } return $this->queried_object; } /** * Retrieves the ID of the currently queried object. * * @since 1.5.0 * * @return int */ public function get_queried_object_id() { $this->get_queried_object(); return $this->queried_object_id ?? 0; } /** * Constructor. * * Sets up the WordPress query, if parameter is not empty. * * @since 1.5.0 * * @see WP_Query::parse_query() for all available arguments. * * @param string|array $query URL query string or array of vars. */ public function __construct( $query = '' ) { if ( ! empty( $query ) ) { $this->query( $query ); } } /** * Makes private properties readable for backward compatibility. * * @since 4.0.0 * * @param string $name Property to get. * @return mixed Property. */ public function __get( $name ) { if ( in_array( $name, $this->compat_fields, true ) ) { return $this->$name; } } /** * Makes private properties checkable for backward compatibility. * * @since 4.0.0 * * @param string $name Property to check if set. * @return bool Whether the property is set. */ public function __isset( $name ) { if ( in_array( $name, $this->compat_fields, true ) ) { return isset( $this->$name ); } return false; } /** * Makes private/protected methods readable for backward compatibility. * * @since 4.0.0 * * @param string $name Method to call. * @param array $arguments Arguments to pass when calling. * @return mixed|false Return value of the callback, false otherwise. */ public function __call( $name, $arguments ) { if ( in_array( $name, $this->compat_methods, true ) ) { return $this->$name( ...$arguments ); } return false; } /** * Determines whether the query is for an existing archive page. * * Archive pages include category, tag, author, date, custom post type, * and custom taxonomy based archives. * * @since 3.1.0 * * @see WP_Query::is_category() * @see WP_Query::is_tag() * @see WP_Query::is_author() * @see WP_Query::is_date() * @see WP_Query::is_post_type_archive() * @see WP_Query::is_tax() * * @return bool Whether the query is for an existing archive page. */ public function is_archive() { return (bool) $this->is_archive; } /** * Determines whether the query is for an existing post type archive page. * * @since 3.1.0 * * @param string|string[] $post_types Optional. Post type or array of posts types * to check against. Default empty. * @return bool Whether the query is for an existing post type archive page. */ public function is_post_type_archive( $post_types = '' ) { if ( empty( $post_types ) || ! $this->is_post_type_archive ) { return (bool) $this->is_post_type_archive; } $post_type = $this->get( 'post_type' ); if ( is_array( $post_type ) ) { $post_type = reset( $post_type ); } $post_type_object = get_post_type_object( $post_type ); if ( ! $post_type_object ) { return false; } return in_array( $post_type_object->name, (array) $post_types, true ); } /** * Determines whether the query is for an existing attachment page. * * @since 3.1.0 * * @param int|string|int[]|string[] $attachment Optional. Attachment ID, title, slug, or array of such * to check against. Default empty. * @return bool Whether the query is for an existing attachment page. */ public function is_attachment( $attachment = '' ) { if ( ! $this->is_attachment ) { return false; } if ( empty( $attachment ) ) { return true; } $attachment = array_map( 'strval', (array) $attachment ); $post_obj = $this->get_queried_object(); if ( ! $post_obj ) { return false; } if ( in_array( (string) $post_obj->ID, $attachment, true ) ) { return true; } elseif ( in_array( $post_obj->post_title, $attachment, true ) ) { return true; } elseif ( in_array( $post_obj->post_name, $attachment, true ) ) { return true; } return false; } /** * Determines whether the query is for an existing author archive page. * * If the $author parameter is specified, this function will additionally * check if the query is for one of the authors specified. * * @since 3.1.0 * * @param int|string|int[]|string[] $author Optional. User ID, nickname, nicename, or array of such * to check against. Default empty. * @return bool Whether the query is for an existing author archive page. */ public function is_author( $author = '' ) { if ( ! $this->is_author ) { return false; } if ( empty( $author ) ) { return true; } $author_obj = $this->get_queried_object(); if ( ! $author_obj ) { return false; } $author = array_map( 'strval', (array) $author ); if ( in_array( (string) $author_obj->ID, $author, true ) ) { return true; } elseif ( in_array( $author_obj->nickname, $author, true ) ) { return true; } elseif ( in_array( $author_obj->user_nicename, $author, true ) ) { return true; } return false; } /** * Determines whether the query is for an existing category archive page. * * If the $category parameter is specified, this function will additionally * check if the query is for one of the categories specified. * * @since 3.1.0 * * @param int|string|int[]|string[] $category Optional. Category ID, name, slug, or array of such * to check against. Default empty. * @return bool Whether the query is for an existing category archive page. */ public function is_category( $category = '' ) { if ( ! $this->is_category ) { return false; } if ( empty( $category ) ) { return true; } $cat_obj = $this->get_queried_object(); if ( ! $cat_obj ) { return false; } $category = array_map( 'strval', (array) $category ); if ( in_array( (string) $cat_obj->term_id, $category, true ) ) { return true; } elseif ( in_array( $cat_obj->name, $category, true ) ) { return true; } elseif ( in_array( $cat_obj->slug, $category, true ) ) { return true; } return false; } /** * Determines whether the query is for an existing tag archive page. * * If the $tag parameter is specified, this function will additionally * check if the query is for one of the tags specified. * * @since 3.1.0 * * @param int|string|int[]|string[] $tag Optional. Tag ID, name, slug, or array of such * to check against. Default empty. * @return bool Whether the query is for an existing tag archive page. */ public function is_tag( $tag = '' ) { if ( ! $this->is_tag ) { return false; } if ( empty( $tag ) ) { return true; } $tag_obj = $this->get_queried_object(); if ( ! $tag_obj ) { return false; } $tag = array_map( 'strval', (array) $tag ); if ( in_array( (string) $tag_obj->term_id, $tag, true ) ) { return true; } elseif ( in_array( $tag_obj->name, $tag, true ) ) { return true; } elseif ( in_array( $tag_obj->slug, $tag, true ) ) { return true; } return false; } /** * Determines whether the query is for an existing custom taxonomy archive page. * * If the $taxonomy parameter is specified, this function will additionally * check if the query is for that specific $taxonomy. * * If the $term parameter is specified in addition to the $taxonomy parameter, * this function will additionally check if the query is for one of the terms * specified. * * @since 3.1.0 * * @global WP_Taxonomy[] $wp_taxonomies Registered taxonomies. * * @param string|string[] $taxonomy Optional. Taxonomy slug or slugs to check against. * Default empty. * @param int|string|int[]|string[] $term Optional. Term ID, name, slug, or array of such * to check against. Default empty. * @return bool Whether the query is for an existing custom taxonomy archive page. * True for custom taxonomy archive pages, false for built-in taxonomies * (category and tag archives). */ public function is_tax( $taxonomy = '', $term = '' ) { global $wp_taxonomies; if ( ! $this->is_tax ) { return false; } if ( empty( $taxonomy ) ) { return true; } $queried_object = $this->get_queried_object(); $tax_array = array_intersect( array_keys( $wp_taxonomies ), (array) $taxonomy ); $term_array = (array) $term; // Check that the taxonomy matches. if ( ! ( isset( $queried_object->taxonomy ) && count( $tax_array ) && in_array( $queried_object->taxonomy, $tax_array, true ) ) ) { return false; } // Only a taxonomy provided. if ( empty( $term ) ) { return true; } return isset( $queried_object->term_id ) && count( array_intersect( array( $queried_object->term_id, $queried_object->name, $queried_object->slug ), $term_array ) ); } /** * Determines whether the current URL is within the comments popup window. * * @since 3.1.0 * @deprecated 4.5.0 * * @return false Always returns false. */ public function is_comments_popup() { _deprecated_function( __FUNCTION__, '4.5.0' ); return false; } /** * Determines whether the query is for an existing date archive. * * @since 3.1.0 * * @return bool Whether the query is for an existing date archive. */ public function is_date() { return (bool) $this->is_date; } /** * Determines whether the query is for an existing day archive. * * @since 3.1.0 * * @return bool Whether the query is for an existing day archive. */ public function is_day() { return (bool) $this->is_day; } /** * Determines whether the query is for a feed. * * @since 3.1.0 * * @param string|string[] $feeds Optional. Feed type or array of feed types * to check against. Default empty. * @return bool Whether the query is for a feed. */ public function is_feed( $feeds = '' ) { if ( empty( $feeds ) || ! $this->is_feed ) { return (bool) $this->is_feed; } $query_var = $this->get( 'feed' ); if ( 'feed' === $query_var ) { $query_var = get_default_feed(); } return in_array( $query_var, (array) $feeds, true ); } /** * Determines whether the query is for a comments feed. * * @since 3.1.0 * * @return bool Whether the query is for a comments feed. */ public function is_comment_feed() { return (bool) $this->is_comment_feed; } /** * Determines whether the query is for the front page of the site. * * This is for what is displayed at your site's main URL. * * Depends on the site's "Front page displays" Reading Settings 'show_on_front' and 'page_on_front'. * * If you set a static page for the front page of your site, this function will return * true when viewing that page. * * Otherwise the same as {@see WP_Query::is_home()}. * * @since 3.1.0 * * @return bool Whether the query is for the front page of the site. */ public function is_front_page() { // Most likely case. if ( 'posts' === get_option( 'show_on_front' ) && $this->is_home() ) { return true; } elseif ( 'page' === get_option( 'show_on_front' ) && get_option( 'page_on_front' ) && $this->is_page( get_option( 'page_on_front' ) ) ) { return true; } else { return false; } } /** * Determines whether the query is for the blog homepage. * * This is the page which shows the time based blog content of your site. * * Depends on the site's "Front page displays" Reading Settings 'show_on_front' and 'page_for_posts'. * * If you set a static page for the front page of your site, this function will return * true only on the page you set as the "Posts page". * * @since 3.1.0 * * @see WP_Query::is_front_page() * * @return bool Whether the query is for the blog homepage. */ public function is_home() { return (bool) $this->is_home; } /** * Determines whether the query is for the Privacy Policy page. * * This is the page which shows the Privacy Policy content of your site. * * Depends on the site's "Change your Privacy Policy page" Privacy Settings 'wp_page_for_privacy_policy'. * * This function will return true only on the page you set as the "Privacy Policy page". * * @since 5.2.0 * * @return bool Whether the query is for the Privacy Policy page. */ public function is_privacy_policy() { if ( get_option( 'wp_page_for_privacy_policy' ) && $this->is_page( get_option( 'wp_page_for_privacy_policy' ) ) ) { return true; } else { return false; } } /** * Determines whether the query is for an existing month archive. * * @since 3.1.0 * * @return bool Whether the query is for an existing month archive. */ public function is_month() { return (bool) $this->is_month; } /** * Determines whether the query is for an existing single page. * * If the $page parameter is specified, this function will additionally * check if the query is for one of the pages specified. * * @since 3.1.0 * * @see WP_Query::is_single() * @see WP_Query::is_singular() * * @param int|string|int[]|string[] $page Optional. Page ID, title, slug, path, or array of such * to check against. Default empty. * @return bool Whether the query is for an existing single page. */ public function is_page( $page = '' ) { if ( ! $this->is_page ) { return false; } if ( empty( $page ) ) { return true; } $page_obj = $this->get_queried_object(); if ( ! $page_obj ) { return false; } $page = array_map( 'strval', (array) $page ); if ( in_array( (string) $page_obj->ID, $page, true ) ) { return true; } elseif ( in_array( $page_obj->post_title, $page, true ) ) { return true; } elseif ( in_array( $page_obj->post_name, $page, true ) ) { return true; } else { foreach ( $page as $pagepath ) { if ( ! strpos( $pagepath, '/' ) ) { continue; } $pagepath_obj = get_page_by_path( $pagepath ); if ( $pagepath_obj && ( $pagepath_obj->ID === $page_obj->ID ) ) { return true; } } } return false; } /** * Determines whether the query is for a paged result and not for the first page. * * @since 3.1.0 * * @return bool Whether the query is for a paged result. */ public function is_paged() { return (bool) $this->is_paged; } /** * Determines whether the query is for a post or page preview. * * @since 3.1.0 * * @return bool Whether the query is for a post or page preview. */ public function is_preview() { return (bool) $this->is_preview; } /** * Determines whether the query is for the robots.txt file. * * @since 3.1.0 * * @return bool Whether the query is for the robots.txt file. */ public function is_robots() { return (bool) $this->is_robots; } /** * Determines whether the query is for the favicon.ico file. * * @since 5.4.0 * * @return bool Whether the query is for the favicon.ico file. */ public function is_favicon() { return (bool) $this->is_favicon; } /** * Determines whether the query is for a search. * * @since 3.1.0 * * @return bool Whether the query is for a search. */ public function is_search() { return (bool) $this->is_search; } /** * Determines whether the query is for an existing single post. * * Works for any post type excluding pages. * * If the $post parameter is specified, this function will additionally * check if the query is for one of the Posts specified. * * @since 3.1.0 * * @see WP_Query::is_page() * @see WP_Query::is_singular() * * @param int|string|int[]|string[] $post Optional. Post ID, title, slug, path, or array of such * to check against. Default empty. * @return bool Whether the query is for an existing single post. */ public function is_single( $post = '' ) { if ( ! $this->is_single ) { return false; } if ( empty( $post ) ) { return true; } $post_obj = $this->get_queried_object(); if ( ! $post_obj ) { return false; } $post = array_map( 'strval', (array) $post ); if ( in_array( (string) $post_obj->ID, $post, true ) ) { return true; } elseif ( in_array( $post_obj->post_title, $post, true ) ) { return true; } elseif ( in_array( $post_obj->post_name, $post, true ) ) { return true; } else { foreach ( $post as $postpath ) { if ( ! strpos( $postpath, '/' ) ) { continue; } $postpath_obj = get_page_by_path( $postpath, OBJECT, $post_obj->post_type ); if ( $postpath_obj && ( $postpath_obj->ID === $post_obj->ID ) ) { return true; } } } return false; } /** * Determines whether the query is for an existing single post of any post type * (post, attachment, page, custom post types). * * If the $post_types parameter is specified, this function will additionally * check if the query is for one of the Posts Types specified. * * @since 3.1.0 * * @see WP_Query::is_page() * @see WP_Query::is_single() * * @param string|string[] $post_types Optional. Post type or array of post types * to check against. Default empty. * @return bool Whether the query is for an existing single post * or any of the given post types. */ public function is_singular( $post_types = '' ) { if ( empty( $post_types ) || ! $this->is_singular ) { return (bool) $this->is_singular; } $post_obj = $this->get_queried_object(); if ( ! $post_obj ) { return false; } return in_array( $post_obj->post_type, (array) $post_types, true ); } /** * Determines whether the query is for a specific time. * * @since 3.1.0 * * @return bool Whether the query is for a specific time. */ public function is_time() { return (bool) $this->is_time; } /** * Determines whether the query is for a trackback endpoint call. * * @since 3.1.0 * * @return bool Whether the query is for a trackback endpoint call. */ public function is_trackback() { return (bool) $this->is_trackback; } /** * Determines whether the query is for an existing year archive. * * @since 3.1.0 * * @return bool Whether the query is for an existing year archive. */ public function is_year() { return (bool) $this->is_year; } /** * Determines whether the query is a 404 (returns no results). * * @since 3.1.0 * * @return bool Whether the query is a 404 error. */ public function is_404() { return (bool) $this->is_404; } /** * Determines whether the query is for an embedded post. * * @since 4.4.0 * * @return bool Whether the query is for an embedded post. */ public function is_embed() { return (bool) $this->is_embed; } /** * Determines whether the query is the main query. * * @since 3.3.0 * * @global WP_Query $wp_the_query WordPress Query object. * * @return bool Whether the query is the main query. */ public function is_main_query() { global $wp_the_query; return $wp_the_query === $this; } /** * Sets up global post data. * * @since 4.1.0 * @since 4.4.0 Added the ability to pass a post ID to `$post`. * * @global int $id * @global WP_User $authordata * @global string $currentday * @global string $currentmonth * @global int $page * @global array $pages * @global int $multipage * @global int $more * @global int $numpages * * @param WP_Post|object|int $post WP_Post instance or Post ID/object. * @return bool True on success, false on failure. */ public function setup_postdata( $post ) { global $id, $authordata, $currentday, $currentmonth, $page, $pages, $multipage, $more, $numpages; if ( ! ( $post instanceof WP_Post ) ) { $post = get_post( $post ); } if ( ! $post ) { return false; } $elements = $this->generate_postdata( $post ); if ( false === $elements ) { return false; } $id = $elements['id']; $authordata = $elements['authordata']; $currentday = $elements['currentday']; $currentmonth = $elements['currentmonth']; $page = $elements['page']; $pages = $elements['pages']; $multipage = $elements['multipage']; $more = $elements['more']; $numpages = $elements['numpages']; /** * Fires once the post data has been set up. * * @since 2.8.0 * @since 4.1.0 Introduced `$query` parameter. * * @param WP_Post $post The Post object (passed by reference). * @param WP_Query $query The current Query object (passed by reference). */ do_action_ref_array( 'the_post', array( &$post, &$this ) ); return true; } /** * Generates post data. * * @since 5.2.0 * * @param WP_Post|object|int $post WP_Post instance or Post ID/object. * @return array|false Elements of post or false on failure. */ public function generate_postdata( $post ) { if ( ! ( $post instanceof WP_Post ) ) { $post = get_post( $post ); } if ( ! $post ) { return false; } $id = (int) $post->ID; $authordata = get_userdata( $post->post_author ); $currentday = false; $currentmonth = false; $post_date = $post->post_date; if ( ! empty( $post_date ) && '0000-00-00 00:00:00' !== $post_date ) { // Avoid using mysql2date for performance reasons. $currentmonth = substr( $post_date, 5, 2 ); $day = substr( $post_date, 8, 2 ); $year = substr( $post_date, 2, 2 ); $currentday = sprintf( '%s.%s.%s', $day, $currentmonth, $year ); } $numpages = 1; $multipage = 0; $page = $this->get( 'page' ); if ( ! $page ) { $page = 1; } /* * Force full post content when viewing the permalink for the $post, * or when on an RSS feed. Otherwise respect the 'more' tag. */ if ( get_queried_object_id() === $post->ID && ( $this->is_page() || $this->is_single() ) ) { $more = 1; } elseif ( $this->is_feed() ) { $more = 1; } else { $more = 0; } $content = $post->post_content; if ( str_contains( $content, '' ) ) { $content = str_replace( "\n\n", '', $content ); $content = str_replace( "\n", '', $content ); $content = str_replace( "\n", '', $content ); // Remove the nextpage block delimiters, to avoid invalid block structures in the split content. $content = str_replace( '', '', $content ); $content = str_replace( '', '', $content ); // Ignore nextpage at the beginning of the content. if ( str_starts_with( $content, '' ) ) { $content = substr( $content, 15 ); } $pages = explode( '', $content ); } else { $pages = array( $post->post_content ); } /** * Filters the "pages" derived from splitting the post content. * * "Pages" are determined by splitting the post content based on the presence * of `` tags. * * @since 4.4.0 * * @param string[] $pages Array of "pages" from the post content split by `` tags. * @param WP_Post $post Current post object. */ $pages = apply_filters( 'content_pagination', $pages, $post ); $numpages = count( $pages ); if ( $numpages > 1 ) { if ( $page > 1 ) { $more = 1; } $multipage = 1; } else { $multipage = 0; } $elements = compact( 'id', 'authordata', 'currentday', 'currentmonth', 'page', 'pages', 'multipage', 'more', 'numpages' ); return $elements; } /** * Generates cache key. * * @since 6.1.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param array $args Query arguments. * @param string $sql SQL statement. * @return string Cache key. */ protected function generate_cache_key( array $args, $sql ) { global $wpdb; unset( $args['cache_results'], $args['fields'], $args['lazy_load_term_meta'], $args['update_post_meta_cache'], $args['update_post_term_cache'], $args['update_menu_item_cache'], $args['suppress_filters'] ); if ( empty( $args['post_type'] ) ) { if ( $this->is_attachment ) { $args['post_type'] = 'attachment'; } elseif ( $this->is_page ) { $args['post_type'] = 'page'; } else { $args['post_type'] = 'post'; } } elseif ( 'any' === $args['post_type'] ) { $args['post_type'] = array_values( get_post_types( array( 'exclude_from_search' => false ) ) ); } $args['post_type'] = (array) $args['post_type']; // Sort post types to ensure same cache key generation. sort( $args['post_type'] ); /* * Sort arrays that can be used for ordering prior to cache key generation. * * These arrays are sorted in the query generator for the purposes of the * WHERE clause but the arguments are not modified as they can be used for * the orderby clause. * * Their use in the orderby clause will generate a different SQL query so * they can be sorted for the cache key generation. */ $sortable_arrays_with_int_values = array( 'post__in', 'post_parent__in', ); foreach ( $sortable_arrays_with_int_values as $key ) { if ( isset( $args[ $key ] ) && is_array( $args[ $key ] ) ) { $args[ $key ] = array_unique( array_map( 'absint', $args[ $key ] ) ); sort( $args[ $key ] ); } } // Sort and unique the 'post_name__in' for cache key generation. if ( isset( $args['post_name__in'] ) && is_array( $args['post_name__in'] ) ) { $args['post_name__in'] = array_unique( $args['post_name__in'] ); sort( $args['post_name__in'] ); } if ( isset( $args['post_status'] ) ) { $args['post_status'] = (array) $args['post_status']; // Sort post status to ensure same cache key generation. sort( $args['post_status'] ); } // Add a default orderby value of date to ensure same cache key generation. if ( ! isset( $args['orderby'] ) ) { $args['orderby'] = 'date'; } $placeholder = $wpdb->placeholder_escape(); array_walk_recursive( $args, /* * Replace wpdb placeholders with the string used in the database * query to avoid unreachable cache keys. This is necessary because * the placeholder is randomly generated in each request. * * $value is passed by reference to allow it to be modified. * array_walk_recursive() does not return an array. */ static function ( &$value ) use ( $wpdb, $placeholder ) { if ( is_string( $value ) && str_contains( $value, $placeholder ) ) { $value = $wpdb->remove_placeholder_escape( $value ); } } ); ksort( $args ); // Replace wpdb placeholder in the SQL statement used by the cache key. $sql = $wpdb->remove_placeholder_escape( $sql ); $key = md5( serialize( $args ) . $sql ); $this->query_cache_key = "wp_query:$key"; return $this->query_cache_key; } /** * After looping through a nested query, this function * restores the $post global to the current post in this query. * * @since 3.7.0 * * @global WP_Post $post Global post object. */ public function reset_postdata() { if ( ! empty( $this->post ) ) { $GLOBALS['post'] = $this->post; $this->setup_postdata( $this->post ); } } /** * Lazyloads term meta for posts in the loop. * * @since 4.4.0 * @deprecated 4.5.0 See wp_queue_posts_for_term_meta_lazyload(). * * @param mixed $check * @param int $term_id * @return mixed */ public function lazyload_term_meta( $check, $term_id ) { _deprecated_function( __METHOD__, '4.5.0' ); return $check; } /** * Lazyloads comment meta for comments in the loop. * * @since 4.4.0 * @deprecated 4.5.0 See wp_lazyload_comment_meta(). * * @param mixed $check * @param int $comment_id * @return mixed */ public function lazyload_comment_meta( $check, $comment_id ) { _deprecated_function( __METHOD__, '4.5.0' ); return $check; } } a bookmark field. * * Sanitizes the bookmark fields based on what the field name is. If the field * has a strict value set, then it will be tested for that, else a more generic * filtering is applied. After the more strict filter is applied, if the `$context` * is 'raw' then the value is immediately return. * * Hooks exist for the more generic cases. With the 'edit' context, the {@see 'edit_$field'} * filter will be called and passed the `$value` and `$bookmark_id` respectively. * * With the 'db' context, the {@see 'pre_$field'} filter is called and passed the value. * The 'display' context is the final context and has the `$field` has the filter name * and is passed the `$value`, `$bookmark_id`, and `$context`, respectively. * * @since 2.3.0 * * @param string $field The bookmark field. * @param mixed $value The bookmark field value. * @param int $bookmark_id Bookmark ID. * @param string $context How to filter the field value. Accepts 'raw', 'edit', 'db', * 'display', 'attribute', or 'js'. Default 'display'. * @return mixed The filtered value. */ function sanitize_bookmark_field( $field, $value, $bookmark_id, $context ) { $int_fields = array( 'link_id', 'link_rating' ); if ( in_array( $field, $int_fields, true ) ) { $value = (int) $value; } switch ( $field ) { case 'link_category': // array( ints ) $value = array_map( 'absint', (array) $value ); /* * We return here so that the categories aren't filtered. * The 'link_category' filter is for the name of a link category, not an array of a link's link categories. */ return $value; case 'link_visible': // bool stored as Y|N $value = preg_replace( '/[^YNyn]/', '', $value ); break; case 'link_target': // "enum" $targets = array( '_top', '_blank' ); if ( ! in_array( $value, $targets, true ) ) { $value = ''; } break; } if ( 'raw' === $context ) { return $value; } if ( 'edit' === $context ) { /** This filter is documented in wp-includes/post.php */ $value = apply_filters( "edit_{$field}", $value, $bookmark_id ); if ( 'link_notes' === $field ) { $value = esc_html( $value ); // textarea_escaped } else { $value = esc_attr( $value ); } } elseif ( 'db' === $context ) { /** This filter is documented in wp-includes/post.php */ $value = apply_filters( "pre_{$field}", $value ); } else { /** This filter is documented in wp-includes/post.php */ $value = apply_filters( "{$field}", $value, $bookmark_id, $context ); if ( 'attribute' === $context ) { $value = esc_attr( $value ); } elseif ( 'js' === $context ) { $value = esc_js( $value ); } } // Restore the type for integer fields after esc_attr(). if ( in_array( $field, $int_fields, true ) ) { $value = (int) $value; } return $value; } /** * Deletes the bookmark cache. * * @since 2.7.0 * * @param int $bookmark_id Bookmark ID. */ function clean_bookmark_cache( $bookmark_id ) { wp_cache_delete( $bookmark_id, 'bookmark' ); wp_cache_delete( 'get_bookmarks', 'bookmark' ); clean_object_term_cache( $bookmark_id, 'link' ); } otherwise. * * @since 6.5.0 * * @param array $metadata Block metadata. * @param string $field_name Field name to pick from metadata. * @param int $index Optional. Index of the script module ID to register when multiple * items passed. Default 0. * @return string|false Script module ID or false on failure. */ function register_block_script_module_id( $metadata, $field_name, $index = 0 ) { if ( empty( $metadata[ $field_name ] ) ) { return false; } $module_id = $metadata[ $field_name ]; if ( is_array( $module_id ) ) { if ( empty( $module_id[ $index ] ) ) { return false; } $module_id = $module_id[ $index ]; } $module_path = remove_block_asset_path_prefix( $module_id ); if ( $module_id === $module_path ) { return $module_id; } $path = dirname( $metadata['file'] ); $module_asset_raw_path = $path . '/' . substr_replace( $module_path, '.asset.php', - strlen( '.js' ) ); $module_id = generate_block_asset_handle( $metadata['name'], $field_name, $index ); $module_asset_path = wp_normalize_path( realpath( $module_asset_raw_path ) ); $module_path_norm = wp_normalize_path( realpath( $path . '/' . $module_path ) ); $module_uri = get_block_asset_url( $module_path_norm ); $module_asset = ! empty( $module_asset_path ) ? require $module_asset_path : array(); $module_dependencies = $module_asset['dependencies'] ?? array(); $block_version = $metadata['version'] ?? false; $module_version = $module_asset['version'] ?? $block_version; $supports_interactivity_true = isset( $metadata['supports']['interactivity'] ) && true === $metadata['supports']['interactivity']; $is_interactive = $supports_interactivity_true || ( isset( $metadata['supports']['interactivity']['interactive'] ) && true === $metadata['supports']['interactivity']['interactive'] ); $supports_client_navigation = $supports_interactivity_true || ( isset( $metadata['supports']['interactivity']['clientNavigation'] ) && true === $metadata['supports']['interactivity']['clientNavigation'] ); $args = array(); // Blocks using the Interactivity API are server-side rendered, so they are // by design not in the critical rendering path and should be deprioritized. if ( $is_interactive ) { $args['fetchpriority'] = 'low'; $args['in_footer'] = true; } // Blocks using the Interactivity API that support client-side navigation // must be marked as such in their script modules. if ( $is_interactive && $supports_client_navigation ) { wp_interactivity()->add_client_navigation_support_to_script_module( $module_id ); } wp_register_script_module( $module_id, $module_uri, $module_dependencies, $module_version, $args ); return $module_id; } /** * Finds a script handle for the selected block metadata field. It detects * when a path to file was provided and optionally finds a corresponding asset * file with details necessary to register the script under automatically * generated handle name. It returns unprocessed script handle otherwise. * * @since 5.5.0 * @since 6.1.0 Added `$index` parameter. * @since 6.5.0 The asset file is optional. Added script handle support in the asset file. * * @param array $metadata Block metadata. * @param string $field_name Field name to pick from metadata. * @param int $index Optional. Index of the script to register when multiple items passed. * Default 0. * @return string|false Script handle provided directly or created through * script's registration, or false on failure. */ function register_block_script_handle( $metadata, $field_name, $index = 0 ) { if ( empty( $metadata[ $field_name ] ) ) { return false; } $script_handle_or_path = $metadata[ $field_name ]; if ( is_array( $script_handle_or_path ) ) { if ( empty( $script_handle_or_path[ $index ] ) ) { return false; } $script_handle_or_path = $script_handle_or_path[ $index ]; } $script_path = remove_block_asset_path_prefix( $script_handle_or_path ); if ( $script_handle_or_path === $script_path ) { return $script_handle_or_path; } $path = dirname( $metadata['file'] ); $script_asset_raw_path = $path . '/' . substr_replace( $script_path, '.asset.php', - strlen( '.js' ) ); $script_asset_path = wp_normalize_path( realpath( $script_asset_raw_path ) ); // Asset file for blocks is optional. See https://core.trac.wordpress.org/ticket/60460. $script_asset = ! empty( $script_asset_path ) ? require $script_asset_path : array(); $script_handle = $script_asset['handle'] ?? generate_block_asset_handle( $metadata['name'], $field_name, $index ); if ( wp_script_is( $script_handle, 'registered' ) ) { return $script_handle; } $script_path_norm = wp_normalize_path( realpath( $path . '/' . $script_path ) ); $script_uri = get_block_asset_url( $script_path_norm ); $script_dependencies = $script_asset['dependencies'] ?? array(); $block_version = $metadata['version'] ?? false; $script_version = $script_asset['version'] ?? $block_version; $script_args = array(); if ( 'viewScript' === $field_name && $script_uri ) { $script_args['strategy'] = 'defer'; } $result = wp_register_script( $script_handle, $script_uri, $script_dependencies, $script_version, $script_args ); if ( ! $result ) { return false; } if ( ! empty( $metadata['textdomain'] ) && in_array( 'wp-i18n', $script_dependencies, true ) ) { wp_set_script_translations( $script_handle, $metadata['textdomain'] ); } return $script_handle; } /** * Finds a style handle for the block metadata field. It detects when a path * to file was provided and registers the style under automatically * generated handle name. It returns unprocessed style handle otherwise. * * @since 5.5.0 * @since 6.1.0 Added `$index` parameter. * * @param array $metadata Block metadata. * @param string $field_name Field name to pick from metadata. * @param int $index Optional. Index of the style to register when multiple items passed. * Default 0. * @return string|false Style handle provided directly or created through * style's registration, or false on failure. */ function register_block_style_handle( $metadata, $field_name, $index = 0 ) { if ( empty( $metadata[ $field_name ] ) ) { return false; } $style_handle = $metadata[ $field_name ]; if ( is_array( $style_handle ) ) { if ( empty( $style_handle[ $index ] ) ) { return false; } $style_handle = $style_handle[ $index ]; } $style_handle_name = generate_block_asset_handle( $metadata['name'], $field_name, $index ); // If the style handle is already registered, skip re-registering. if ( wp_style_is( $style_handle_name, 'registered' ) ) { return $style_handle_name; } static $wpinc_path_norm = ''; if ( ! $wpinc_path_norm ) { $wpinc_path_norm = wp_normalize_path( realpath( ABSPATH . WPINC ) ); } $is_core_block = isset( $metadata['file'] ) && str_starts_with( $metadata['file'], $wpinc_path_norm ); // Skip registering individual styles for each core block when a bundled version provided. if ( $is_core_block && ! wp_should_load_separate_core_block_assets() ) { return false; } $style_path = remove_block_asset_path_prefix( $style_handle ); $is_style_handle = $style_handle === $style_path; // Allow only passing style handles for core blocks. if ( $is_core_block && ! $is_style_handle ) { return false; } // Return the style handle unless it's the first item for every core block that requires special treatment. if ( $is_style_handle && ! ( $is_core_block && 0 === $index ) ) { return $style_handle; } // Check whether styles should have a ".min" suffix or not. $suffix = SCRIPT_DEBUG ? '' : '.min'; if ( $is_core_block ) { $style_path = ( 'editorStyle' === $field_name ) ? "editor{$suffix}.css" : "style{$suffix}.css"; } $style_path_norm = wp_normalize_path( realpath( dirname( $metadata['file'] ) . '/' . $style_path ) ); $style_uri = get_block_asset_url( $style_path_norm ); $block_version = ! $is_core_block && isset( $metadata['version'] ) ? $metadata['version'] : false; $version = $style_path_norm && defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? filemtime( $style_path_norm ) : $block_version; $result = wp_register_style( $style_handle_name, $style_uri, array(), $version ); if ( ! $result ) { return false; } if ( $style_uri ) { wp_style_add_data( $style_handle_name, 'path', $style_path_norm ); if ( $is_core_block ) { $rtl_file = str_replace( "{$suffix}.css", "-rtl{$suffix}.css", $style_path_norm ); } else { $rtl_file = str_replace( '.css', '-rtl.css', $style_path_norm ); } if ( is_rtl() && file_exists( $rtl_file ) ) { wp_style_add_data( $style_handle_name, 'rtl', 'replace' ); wp_style_add_data( $style_handle_name, 'suffix', $suffix ); wp_style_add_data( $style_handle_name, 'path', $rtl_file ); } } return $style_handle_name; } /** * Gets i18n schema for block's metadata read from `block.json` file. * * @since 5.9.0 * * @return object The schema for block's metadata. */ function get_block_metadata_i18n_schema() { static $i18n_block_schema; if ( ! isset( $i18n_block_schema ) ) { $i18n_block_schema = wp_json_file_decode( __DIR__ . '/block-i18n.json' ); } return $i18n_block_schema; } /** * Registers all block types from a block metadata collection. * * This can either reference a previously registered metadata collection or, if the `$manifest` parameter is provided, * register the metadata collection directly within the same function call. * * @since 6.8.0 * @see wp_register_block_metadata_collection() * @see register_block_type_from_metadata() * * @param string $path The absolute base path for the collection ( e.g., WP_PLUGIN_DIR . '/my-plugin/blocks/' ). * @param string $manifest Optional. The absolute path to the manifest file containing the metadata collection, in * order to register the collection. If this parameter is not provided, the `$path` parameter * must reference a previously registered block metadata collection. */ function wp_register_block_types_from_metadata_collection( $path, $manifest = '' ) { if ( $manifest ) { wp_register_block_metadata_collection( $path, $manifest ); } $block_metadata_files = WP_Block_Metadata_Registry::get_collection_block_metadata_files( $path ); foreach ( $block_metadata_files as $block_metadata_file ) { register_block_type_from_metadata( $block_metadata_file ); } } /** * Registers a block metadata collection. * * This function allows core and third-party plugins to register their block metadata * collections in a centralized location. Registering collections can improve performance * by avoiding multiple reads from the filesystem and parsing JSON. * * @since 6.7.0 * * @param string $path The base path in which block files for the collection reside. * @param string $manifest The path to the manifest file for the collection. */ function wp_register_block_metadata_collection( $path, $manifest ) { WP_Block_Metadata_Registry::register_collection( $path, $manifest ); } /** * Registers a block type from the metadata stored in the `block.json` file. * * @since 5.5.0 * @since 5.7.0 Added support for `textdomain` field and i18n handling for all translatable fields. * @since 5.9.0 Added support for `variations` and `viewScript` fields. * @since 6.1.0 Added support for `render` field. * @since 6.3.0 Added `selectors` field. * @since 6.4.0 Added support for `blockHooks` field. * @since 6.5.0 Added support for `allowedBlocks`, `viewScriptModule`, and `viewStyle` fields. * @since 6.7.0 Allow PHP filename as `variations` argument. * * @param string $file_or_folder Path to the JSON file with metadata definition for * the block or path to the folder where the `block.json` file is located. * If providing the path to a JSON file, the filename must end with `block.json`. * @param array $args Optional. Array of block type arguments. Accepts any public property * of `WP_Block_Type`. See WP_Block_Type::__construct() for information * on accepted arguments. Default empty array. * @return WP_Block_Type|false The registered block type on success, or false on failure. */ function register_block_type_from_metadata( $file_or_folder, $args = array() ) { /* * Get an array of metadata from a PHP file. * This improves performance for core blocks as it's only necessary to read a single PHP file * instead of reading a JSON file per-block, and then decoding from JSON to PHP. * Using a static variable ensures that the metadata is only read once per request. */ $file_or_folder = wp_normalize_path( $file_or_folder ); $metadata_file = ( ! str_ends_with( $file_or_folder, 'block.json' ) ) ? trailingslashit( $file_or_folder ) . 'block.json' : $file_or_folder; $is_core_block = str_starts_with( $file_or_folder, wp_normalize_path( ABSPATH . WPINC ) ); $metadata_file_exists = $is_core_block || file_exists( $metadata_file ); $registry_metadata = WP_Block_Metadata_Registry::get_metadata( $file_or_folder ); if ( $registry_metadata ) { $metadata = $registry_metadata; } elseif ( $metadata_file_exists ) { $metadata = wp_json_file_decode( $metadata_file, array( 'associative' => true ) ); } else { $metadata = array(); } if ( ! is_array( $metadata ) || ( empty( $metadata['name'] ) && empty( $args['name'] ) ) ) { return false; } $metadata['file'] = $metadata_file_exists ? wp_normalize_path( realpath( $metadata_file ) ) : null; /** * Filters the metadata provided for registering a block type. * * @since 5.7.0 * * @param array $metadata Metadata for registering a block type. */ $metadata = apply_filters( 'block_type_metadata', $metadata ); // Add `style` and `editor_style` for core blocks if missing. if ( ! empty( $metadata['name'] ) && str_starts_with( $metadata['name'], 'core/' ) ) { $block_name = str_replace( 'core/', '', $metadata['name'] ); if ( ! isset( $metadata['style'] ) ) { $metadata['style'] = "wp-block-$block_name"; } if ( current_theme_supports( 'wp-block-styles' ) && wp_should_load_separate_core_block_assets() ) { $metadata['style'] = (array) $metadata['style']; $metadata['style'][] = "wp-block-{$block_name}-theme"; } if ( ! isset( $metadata['editorStyle'] ) ) { $metadata['editorStyle'] = "wp-block-{$block_name}-editor"; } } $settings = array(); $property_mappings = array( 'apiVersion' => 'api_version', 'name' => 'name', 'title' => 'title', 'category' => 'category', 'parent' => 'parent', 'ancestor' => 'ancestor', 'icon' => 'icon', 'description' => 'description', 'keywords' => 'keywords', 'attributes' => 'attributes', 'providesContext' => 'provides_context', 'usesContext' => 'uses_context', 'selectors' => 'selectors', 'supports' => 'supports', 'styles' => 'styles', 'variations' => 'variations', 'example' => 'example', 'allowedBlocks' => 'allowed_blocks', ); $textdomain = ! empty( $metadata['textdomain'] ) ? $metadata['textdomain'] : null; $i18n_schema = get_block_metadata_i18n_schema(); foreach ( $property_mappings as $key => $mapped_key ) { if ( isset( $metadata[ $key ] ) ) { $settings[ $mapped_key ] = $metadata[ $key ]; if ( $metadata_file_exists && $textdomain && isset( $i18n_schema->$key ) ) { $settings[ $mapped_key ] = translate_settings_using_i18n_schema( $i18n_schema->$key, $settings[ $key ], $textdomain ); } } } if ( ! empty( $metadata['render'] ) ) { $template_path = wp_normalize_path( realpath( dirname( $metadata['file'] ) . '/' . remove_block_asset_path_prefix( $metadata['render'] ) ) ); if ( $template_path ) { /** * Renders the block on the server. * * @since 6.1.0 * * @param array $attributes Block attributes. * @param string $content Block default content. * @param WP_Block $block Block instance. * * @return string Returns the block content. */ $settings['render_callback'] = static function ( $attributes, $content, $block ) use ( $template_path ) { ob_start(); require $template_path; return ob_get_clean(); }; } } // If `variations` is a string, it's the name of a PHP file that // generates the variations. if ( ! empty( $metadata['variations'] ) && is_string( $metadata['variations'] ) ) { $variations_path = wp_normalize_path( realpath( dirname( $metadata['file'] ) . '/' . remove_block_asset_path_prefix( $metadata['variations'] ) ) ); if ( $variations_path ) { /** * Generates the list of block variations. * * @since 6.7.0 * * @return string Returns the list of block variations. */ $settings['variation_callback'] = static function () use ( $variations_path ) { $variations = require $variations_path; return $variations; }; // The block instance's `variations` field is only allowed to be an array // (of known block variations). We unset it so that the block instance will // provide a getter that returns the result of the `variation_callback` instead. unset( $settings['variations'] ); } } $settings = array_merge( $settings, $args ); $script_fields = array( 'editorScript' => 'editor_script_handles', 'script' => 'script_handles', 'viewScript' => 'view_script_handles', ); foreach ( $script_fields as $metadata_field_name => $settings_field_name ) { if ( ! empty( $settings[ $metadata_field_name ] ) ) { $metadata[ $metadata_field_name ] = $settings[ $metadata_field_name ]; } if ( ! empty( $metadata[ $metadata_field_name ] ) ) { $scripts = $metadata[ $metadata_field_name ]; $processed_scripts = array(); if ( is_array( $scripts ) ) { for ( $index = 0; $index < count( $scripts ); $index++ ) { $result = register_block_script_handle( $metadata, $metadata_field_name, $index ); if ( $result ) { $processed_scripts[] = $result; } } } else { $result = register_block_script_handle( $metadata, $metadata_field_name ); if ( $result ) { $processed_scripts[] = $result; } } $settings[ $settings_field_name ] = $processed_scripts; } } $module_fields = array( 'viewScriptModule' => 'view_script_module_ids', ); foreach ( $module_fields as $metadata_field_name => $settings_field_name ) { if ( ! empty( $settings[ $metadata_field_name ] ) ) { $metadata[ $metadata_field_name ] = $settings[ $metadata_field_name ]; } if ( ! empty( $metadata[ $metadata_field_name ] ) ) { $modules = $metadata[ $metadata_field_name ]; $processed_modules = array(); if ( is_array( $modules ) ) { for ( $index = 0; $index < count( $modules ); $index++ ) { $result = register_block_script_module_id( $metadata, $metadata_field_name, $index ); if ( $result ) { $processed_modules[] = $result; } } } else { $result = register_block_script_module_id( $metadata, $metadata_field_name ); if ( $result ) { $processed_modules[] = $result; } } $settings[ $settings_field_name ] = $processed_modules; } } $style_fields = array( 'editorStyle' => 'editor_style_handles', 'style' => 'style_handles', 'viewStyle' => 'view_style_handles', ); foreach ( $style_fields as $metadata_field_name => $settings_field_name ) { if ( ! empty( $settings[ $metadata_field_name ] ) ) { $metadata[ $metadata_field_name ] = $settings[ $metadata_field_name ]; } if ( ! empty( $metadata[ $metadata_field_name ] ) ) { $styles = $metadata[ $metadata_field_name ]; $processed_styles = array(); if ( is_array( $styles ) ) { for ( $index = 0; $index < count( $styles ); $index++ ) { $result = register_block_style_handle( $metadata, $metadata_field_name, $index ); if ( $result ) { $processed_styles[] = $result; } } } else { $result = register_block_style_handle( $metadata, $metadata_field_name ); if ( $result ) { $processed_styles[] = $result; } } $settings[ $settings_field_name ] = $processed_styles; } } if ( ! empty( $metadata['blockHooks'] ) ) { /** * Map camelCased position string (from block.json) to snake_cased block type position. * * @var array */ $position_mappings = array( 'before' => 'before', 'after' => 'after', 'firstChild' => 'first_child', 'lastChild' => 'last_child', ); $settings['block_hooks'] = array(); foreach ( $metadata['blockHooks'] as $anchor_block_name => $position ) { // Avoid infinite recursion (hooking to itself). if ( $metadata['name'] === $anchor_block_name ) { _doing_it_wrong( __METHOD__, __( 'Cannot hook block to itself.' ), '6.4.0' ); continue; } if ( ! isset( $position_mappings[ $position ] ) ) { continue; } $settings['block_hooks'][ $anchor_block_name ] = $position_mappings[ $position ]; } } /** * Filters the settings determined from the block type metadata. * * @since 5.7.0 * * @param array $settings Array of determined settings for registering a block type. * @param array $metadata Metadata provided for registering a block type. */ $settings = apply_filters( 'block_type_metadata_settings', $settings, $metadata ); $metadata['name'] = ! empty( $settings['name'] ) ? $settings['name'] : $metadata['name']; return WP_Block_Type_Registry::get_instance()->register( $metadata['name'], $settings ); } /** * Registers a block type. The recommended way is to register a block type using * the metadata stored in the `block.json` file. * * @since 5.0.0 * @since 5.8.0 First parameter now accepts a path to the `block.json` file. * * @param string|WP_Block_Type $block_type Block type name including namespace, or alternatively * a path to the JSON file with metadata definition for the block, * or a path to the folder where the `block.json` file is located, * or a complete WP_Block_Type instance. * In case a WP_Block_Type is provided, the $args parameter will be ignored. * @param array $args Optional. Array of block type arguments. Accepts any public property * of `WP_Block_Type`. See WP_Block_Type::__construct() for information * on accepted arguments. Default empty array. * * @return WP_Block_Type|false The registered block type on success, or false on failure. */ function register_block_type( $block_type, $args = array() ) { if ( is_string( $block_type ) && file_exists( $block_type ) ) { return register_block_type_from_metadata( $block_type, $args ); } return WP_Block_Type_Registry::get_instance()->register( $block_type, $args ); } /** * Unregisters a block type. * * @since 5.0.0 * * @param string|WP_Block_Type $name Block type name including namespace, or alternatively * a complete WP_Block_Type instance. * @return WP_Block_Type|false The unregistered block type on success, or false on failure. */ function unregister_block_type( $name ) { return WP_Block_Type_Registry::get_instance()->unregister( $name ); } /** * Determines whether a post or content string has blocks. * * This test optimizes for performance rather than strict accuracy, detecting * the pattern of a block but not validating its structure. For strict accuracy, * you should use the block parser on post content. * * @since 5.0.0 * * @see parse_blocks() * * @param int|string|WP_Post|null $post Optional. Post content, post ID, or post object. * Defaults to global $post. * @return bool Whether the post has blocks. */ function has_blocks( $post = null ) { if ( ! is_string( $post ) ) { $wp_post = get_post( $post ); if ( ! $wp_post instanceof WP_Post ) { return false; } $post = $wp_post->post_content; } return str_contains( (string) $post, '' ) + strlen( '-->' ); $end = strrpos( $serialized_block, '' ) + strlen( '-->' ); $end = strrpos( $serialized_block, '', $serialized_block_name, $serialized_attributes ); } return sprintf( '%s', $serialized_block_name, $serialized_attributes, $block_content, $serialized_block_name ); } /** * Returns the content of a block, including comment delimiters, serializing all * attributes from the given parsed block. * * This should be used when preparing a block to be saved to post content. * Prefer `render_block` when preparing a block for display. Unlike * `render_block`, this does not evaluate a block's `render_callback`, and will * instead preserve the markup as parsed. * * @since 5.3.1 * * @param array $block { * An associative array of a single parsed block object. See WP_Block_Parser_Block. * * @type string|null $blockName Name of block. * @type array $attrs Attributes from block comment delimiters. * @type array[] $innerBlocks List of inner blocks. An array of arrays that * have the same structure as this one. * @type string $innerHTML HTML from inside block comment delimiters. * @type array $innerContent List of string fragments and null markers where * inner blocks were found. * } * @return string String of rendered HTML. */ function serialize_block( $block ) { $block_content = ''; $index = 0; foreach ( $block['innerContent'] as $chunk ) { $block_content .= is_string( $chunk ) ? $chunk : serialize_block( $block['innerBlocks'][ $index++ ] ); } if ( ! is_array( $block['attrs'] ) ) { $block['attrs'] = array(); } return get_comment_delimited_block_content( $block['blockName'], $block['attrs'], $block_content ); } /** * Returns a joined string of the aggregate serialization of the given * parsed blocks. * * @since 5.3.1 * * @param array[] $blocks { * Array of block structures. * * @type array ...$0 { * An associative array of a single parsed block object. See WP_Block_Parser_Block. * * @type string|null $blockName Name of block. * @type array $attrs Attributes from block comment delimiters. * @type array[] $innerBlocks List of inner blocks. An array of arrays that * have the same structure as this one. * @type string $innerHTML HTML from inside block comment delimiters. * @type array $innerContent List of string fragments and null markers where * inner blocks were found. * } * } * @return string String of rendered HTML. */ function serialize_blocks( $blocks ) { return implode( '', array_map( 'serialize_block', $blocks ) ); } /** * Traverses a parsed block tree and applies callbacks before and after serializing it. * * Recursively traverses the block and its inner blocks and applies the two callbacks provided as * arguments, the first one before serializing the block, and the second one after serializing it. * If either callback returns a string value, it will be prepended and appended to the serialized * block markup, respectively. * * The callbacks will receive a reference to the current block as their first argument, so that they * can also modify it, and the current block's parent block as second argument. Finally, the * `$pre_callback` receives the previous block, whereas the `$post_callback` receives * the next block as third argument. * * Serialized blocks are returned including comment delimiters, and with all attributes serialized. * * This function should be used when there is a need to modify the saved block, or to inject markup * into the return value. Prefer `serialize_block` when preparing a block to be saved to post content. * * This function is meant for internal use only. * * @since 6.4.0 * @access private * * @see serialize_block() * * @param array $block An associative array of a single parsed block object. See WP_Block_Parser_Block. * @param callable $pre_callback Callback to run on each block in the tree before it is traversed and serialized. * It is called with the following arguments: &$block, $parent_block, $previous_block. * Its string return value will be prepended to the serialized block markup. * @param callable $post_callback Callback to run on each block in the tree after it is traversed and serialized. * It is called with the following arguments: &$block, $parent_block, $next_block. * Its string return value will be appended to the serialized block markup. * @return string Serialized block markup. */ function traverse_and_serialize_block( $block, $pre_callback = null, $post_callback = null ) { $block_content = ''; $block_index = 0; foreach ( $block['innerContent'] as $chunk ) { if ( is_string( $chunk ) ) { $block_content .= $chunk; } else { $inner_block = $block['innerBlocks'][ $block_index ]; if ( is_callable( $pre_callback ) ) { $prev = 0 === $block_index ? null : $block['innerBlocks'][ $block_index - 1 ]; $block_content .= call_user_func_array( $pre_callback, array( &$inner_block, &$block, $prev ) ); } if ( is_callable( $post_callback ) ) { $next = count( $block['innerBlocks'] ) - 1 === $block_index ? null : $block['innerBlocks'][ $block_index + 1 ]; $post_markup = call_user_func_array( $post_callback, array( &$inner_block, &$block, $next ) ); } $block_content .= traverse_and_serialize_block( $inner_block, $pre_callback, $post_callback ); $block_content .= $post_markup ?? ''; ++$block_index; } } if ( ! is_array( $block['attrs'] ) ) { $block['attrs'] = array(); } return get_comment_delimited_block_content( $block['blockName'], $block['attrs'], $block_content ); } /** * Replaces patterns in a block tree with their content. * * @since 6.6.0 * @since 7.0.0 Adds metadata to attributes of single-pattern container blocks. * * @param array $blocks An array blocks. * * @return array An array of blocks with patterns replaced by their content. */ function resolve_pattern_blocks( $blocks ) { static $inner_content; // Keep track of seen references to avoid infinite loops. static $seen_refs = array(); $i = 0; while ( $i < count( $blocks ) ) { if ( 'core/pattern' === $blocks[ $i ]['blockName'] ) { $attrs = $blocks[ $i ]['attrs']; if ( empty( $attrs['slug'] ) ) { ++$i; continue; } $slug = $attrs['slug']; if ( isset( $seen_refs[ $slug ] ) ) { // Skip recursive patterns. array_splice( $blocks, $i, 1 ); continue; } $registry = WP_Block_Patterns_Registry::get_instance(); $pattern = $registry->get_registered( $slug ); // Skip unknown patterns. if ( ! $pattern ) { ++$i; continue; } $blocks_to_insert = parse_blocks( trim( $pattern['content'] ) ); /* * For single-root patterns, add the pattern name to make this a pattern instance in the editor. * If the pattern has metadata, merge it with the existing metadata. */ if ( count( $blocks_to_insert ) === 1 ) { $block_metadata = $blocks_to_insert[0]['attrs']['metadata'] ?? array(); $block_metadata['patternName'] = $slug; /* * Merge pattern metadata with existing block metadata. * Pattern metadata takes precedence, but existing block metadata * is preserved as a fallback when the pattern doesn't define that field. * Only the defined fields (name, description, categories) are updated; * other metadata keys are preserved. */ foreach ( array( 'name' => 'title', // 'title' is the field in the pattern object 'name' is the field in the block metadata. 'description' => 'description', 'categories' => 'categories', ) as $key => $pattern_key ) { $value = $pattern[ $pattern_key ] ?? $block_metadata[ $key ] ?? null; if ( $value ) { $block_metadata[ $key ] = is_array( $value ) ? array_map( 'sanitize_text_field', $value ) : sanitize_text_field( $value ); } } $blocks_to_insert[0]['attrs']['metadata'] = $block_metadata; } $seen_refs[ $slug ] = true; $prev_inner_content = $inner_content; $inner_content = null; $blocks_to_insert = resolve_pattern_blocks( $blocks_to_insert ); $inner_content = $prev_inner_content; unset( $seen_refs[ $slug ] ); array_splice( $blocks, $i, 1, $blocks_to_insert ); // If we have inner content, we need to insert nulls in the // inner content array, otherwise serialize_blocks will skip // blocks. if ( $inner_content ) { $null_indices = array_keys( $inner_content, null, true ); $content_index = $null_indices[ $i ]; $nulls = array_fill( 0, count( $blocks_to_insert ), null ); array_splice( $inner_content, $content_index, 1, $nulls ); } // Skip inserted blocks. $i += count( $blocks_to_insert ); } else { if ( ! empty( $blocks[ $i ]['innerBlocks'] ) ) { $prev_inner_content = $inner_content; $inner_content = $blocks[ $i ]['innerContent']; $blocks[ $i ]['innerBlocks'] = resolve_pattern_blocks( $blocks[ $i ]['innerBlocks'] ); $blocks[ $i ]['innerContent'] = $inner_content; $inner_content = $prev_inner_content; } ++$i; } } return $blocks; } /** * Given an array of parsed block trees, applies callbacks before and after serializing them and * returns their concatenated output. * * Recursively traverses the blocks and their inner blocks and applies the two callbacks provided as * arguments, the first one before serializing a block, and the second one after serializing. * If either callback returns a string value, it will be prepended and appended to the serialized * block markup, respectively. * * The callbacks will receive a reference to the current block as their first argument, so that they * can also modify it, and the current block's parent block as second argument. Finally, the * `$pre_callback` receives the previous block, whereas the `$post_callback` receives * the next block as third argument. * * Serialized blocks are returned including comment delimiters, and with all attributes serialized. * * This function should be used when there is a need to modify the saved blocks, or to inject markup * into the return value. Prefer `serialize_blocks` when preparing blocks to be saved to post content. * * This function is meant for internal use only. * * @since 6.4.0 * @access private * * @see serialize_blocks() * * @param array[] $blocks An array of parsed blocks. See WP_Block_Parser_Block. * @param callable $pre_callback Callback to run on each block in the tree before it is traversed and serialized. * It is called with the following arguments: &$block, $parent_block, $previous_block. * Its string return value will be prepended to the serialized block markup. * @param callable $post_callback Callback to run on each block in the tree after it is traversed and serialized. * It is called with the following arguments: &$block, $parent_block, $next_block. * Its string return value will be appended to the serialized block markup. * @return string Serialized block markup. */ function traverse_and_serialize_blocks( $blocks, $pre_callback = null, $post_callback = null ) { $result = ''; $parent_block = null; // At the top level, there is no parent block to pass to the callbacks; yet the callbacks expect a reference. $pre_callback_is_callable = is_callable( $pre_callback ); $post_callback_is_callable = is_callable( $post_callback ); foreach ( $blocks as $index => $block ) { if ( $pre_callback_is_callable ) { $prev = 0 === $index ? null : $blocks[ $index - 1 ]; $result .= call_user_func_array( $pre_callback, array( &$block, &$parent_block, $prev ) ); } if ( $post_callback_is_callable ) { $next = count( $blocks ) - 1 === $index ? null : $blocks[ $index + 1 ]; $post_markup = call_user_func_array( $post_callback, array( &$block, &$parent_block, $next ) ); } $result .= traverse_and_serialize_block( $block, $pre_callback, $post_callback ); $result .= $post_markup ?? ''; } return $result; } /** * Filters and sanitizes block content to remove non-allowable HTML * from parsed block attribute values. * * @since 5.3.1 * * @param string $text Text that may contain block content. * @param array[]|string $allowed_html Optional. An array of allowed HTML elements and attributes, * or a context name such as 'post'. See wp_kses_allowed_html() * for the list of accepted context names. Default 'post'. * @param string[] $allowed_protocols Optional. Array of allowed URL protocols. * Defaults to the result of wp_allowed_protocols(). * @return string The filtered and sanitized content result. */ function filter_block_content( $text, $allowed_html = 'post', $allowed_protocols = array() ) { $result = ''; if ( str_contains( $text, '' ) ) { $text = preg_replace_callback( '%%', '_filter_block_content_callback', $text ); } $blocks = parse_blocks( $text ); foreach ( $blocks as $block ) { $block = filter_block_kses( $block, $allowed_html, $allowed_protocols ); $result .= serialize_block( $block ); } return $result; } /** * Callback used for regular expression replacement in filter_block_content(). * * @since 6.2.1 * @access private * * @param array $matches Array of preg_replace_callback matches. * @return string Replacement string. */ function _filter_block_content_callback( $matches ) { return ''; } /** * Filters and sanitizes a parsed block to remove non-allowable HTML * from block attribute values. * * @since 5.3.1 * * @param WP_Block_Parser_Block $block The parsed block object. * @param array[]|string $allowed_html An array of allowed HTML elements and attributes, * or a context name such as 'post'. See wp_kses_allowed_html() * for the list of accepted context names. * @param string[] $allowed_protocols Optional. Array of allowed URL protocols. * Defaults to the result of wp_allowed_protocols(). * @return array The filtered and sanitized block object result. */ function filter_block_kses( $block, $allowed_html, $allowed_protocols = array() ) { $block['attrs'] = filter_block_kses_value( $block['attrs'], $allowed_html, $allowed_protocols, $block ); if ( is_array( $block['innerBlocks'] ) ) { foreach ( $block['innerBlocks'] as $i => $inner_block ) { $block['innerBlocks'][ $i ] = filter_block_kses( $inner_block, $allowed_html, $allowed_protocols ); } } return $block; } /** * Filters and sanitizes a parsed block attribute value to remove * non-allowable HTML. * * @since 5.3.1 * @since 6.5.5 Added the `$block_context` parameter. * * @param string[]|string $value The attribute value to filter. * @param array[]|string $allowed_html An array of allowed HTML elements and attributes, * or a context name such as 'post'. See wp_kses_allowed_html() * for the list of accepted context names. * @param string[] $allowed_protocols Optional. Array of allowed URL protocols. * Defaults to the result of wp_allowed_protocols(). * @param array $block_context Optional. The block the attribute belongs to, in parsed block array format. * @return string[]|string The filtered and sanitized result. */ function filter_block_kses_value( $value, $allowed_html, $allowed_protocols = array(), $block_context = null ) { if ( is_array( $value ) ) { foreach ( $value as $key => $inner_value ) { $filtered_key = filter_block_kses_value( $key, $allowed_html, $allowed_protocols, $block_context ); $filtered_value = filter_block_kses_value( $inner_value, $allowed_html, $allowed_protocols, $block_context ); if ( isset( $block_context['blockName'] ) && 'core/template-part' === $block_context['blockName'] ) { $filtered_value = filter_block_core_template_part_attributes( $filtered_value, $filtered_key, $allowed_html ); } if ( $filtered_key !== $key ) { unset( $value[ $key ] ); } $value[ $filtered_key ] = $filtered_value; } } elseif ( is_string( $value ) ) { return wp_kses( $value, $allowed_html, $allowed_protocols ); } return $value; } /** * Sanitizes the value of the Template Part block's `tagName` attribute. * * @since 6.5.5 * * @param string $attribute_value The attribute value to filter. * @param string $attribute_name The attribute name. * @param array[]|string $allowed_html An array of allowed HTML elements and attributes, * or a context name such as 'post'. See wp_kses_allowed_html() * for the list of accepted context names. * @return string The sanitized attribute value. */ function filter_block_core_template_part_attributes( $attribute_value, $attribute_name, $allowed_html ) { if ( empty( $attribute_value ) || 'tagName' !== $attribute_name ) { return $attribute_value; } if ( ! is_array( $allowed_html ) ) { $allowed_html = wp_kses_allowed_html( $allowed_html ); } return isset( $allowed_html[ $attribute_value ] ) ? $attribute_value : ''; } /** * Parses blocks out of a content string, and renders those appropriate for the excerpt. * * As the excerpt should be a small string of text relevant to the full post content, * this function renders the blocks that are most likely to contain such text. * * @since 5.0.0 * * @param string $content The content to parse. * @return string The parsed and filtered content. */ function excerpt_remove_blocks( $content ) { if ( ! has_blocks( $content ) ) { return $content; } $allowed_inner_blocks = array( // Classic blocks have their blockName set to null. null, 'core/freeform', 'core/heading', 'core/html', 'core/list', 'core/media-text', 'core/paragraph', 'core/preformatted', 'core/pullquote', 'core/quote', 'core/table', 'core/verse', ); $allowed_wrapper_blocks = array( 'core/columns', 'core/column', 'core/group', ); /** * Filters the list of blocks that can be used as wrapper blocks, allowing * excerpts to be generated from the `innerBlocks` of these wrappers. * * @since 5.8.0 * * @param string[] $allowed_wrapper_blocks The list of names of allowed wrapper blocks. */ $allowed_wrapper_blocks = apply_filters( 'excerpt_allowed_wrapper_blocks', $allowed_wrapper_blocks ); $allowed_blocks = array_merge( $allowed_inner_blocks, $allowed_wrapper_blocks ); /** * Filters the list of blocks that can contribute to the excerpt. * * If a dynamic block is added to this list, it must not generate another * excerpt, as this will cause an infinite loop to occur. * * @since 5.0.0 * * @param string[] $allowed_blocks The list of names of allowed blocks. */ $allowed_blocks = apply_filters( 'excerpt_allowed_blocks', $allowed_blocks ); $blocks = parse_blocks( $content ); $output = ''; foreach ( $blocks as $block ) { if ( in_array( $block['blockName'], $allowed_blocks, true ) ) { if ( ! empty( $block['innerBlocks'] ) ) { if ( in_array( $block['blockName'], $allowed_wrapper_blocks, true ) ) { $output .= _excerpt_render_inner_blocks( $block, $allowed_blocks ); continue; } // Skip the block if it has disallowed or nested inner blocks. foreach ( $block['innerBlocks'] as $inner_block ) { if ( ! in_array( $inner_block['blockName'], $allowed_inner_blocks, true ) || ! empty( $inner_block['innerBlocks'] ) ) { continue 2; } } } $output .= render_block( $block ); } } return $output; } /** * Parses footnotes markup out of a content string, * and renders those appropriate for the excerpt. * * @since 6.3.0 * * @param string $content The content to parse. * @return string The parsed and filtered content. */ function excerpt_remove_footnotes( $content ) { if ( ! str_contains( $content, 'data-fn=' ) ) { return $content; } return preg_replace( '_\s*\d+\s*_', '', $content ); } /** * Renders inner blocks from the allowed wrapper blocks * for generating an excerpt. * * @since 5.8.0 * @access private * * @param array $parsed_block The parsed block. * @param array $allowed_blocks The list of allowed inner blocks. * @return string The rendered inner blocks. */ function _excerpt_render_inner_blocks( $parsed_block, $allowed_blocks ) { $output = ''; foreach ( $parsed_block['innerBlocks'] as $inner_block ) { if ( ! in_array( $inner_block['blockName'], $allowed_blocks, true ) ) { continue; } if ( empty( $inner_block['innerBlocks'] ) ) { $output .= render_block( $inner_block ); } else { $output .= _excerpt_render_inner_blocks( $inner_block, $allowed_blocks ); } } return $output; } /** * Renders a single block into a HTML string. * * @since 5.0.0 * * @global WP_Post $post The post to edit. * * @param array $parsed_block { * An associative array of the block being rendered. See WP_Block_Parser_Block. * * @type string|null $blockName Name of block. * @type array $attrs Attributes from block comment delimiters. * @type array[] $innerBlocks List of inner blocks. An array of arrays that * have the same structure as this one. * @type string $innerHTML HTML from inside block comment delimiters. * @type array $innerContent List of string fragments and null markers where * inner blocks were found. * } * @return string String of rendered HTML. */ function render_block( $parsed_block ) { global $post; $parent_block = null; /** * Allows render_block() to be short-circuited, by returning a non-null value. * * @since 5.1.0 * @since 5.9.0 The `$parent_block` parameter was added. * * @param string|null $pre_render The pre-rendered content. Default null. * @param array $parsed_block { * An associative array of the block being rendered. See WP_Block_Parser_Block. * * @type string|null $blockName Name of block. * @type array $attrs Attributes from block comment delimiters. * @type array[] $innerBlocks List of inner blocks. An array of arrays that * have the same structure as this one. * @type string $innerHTML HTML from inside block comment delimiters. * @type array $innerContent List of string fragments and null markers where * inner blocks were found. * } * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block. */ $pre_render = apply_filters( 'pre_render_block', null, $parsed_block, $parent_block ); if ( ! is_null( $pre_render ) ) { return $pre_render; } $source_block = $parsed_block; /** * Filters the block being rendered in render_block(), before it's processed. * * @since 5.1.0 * @since 5.9.0 The `$parent_block` parameter was added. * * @param array $parsed_block { * An associative array of the block being rendered. See WP_Block_Parser_Block. * * @type string|null $blockName Name of block. * @type array $attrs Attributes from block comment delimiters. * @type array[] $innerBlocks List of inner blocks. An array of arrays that * have the same structure as this one. * @type string $innerHTML HTML from inside block comment delimiters. * @type array $innerContent List of string fragments and null markers where * inner blocks were found. * } * @param array $source_block { * An un-modified copy of `$parsed_block`, as it appeared in the source content. * See WP_Block_Parser_Block. * * @type string|null $blockName Name of block. * @type array $attrs Attributes from block comment delimiters. * @type array[] $innerBlocks List of inner blocks. An array of arrays that * have the same structure as this one. * @type string $innerHTML HTML from inside block comment delimiters. * @type array $innerContent List of string fragments and null markers where * inner blocks were found. * } * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block. */ $parsed_block = apply_filters( 'render_block_data', $parsed_block, $source_block, $parent_block ); $context = array(); if ( $post instanceof WP_Post ) { $context['postId'] = $post->ID; /* * The `postType` context is largely unnecessary server-side, since the ID * is usually sufficient on its own. That being said, since a block's * manifest is expected to be shared between the server and the client, * it should be included to consistently fulfill the expectation. */ $context['postType'] = $post->post_type; } /** * Filters the default context provided to a rendered block. * * @since 5.5.0 * @since 5.9.0 The `$parent_block` parameter was added. * * @param array $context Default context. * @param array $parsed_block { * An associative array of the block being rendered. See WP_Block_Parser_Block. * * @type string|null $blockName Name of block. * @type array $attrs Attributes from block comment delimiters. * @type array[] $innerBlocks List of inner blocks. An array of arrays that * have the same structure as this one. * @type string $innerHTML HTML from inside block comment delimiters. * @type array $innerContent List of string fragments and null markers where * inner blocks were found. * } * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block. */ $context = apply_filters( 'render_block_context', $context, $parsed_block, $parent_block ); $block = new WP_Block( $parsed_block, $context ); return $block->render(); } /** * Parses blocks out of a content string. * * Given an HTML document, this function fully-parses block content, producing * a tree of blocks and their contents, as well as top-level non-block content, * which will appear as a block with no `blockName`. * * This function can be memory heavy for certain documents, particularly those * with deeply-nested blocks or blocks with extensive attribute values. Further, * this function must parse an entire document in one atomic operation. * * If the entire parsed document is not necessary, consider using {@see WP_Block_Processor} * instead, as it provides a streaming and low-overhead interface for finding blocks. * * @since 5.0.0 * * @param string $content Post content. * @return array[] { * Array of block structures. * * @type array ...$0 { * An associative array of a single parsed block object. See WP_Block_Parser_Block. * * @type string|null $blockName Name of block. * @type array $attrs Attributes from block comment delimiters. * @type array[] $innerBlocks List of inner blocks. An array of arrays that * have the same structure as this one. * @type string $innerHTML HTML from inside block comment delimiters. * @type array $innerContent List of string fragments and null markers where * inner blocks were found. * } * } */ function parse_blocks( $content ) { /** * Filter to allow plugins to replace the server-side block parser. * * @since 5.0.0 * * @param string $parser_class Name of block parser class. */ $parser_class = apply_filters( 'block_parser_class', 'WP_Block_Parser' ); $parser = new $parser_class(); return $parser->parse( $content ); } /** * Parses dynamic blocks out of `post_content` and re-renders them. * * @since 5.0.0 * * @param string $content Post content. * @return string Updated post content. */ function do_blocks( $content ) { $blocks = parse_blocks( $content ); $top_level_block_count = count( $blocks ); $output = ''; /** * Parsed blocks consist of a list of top-level blocks. Those top-level * blocks may themselves contain nested inner blocks. However, every * top-level block is rendered independently, meaning there are no data * dependencies between them. * * Ideally, therefore, the parser would only need to parse one complete * top-level block at a time, render it, and move on. Unfortunately, this * is not possible with {@see \parse_blocks()} because it must parse the * entire given document at once. * * While the current implementation prevents this optimization, it’s still * possible to reduce the peak memory use when calls to `render_block()` * on those top-level blocks are memory-heavy (which many of them are). * By setting each parsed block to `NULL` after rendering it, any memory * allocated during the render will be freed and reused for the next block. * Before making this change, that memory was retained and would lead to * out-of-memory crashes for certain posts that now run with this change. */ for ( $i = 0; $i < $top_level_block_count; $i++ ) { $output .= render_block( $blocks[ $i ] ); $blocks[ $i ] = null; } // If there are blocks in this content, we shouldn't run wpautop() on it later. $priority = has_filter( 'the_content', 'wpautop' ); if ( false !== $priority && doing_filter( 'the_content' ) && has_blocks( $content ) ) { remove_filter( 'the_content', 'wpautop', $priority ); add_filter( 'the_content', '_restore_wpautop_hook', $priority + 1 ); } return $output; } /** * If do_blocks() needs to remove wpautop() from the `the_content` filter, this re-adds it afterwards, * for subsequent `the_content` usage. * * @since 5.0.0 * @access private * * @param string $content The post content running through this filter. * @return string The unmodified content. */ function _restore_wpautop_hook( $content ) { $current_priority = has_filter( 'the_content', '_restore_wpautop_hook' ); add_filter( 'the_content', 'wpautop', $current_priority - 1 ); remove_filter( 'the_content', '_restore_wpautop_hook', $current_priority ); return $content; } /** * Returns the current version of the block format that the content string is using. * * If the string doesn't contain blocks, it returns 0. * * @since 5.0.0 * * @param string $content Content to test. * @return int The block format version is 1 if the content contains one or more blocks, 0 otherwise. */ function block_version( $content ) { return has_blocks( $content ) ? 1 : 0; } /** * Registers a new block style. * * @since 5.3.0 * @since 6.6.0 Added support for registering styles for multiple block types. * * @link https://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles/ * * @param string|string[] $block_name Block type name including namespace or array of namespaced block type names. * @param array $style_properties Array containing the properties of the style name, label, * style_handle (name of the stylesheet to be enqueued), * inline_style (string containing the CSS to be added), * style_data (theme.json-like array to generate CSS from). * See WP_Block_Styles_Registry::register(). * @return bool True if the block style was registered with success and false otherwise. */ function register_block_style( $block_name, $style_properties ) { return WP_Block_Styles_Registry::get_instance()->register( $block_name, $style_properties ); } /** * Unregisters a block style. * * @since 5.3.0 * * @param string $block_name Block type name including namespace. * @param string $block_style_name Block style name. * @return bool True if the block style was unregistered with success and false otherwise. */ function unregister_block_style( $block_name, $block_style_name ) { return WP_Block_Styles_Registry::get_instance()->unregister( $block_name, $block_style_name ); } /** * Checks whether the current block type supports the feature requested. * * @since 5.8.0 * @since 6.4.0 The `$feature` parameter now supports a string. * * @param WP_Block_Type|null $block_type Block type to check for support. * @param string|array $feature Feature slug, or path to a specific feature to check support for. * @param mixed $default_value Optional. Fallback value for feature support. Default false. * @return bool Whether the feature is supported. */ function block_has_support( $block_type, $feature, $default_value = false ) { $block_support = $default_value; if ( $block_type instanceof WP_Block_Type ) { if ( is_array( $feature ) && count( $feature ) === 1 ) { $feature = $feature[0]; } if ( is_array( $feature ) ) { $block_support = _wp_array_get( $block_type->supports, $feature, $default_value ); } elseif ( isset( $block_type->supports[ $feature ] ) ) { $block_support = $block_type->supports[ $feature ]; } } return true === $block_support || is_array( $block_support ); } /** * Converts typography keys declared under `supports.*` to `supports.typography.*`. * * Displays a `_doing_it_wrong()` notice when a block using the older format is detected. * * @since 5.8.0 * * @param array $metadata Metadata for registering a block type. * @return array Filtered metadata for registering a block type. */ function wp_migrate_old_typography_shape( $metadata ) { if ( ! isset( $metadata['supports'] ) ) { return $metadata; } $typography_keys = array( '__experimentalFontFamily', '__experimentalFontStyle', '__experimentalFontWeight', '__experimentalLetterSpacing', '__experimentalTextDecoration', '__experimentalTextTransform', 'fontSize', 'lineHeight', ); foreach ( $typography_keys as $typography_key ) { $support_for_key = $metadata['supports'][ $typography_key ] ?? null; if ( null !== $support_for_key ) { _doing_it_wrong( 'register_block_type_from_metadata()', sprintf( /* translators: 1: Block type, 2: Typography supports key, e.g: fontSize, lineHeight, etc. 3: block.json, 4: Old metadata key, 5: New metadata key. */ __( 'Block "%1$s" is declaring %2$s support in %3$s file under %4$s. %2$s support is now declared under %5$s.' ), $metadata['name'], "$typography_key", 'block.json', "supports.$typography_key", "supports.typography.$typography_key" ), '5.8.0' ); _wp_array_set( $metadata['supports'], array( 'typography', $typography_key ), $support_for_key ); unset( $metadata['supports'][ $typography_key ] ); } } return $metadata; } /** * Helper function that constructs a WP_Query args array from * a `Query` block properties. * * It's used in Query Loop, Query Pagination Numbers and Query Pagination Next blocks. * * @since 5.8.0 * @since 6.1.0 Added `query_loop_block_query_vars` filter and `parents` support in query. * @since 6.7.0 Added support for the `format` property in query. * @since 7.0.0 Updated `taxQuery` structure. * * @param WP_Block $block Block instance. * @param int $page Current query's page. * * @return array Returns the constructed WP_Query arguments. */ function build_query_vars_from_query_block( $block, $page ) { $query = array( 'post_type' => 'post', 'order' => 'DESC', 'orderby' => 'date', 'post__not_in' => array(), 'tax_query' => array(), ); if ( isset( $block->context['query'] ) ) { if ( ! empty( $block->context['query']['postType'] ) ) { $post_type_param = $block->context['query']['postType']; if ( is_post_type_viewable( $post_type_param ) ) { $query['post_type'] = $post_type_param; } } if ( isset( $block->context['query']['sticky'] ) && ! empty( $block->context['query']['sticky'] ) ) { $sticky = get_option( 'sticky_posts' ); if ( 'only' === $block->context['query']['sticky'] ) { /* * Passing an empty array to post__in will return have_posts() as true (and all posts will be returned). * Logic should be used before hand to determine if WP_Query should be used in the event that the array * being passed to post__in is empty. * * @see https://core.trac.wordpress.org/ticket/28099 */ $query['post__in'] = ! empty( $sticky ) ? $sticky : array( 0 ); $query['ignore_sticky_posts'] = 1; } elseif ( 'exclude' === $block->context['query']['sticky'] ) { $query['post__not_in'] = array_merge( $query['post__not_in'], $sticky ); } elseif ( 'ignore' === $block->context['query']['sticky'] ) { $query['ignore_sticky_posts'] = 1; } } if ( ! empty( $block->context['query']['exclude'] ) ) { $excluded_post_ids = array_map( 'intval', $block->context['query']['exclude'] ); $excluded_post_ids = array_filter( $excluded_post_ids ); $query['post__not_in'] = array_merge( $query['post__not_in'], $excluded_post_ids ); } if ( isset( $block->context['query']['perPage'] ) && is_numeric( $block->context['query']['perPage'] ) ) { $per_page = absint( $block->context['query']['perPage'] ); $offset = 0; if ( isset( $block->context['query']['offset'] ) && is_numeric( $block->context['query']['offset'] ) ) { $offset = absint( $block->context['query']['offset'] ); } $query['offset'] = ( $per_page * ( $page - 1 ) ) + $offset; $query['posts_per_page'] = $per_page; } // Migrate `categoryIds` and `tagIds` to `tax_query` for backwards compatibility. if ( ! empty( $block->context['query']['categoryIds'] ) || ! empty( $block->context['query']['tagIds'] ) ) { $tax_query_back_compat = array(); if ( ! empty( $block->context['query']['categoryIds'] ) ) { $tax_query_back_compat[] = array( 'taxonomy' => 'category', 'terms' => array_filter( array_map( 'intval', $block->context['query']['categoryIds'] ) ), 'include_children' => false, ); } if ( ! empty( $block->context['query']['tagIds'] ) ) { $tax_query_back_compat[] = array( 'taxonomy' => 'post_tag', 'terms' => array_filter( array_map( 'intval', $block->context['query']['tagIds'] ) ), 'include_children' => false, ); } $query['tax_query'] = array_merge( $query['tax_query'], $tax_query_back_compat ); } if ( ! empty( $block->context['query']['taxQuery'] ) && is_array( $block->context['query']['taxQuery'] ) ) { $tax_query_input = $block->context['query']['taxQuery']; $tax_query = array(); // If there are keys other than include/exclude, it's the old // format e.g. "taxQuery":{"category":[4]} if ( ! empty( array_diff( array_keys( $tax_query_input ), array( 'include', 'exclude' ) ) ) ) { foreach ( $block->context['query']['taxQuery'] as $taxonomy => $terms ) { if ( is_taxonomy_viewable( $taxonomy ) && ! empty( $terms ) ) { $tax_query[] = array( 'taxonomy' => $taxonomy, 'terms' => array_filter( array_map( 'intval', $terms ) ), 'include_children' => false, ); } } } else { // This is the new format e.g. "taxQuery":{"include":{"category":[4]},"exclude":{"post_tag":[5]}} // Helper function to build tax_query conditions from taxonomy terms. $build_conditions = static function ( $terms, string $operator = 'IN' ): array { $terms = (array) $terms; $conditions = array(); foreach ( $terms as $taxonomy => $tax_terms ) { if ( ! empty( $tax_terms ) && is_taxonomy_viewable( $taxonomy ) ) { $conditions[] = array( 'taxonomy' => $taxonomy, 'terms' => array_filter( array_map( 'intval', $tax_terms ) ), 'operator' => $operator, 'include_children' => false, ); } } return $conditions; }; // Separate exclude from include terms. $exclude_terms = isset( $tax_query_input['exclude'] ) && is_array( $tax_query_input['exclude'] ) ? $tax_query_input['exclude'] : array(); $include_terms = isset( $tax_query_input['include'] ) && is_array( $tax_query_input['include'] ) ? $tax_query_input['include'] : array(); $tax_query = array_merge( $build_conditions( $include_terms ), $build_conditions( $exclude_terms, 'NOT IN' ) ); } if ( ! empty( $tax_query ) ) { // Merge with any existing `tax_query` conditions. $query['tax_query'] = array_merge( $query['tax_query'], $tax_query ); } } if ( ! empty( $block->context['query']['format'] ) && is_array( $block->context['query']['format'] ) ) { $formats = $block->context['query']['format']; /* * Validate that the format is either `standard` or a supported post format. * - First, add `standard` to the array of valid formats. * - Then, remove any invalid formats. */ $valid_formats = array_merge( array( 'standard' ), get_post_format_slugs() ); $formats = array_intersect( $formats, $valid_formats ); /* * The relation needs to be set to `OR` since the request can contain * two separate conditions. The user may be querying for items that have * either the `standard` format or a specific format. */ $formats_query = array( 'relation' => 'OR' ); /* * The default post format, `standard`, is not stored in the database. * If `standard` is part of the request, the query needs to exclude all post items that * have a format assigned. */ if ( in_array( 'standard', $formats, true ) ) { $formats_query[] = array( 'taxonomy' => 'post_format', 'field' => 'slug', 'operator' => 'NOT EXISTS', ); // Remove the `standard` format, since it cannot be queried. unset( $formats[ array_search( 'standard', $formats, true ) ] ); } // Add any remaining formats to the formats query. if ( ! empty( $formats ) ) { // Add the `post-format-` prefix. $terms = array_map( static function ( $format ) { return "post-format-$format"; }, $formats ); $formats_query[] = array( 'taxonomy' => 'post_format', 'field' => 'slug', 'terms' => $terms, 'operator' => 'IN', ); } /* * Add `$formats_query` to `$query`, as long as it contains more than one key: * If `$formats_query` only contains the initial `relation` key, there are no valid formats to query, * and the query should not be modified. */ if ( count( $formats_query ) > 1 ) { // Enable filtering by both post formats and other taxonomies by combining them with `AND`. if ( empty( $query['tax_query'] ) ) { $query['tax_query'] = $formats_query; } else { $query['tax_query'] = array( 'relation' => 'AND', $query['tax_query'], $formats_query, ); } } } if ( isset( $block->context['query']['order'] ) && in_array( strtoupper( $block->context['query']['order'] ), array( 'ASC', 'DESC' ), true ) ) { $query['order'] = strtoupper( $block->context['query']['order'] ); } if ( isset( $block->context['query']['orderBy'] ) ) { $query['orderby'] = $block->context['query']['orderBy']; } if ( isset( $block->context['query']['author'] ) ) { if ( is_array( $block->context['query']['author'] ) ) { $query['author__in'] = array_filter( array_map( 'intval', $block->context['query']['author'] ) ); } elseif ( is_string( $block->context['query']['author'] ) ) { $query['author__in'] = array_filter( array_map( 'intval', explode( ',', $block->context['query']['author'] ) ) ); } elseif ( is_int( $block->context['query']['author'] ) && $block->context['query']['author'] > 0 ) { $query['author'] = $block->context['query']['author']; } } if ( ! empty( $block->context['query']['search'] ) ) { $query['s'] = $block->context['query']['search']; } if ( ! empty( $block->context['query']['parents'] ) && is_post_type_hierarchical( $query['post_type'] ) ) { $query['post_parent__in'] = array_unique( array_map( 'intval', $block->context['query']['parents'] ) ); } } /** * Filters the arguments which will be passed to `WP_Query` for the Query Loop Block. * * Anything to this filter should be compatible with the `WP_Query` API to form * the query context which will be passed down to the Query Loop Block's children. * This can help, for example, to include additional settings or meta queries not * directly supported by the core Query Loop Block, and extend its capabilities. * * Please note that this will only influence the query that will be rendered on the * front-end. The editor preview is not affected by this filter. Also, worth noting * that the editor preview uses the REST API, so, ideally, one should aim to provide * attributes which are also compatible with the REST API, in order to be able to * implement identical queries on both sides. * * @since 6.1.0 * * @param array $query Array containing parameters for `WP_Query` as parsed by the block context. * @param WP_Block $block Block instance. * @param int $page Current query's page. */ return apply_filters( 'query_loop_block_query_vars', $query, $block, $page ); } /** * Helper function that returns the proper pagination arrow HTML for * `QueryPaginationNext` and `QueryPaginationPrevious` blocks based * on the provided `paginationArrow` from `QueryPagination` context. * * It's used in QueryPaginationNext and QueryPaginationPrevious blocks. * * @since 5.9.0 * * @param WP_Block $block Block instance. * @param bool $is_next Flag for handling `next/previous` blocks. * @return string|null The pagination arrow HTML or null if there is none. */ function get_query_pagination_arrow( $block, $is_next ) { $arrow_map = array( 'none' => '', 'arrow' => array( 'next' => '→', 'previous' => '←', ), 'chevron' => array( 'next' => '»', 'previous' => '«', ), ); if ( ! empty( $block->context['paginationArrow'] ) && array_key_exists( $block->context['paginationArrow'], $arrow_map ) && ! empty( $arrow_map[ $block->context['paginationArrow'] ] ) ) { $pagination_type = $is_next ? 'next' : 'previous'; $arrow_attribute = $block->context['paginationArrow']; $arrow = $arrow_map[ $block->context['paginationArrow'] ][ $pagination_type ]; $arrow_classes = "wp-block-query-pagination-$pagination_type-arrow is-arrow-$arrow_attribute"; return ""; } return null; } /** * Helper function that constructs a comment query vars array from the passed * block properties. * * It's used with the Comment Query Loop inner blocks. * * @since 6.0.0 * * @param WP_Block $block Block instance. * @return array Returns the comment query parameters to use with the * WP_Comment_Query constructor. */ function build_comment_query_vars_from_block( $block ) { $comment_args = array( 'orderby' => 'comment_date_gmt', 'order' => 'ASC', 'status' => 'approve', 'no_found_rows' => false, ); if ( is_user_logged_in() ) { $comment_args['include_unapproved'] = array( get_current_user_id() ); } else { $unapproved_email = wp_get_unapproved_comment_author_email(); if ( $unapproved_email ) { $comment_args['include_unapproved'] = array( $unapproved_email ); } } if ( ! empty( $block->context['postId'] ) ) { $comment_args['post_id'] = (int) $block->context['postId']; } if ( get_option( 'thread_comments' ) ) { $comment_args['hierarchical'] = 'threaded'; } else { $comment_args['hierarchical'] = false; } if ( get_option( 'page_comments' ) === '1' || get_option( 'page_comments' ) === true ) { $per_page = get_option( 'comments_per_page' ); $default_page = get_option( 'default_comments_page' ); if ( $per_page > 0 ) { $comment_args['number'] = $per_page; $page = (int) get_query_var( 'cpage' ); if ( $page ) { $comment_args['paged'] = $page; } elseif ( 'oldest' === $default_page ) { $comment_args['paged'] = 1; } elseif ( 'newest' === $default_page ) { $max_num_pages = (int) ( new WP_Comment_Query( $comment_args ) )->max_num_pages; if ( 0 !== $max_num_pages ) { $comment_args['paged'] = $max_num_pages; } } } } return $comment_args; } /** * Helper function that returns the proper pagination arrow HTML for * `CommentsPaginationNext` and `CommentsPaginationPrevious` blocks based on the * provided `paginationArrow` from `CommentsPagination` context. * * It's used in CommentsPaginationNext and CommentsPaginationPrevious blocks. * * @since 6.0.0 * * @param WP_Block $block Block instance. * @param string $pagination_type Optional. Type of the arrow we will be rendering. * Accepts 'next' or 'previous'. Default 'next'. * @return string|null The pagination arrow HTML or null if there is none. */ function get_comments_pagination_arrow( $block, $pagination_type = 'next' ) { $arrow_map = array( 'none' => '', 'arrow' => array( 'next' => '→', 'previous' => '←', ), 'chevron' => array( 'next' => '»', 'previous' => '«', ), ); if ( ! empty( $block->context['comments/paginationArrow'] ) && ! empty( $arrow_map[ $block->context['comments/paginationArrow'] ][ $pagination_type ] ) ) { $arrow_attribute = $block->context['comments/paginationArrow']; $arrow = $arrow_map[ $block->context['comments/paginationArrow'] ][ $pagination_type ]; $arrow_classes = "wp-block-comments-pagination-$pagination_type-arrow is-arrow-$arrow_attribute"; return ""; } return null; } /** * Strips all HTML from the content of footnotes, and sanitizes the ID. * * This function expects slashed data on the footnotes content. * * @access private * @since 6.3.2 * * @param string $footnotes JSON-encoded string of an array containing the content and ID of each footnote. * @return string Filtered content without any HTML on the footnote content and with the sanitized ID. */ function _wp_filter_post_meta_footnotes( $footnotes ) { $footnotes_decoded = json_decode( $footnotes, true ); if ( ! is_array( $footnotes_decoded ) ) { return ''; } $footnotes_sanitized = array(); foreach ( $footnotes_decoded as $footnote ) { if ( ! empty( $footnote['content'] ) && ! empty( $footnote['id'] ) ) { $footnotes_sanitized[] = array( 'id' => sanitize_key( $footnote['id'] ), 'content' => wp_unslash( wp_filter_post_kses( wp_slash( $footnote['content'] ) ) ), ); } } return wp_json_encode( $footnotes_sanitized ); } /** * Adds the filters for footnotes meta field. * * @access private * @since 6.3.2 */ function _wp_footnotes_kses_init_filters() { add_filter( 'sanitize_post_meta_footnotes', '_wp_filter_post_meta_footnotes' ); } /** * Removes the filters for footnotes meta field. * * @access private * @since 6.3.2 */ function _wp_footnotes_remove_filters() { remove_filter( 'sanitize_post_meta_footnotes', '_wp_filter_post_meta_footnotes' ); } /** * Registers the filter of footnotes meta field if the user does not have `unfiltered_html` capability. * * @access private * @since 6.3.2 */ function _wp_footnotes_kses_init() { _wp_footnotes_remove_filters(); if ( ! current_user_can( 'unfiltered_html' ) ) { _wp_footnotes_kses_init_filters(); } } /** * Initializes the filters for footnotes meta field when imported data should be filtered. * * This filter is the last one being executed on {@see 'force_filtered_html_on_import'}. * If the input of the filter is true, it means we are in an import situation and should * enable kses, independently of the user capabilities. So in that case we call * _wp_footnotes_kses_init_filters(). * * @access private * @since 6.3.2 * * @param string $arg Input argument of the filter. * @return string Input argument of the filter. */ function _wp_footnotes_force_filtered_html_on_import_filter( $arg ) { // If `force_filtered_html_on_import` is true, we need to init the global styles kses filters. if ( $arg ) { _wp_footnotes_kses_init_filters(); } return $arg; } /** * Exposes blocks with autoRegister flag for ServerSideRender in the editor. * * Detects blocks that have the autoRegister flag set in their supports * and passes them to JavaScript for auto-registration with ServerSideRender. * * @access private * @since 7.0.0 */ function _wp_enqueue_auto_register_blocks() { $auto_register_blocks = array(); $registered_blocks = WP_Block_Type_Registry::get_instance()->get_all_registered(); foreach ( $registered_blocks as $block_name => $block_type ) { if ( ! empty( $block_type->supports['autoRegister'] ) && ! empty( $block_type->render_callback ) ) { $auto_register_blocks[] = $block_name; } } if ( ! empty( $auto_register_blocks ) ) { wp_add_inline_script( 'wp-block-library', sprintf( 'window.__unstableAutoRegisterBlocks = %s;', wp_json_encode( $auto_register_blocks ) ), 'before' ); } }