$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 "$arrow";
}
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 "$arrow";
}
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'
);
}
}
unable to determine the date of this action. It may may have been deleted by another process.', 'woocommerce' ), $action_id ) );
}
if ( self::STATUS_PENDING === $record->status ) {
return as_get_datetime_object( $record->scheduled_date_gmt );
} else {
return as_get_datetime_object( $record->last_attempt_gmt );
}
}
/**
* Stake a claim on actions.
*
* @param int $max_actions Maximum number of action to include in claim.
* @param DateTime|null $before_date Jobs must be schedule before this date. Defaults to now.
* @param array $hooks Hooks to filter for.
* @param string $group Group to filter for.
*
* @return ActionScheduler_ActionClaim
*/
public function stake_claim( $max_actions = 10, ?DateTime $before_date = null, $hooks = array(), $group = '' ) {
$claim_id = $this->generate_claim_id();
$this->claim_before_date = $before_date;
$this->claim_actions( $claim_id, $max_actions, $before_date, $hooks, $group );
$action_ids = $this->find_actions_by_claim_id( $claim_id );
$this->claim_before_date = null;
return new ActionScheduler_ActionClaim( $claim_id, $action_ids );
}
/**
* Generate a new action claim.
*
* @return int Claim ID.
*/
protected function generate_claim_id() {
/**
* Global.
*
* @var \wpdb $wpdb
*/
global $wpdb;
$now = as_get_datetime_object();
$wpdb->insert( $wpdb->actionscheduler_claims, array( 'date_created_gmt' => $now->format( 'Y-m-d H:i:s' ) ) );
return $wpdb->insert_id;
}
/**
* Set a claim filter.
*
* @param string $filter_name Claim filter name.
* @param mixed $filter_values Values to filter.
* @return void
*/
public function set_claim_filter( $filter_name, $filter_values ) {
if ( isset( $this->claim_filters[ $filter_name ] ) ) {
$this->claim_filters[ $filter_name ] = $filter_values;
}
}
/**
* Get the claim filter value.
*
* @param string $filter_name Claim filter name.
* @return mixed
*/
public function get_claim_filter( $filter_name ) {
if ( isset( $this->claim_filters[ $filter_name ] ) ) {
return $this->claim_filters[ $filter_name ];
}
return '';
}
/**
* Mark actions claimed.
*
* @param string $claim_id Claim Id.
* @param int $limit Number of action to include in claim.
* @param DateTime|null $before_date Should use UTC timezone.
* @param array $hooks Hooks to filter for.
* @param string $group Group to filter for.
*
* @return int The number of actions that were claimed.
* @throws \InvalidArgumentException Throws InvalidArgumentException if group doesn't exist.
* @throws \RuntimeException Throws RuntimeException if unable to claim action.
*/
protected function claim_actions( $claim_id, $limit, ?DateTime $before_date = null, $hooks = array(), $group = '' ) {
/**
* Global.
*
* @var \wpdb $wpdb
*/
global $wpdb;
$now = as_get_datetime_object();
$date = is_null( $before_date ) ? $now : clone $before_date;
// Set claim filters.
if ( ! empty( $hooks ) ) {
$this->set_claim_filter( 'hooks', $hooks );
} else {
$hooks = $this->get_claim_filter( 'hooks' );
}
if ( ! empty( $group ) ) {
$this->set_claim_filter( 'group', $group );
} else {
$group = $this->get_claim_filter( 'group' );
}
$where = 'WHERE claim_id = 0 AND scheduled_date_gmt <= %s AND status=%s';
$where_params = array(
$date->format( 'Y-m-d H:i:s' ),
self::STATUS_PENDING,
);
if ( ! empty( $hooks ) ) {
$placeholders = array_fill( 0, count( $hooks ), '%s' );
$where .= ' AND hook IN (' . join( ', ', $placeholders ) . ')';
$where_params = array_merge( $where_params, array_values( $hooks ) );
}
$group_operator = 'IN';
if ( empty( $group ) ) {
$group = $this->get_claim_filter( 'exclude-groups' );
$group_operator = 'NOT IN';
}
if ( ! empty( $group ) ) {
$group_ids = $this->get_group_ids( $group, false );
// throw exception if no matching group(s) found, this matches ActionScheduler_wpPostStore's behaviour.
if ( empty( $group_ids ) ) {
throw new InvalidArgumentException(
sprintf(
/* translators: %s: group name(s) */
_n(
'The group "%s" does not exist.',
'The groups "%s" do not exist.',
is_array( $group ) ? count( $group ) : 1,
'woocommerce'
),
$group
)
);
}
$id_list = implode( ',', array_map( 'intval', $group_ids ) );
$where .= " AND group_id {$group_operator} ( $id_list )";
}
/**
* Sets the order-by clause used in the action claim query.
*
* @param string $order_by_sql
* @param string $claim_id Claim Id.
* @param array $hooks Hooks to filter for.
*
* @since 3.8.3 Made $claim_id and $hooks available.
* @since 3.4.0
*/
$order = apply_filters( 'action_scheduler_claim_actions_order_by', 'ORDER BY priority ASC, attempts ASC, scheduled_date_gmt ASC, action_id ASC', $claim_id, $hooks );
$skip_locked = $this->db_supports_skip_locked() ? ' SKIP LOCKED' : '';
// Selecting the action_ids that we plan to claim, while skipping any locked rows to avoid deadlocking.
$select_sql = $wpdb->prepare( "SELECT action_id from {$wpdb->actionscheduler_actions} {$where} {$order} LIMIT %d FOR UPDATE{$skip_locked}", array_merge( $where_params, array( $limit ) ) );
// Now place it into an UPDATE statement by joining the result sets, allowing for the SKIP LOCKED behavior to take effect.
$update_sql = "UPDATE {$wpdb->actionscheduler_actions} t1 JOIN ( $select_sql ) t2 ON t1.action_id = t2.action_id SET claim_id=%d, last_attempt_gmt=%s, last_attempt_local=%s";
$update_params = array(
$claim_id,
$now->format( 'Y-m-d H:i:s' ),
current_time( 'mysql' ),
);
$rows_affected = $wpdb->query( $wpdb->prepare( $update_sql, $update_params ) );
if ( false === $rows_affected ) {
$error = empty( $wpdb->last_error )
? _x( 'unknown', 'database error', 'woocommerce' )
: $wpdb->last_error;
throw new \RuntimeException(
sprintf(
/* translators: %s database error. */
__( 'Unable to claim actions. Database error: %s.', 'woocommerce' ),
$error
)
);
}
return (int) $rows_affected;
}
/**
* Determines whether the database supports using SKIP LOCKED. This logic mimicks the $wpdb::has_cap() logic.
*
* SKIP_LOCKED support was added to MariaDB in 10.6.0 and to MySQL in 8.0.1
*
* @return bool
*/
private function db_supports_skip_locked() {
global $wpdb;
$db_version = $wpdb->db_version();
$db_server_info = $wpdb->db_server_info();
$is_mariadb = ( false !== strpos( $db_server_info, 'MariaDB' ) );
if ( $is_mariadb &&
'5.5.5' === $db_version &&
PHP_VERSION_ID < 80016 // PHP 8.0.15 or older.
) {
/*
* Account for MariaDB version being prefixed with '5.5.5-' on older PHP versions.
*/
$db_server_info = preg_replace( '/^5\.5\.5-(.*)/', '$1', $db_server_info );
$db_version = preg_replace( '/[^0-9.].*/', '', $db_server_info );
}
$is_supported = ( $is_mariadb && version_compare( $db_version, '10.6.0', '>=' ) ) ||
( ! $is_mariadb && version_compare( $db_version, '8.0.1', '>=' ) );
/**
* Filter whether the database supports the SKIP LOCKED modifier for queries.
*
* @param bool $is_supported Whether SKIP LOCKED is supported.
*
* @since 3.9.3
*/
return apply_filters( 'action_scheduler_db_supports_skip_locked', $is_supported );
}
/**
* Get the number of active claims.
*
* @return int
*/
public function get_claim_count() {
global $wpdb;
$sql = "SELECT COUNT(DISTINCT claim_id) FROM {$wpdb->actionscheduler_actions} WHERE claim_id != 0 AND status IN ( %s, %s)";
$sql = $wpdb->prepare( $sql, array( self::STATUS_PENDING, self::STATUS_RUNNING ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
return (int) $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Return an action's claim ID, as stored in the claim_id column.
*
* @param string $action_id Action ID.
* @return mixed
*/
public function get_claim_id( $action_id ) {
/**
* Global.
*
* @var \wpdb $wpdb
*/
global $wpdb;
$sql = "SELECT claim_id FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d";
$sql = $wpdb->prepare( $sql, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
return (int) $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Retrieve the action IDs of action in a claim.
*
* @param int $claim_id Claim ID.
* @return int[]
*/
public function find_actions_by_claim_id( $claim_id ) {
/**
* Global.
*
* @var \wpdb $wpdb
*/
global $wpdb;
$action_ids = array();
$before_date = isset( $this->claim_before_date ) ? $this->claim_before_date : as_get_datetime_object();
$cut_off = $before_date->format( 'Y-m-d H:i:s' );
$sql = $wpdb->prepare(
"SELECT action_id, scheduled_date_gmt FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d ORDER BY priority ASC, attempts ASC, scheduled_date_gmt ASC, action_id ASC",
$claim_id
);
// Verify that the scheduled date for each action is within the expected bounds (in some unusual
// cases, we cannot depend on MySQL to honor all of the WHERE conditions we specify).
foreach ( $wpdb->get_results( $sql ) as $claimed_action ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
if ( $claimed_action->scheduled_date_gmt <= $cut_off ) {
$action_ids[] = absint( $claimed_action->action_id );
}
}
return $action_ids;
}
/**
* Release pending actions from a claim and delete the claim.
*
* @param ActionScheduler_ActionClaim $claim Claim object.
* @throws \RuntimeException When unable to release actions from claim.
*/
public function release_claim( ActionScheduler_ActionClaim $claim ) {
/**
* Global.
*
* @var \wpdb $wpdb
*/
global $wpdb;
if ( 0 === intval( $claim->get_id() ) ) {
// Verify that the claim_id is valid before attempting to release it.
return;
}
/**
* Deadlock warning: This function modifies actions to release them from claims that have been processed. Earlier, we used to it in a atomic query, i.e. we would update all actions belonging to a particular claim_id with claim_id = 0.
* While this was functionally correct, it would cause deadlock, since this update query will hold a lock on the claim_id_.. index on the action table.
* This allowed the possibility of a race condition, where the claimer query is also running at the same time, then the claimer query will also try to acquire a lock on the claim_id_.. index, and in this case if claim release query has already progressed to the point of acquiring the lock, but have not updated yet, it would cause a deadlock.
*
* We resolve this by getting all the actions_id that we want to release claim from in a separate query, and then releasing the claim on each of them. This way, our lock is acquired on the action_id index instead of the claim_id index. Note that the lock on claim_id will still be acquired, but it will only when we actually make the update, rather than when we select the actions.
*
* We only release pending actions in order for them to be claimed by another process.
*/
$action_ids = $wpdb->get_col( $wpdb->prepare( "SELECT action_id FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d AND status = %s", $claim->get_id(), self::STATUS_PENDING ) );
$row_updates = 0;
if ( count( $action_ids ) > 0 ) {
$action_id_string = implode( ',', array_map( 'absint', $action_ids ) );
$row_updates = $wpdb->query( "UPDATE {$wpdb->actionscheduler_actions} SET claim_id = 0 WHERE action_id IN ({$action_id_string})" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
$wpdb->delete( $wpdb->actionscheduler_claims, array( 'claim_id' => $claim->get_id() ), array( '%d' ) );
if ( $row_updates < count( $action_ids ) ) {
throw new RuntimeException(
sprintf(
// translators: %d is an id.
__( 'Unable to release actions from claim id %d.', 'woocommerce' ),
$claim->get_id()
)
);
}
}
/**
* Remove the claim from an action.
*
* @param int $action_id Action ID.
*
* @return void
*/
public function unclaim_action( $action_id ) {
/**
* Global.
*
* @var \wpdb $wpdb
*/
global $wpdb;
$wpdb->update(
$wpdb->actionscheduler_actions,
array( 'claim_id' => 0 ),
array( 'action_id' => $action_id ),
array( '%s' ),
array( '%d' )
);
}
/**
* Mark an action as failed.
*
* @param int $action_id Action ID.
* @throws \InvalidArgumentException Throw an exception if action was not updated.
*/
public function mark_failure( $action_id ) {
/**
* Global.
* @var \wpdb $wpdb
*/
global $wpdb;
$updated = $wpdb->update(
$wpdb->actionscheduler_actions,
array( 'status' => self::STATUS_FAILED ),
array( 'action_id' => $action_id ),
array( '%s' ),
array( '%d' )
);
if ( empty( $updated ) ) {
/* translators: %s is the action ID */
throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to mark this action as having failed. It may may have been deleted by another process.', 'woocommerce' ), $action_id ) );
}
}
/**
* Add execution message to action log.
*
* @throws Exception If the action status cannot be updated to self::STATUS_RUNNING ('in-progress').
*
* @param int $action_id Action ID.
*
* @return void
*/
public function log_execution( $action_id ) {
/**
* Global.
*
* @var \wpdb $wpdb
*/
global $wpdb;
$sql = "UPDATE {$wpdb->actionscheduler_actions} SET attempts = attempts+1, status=%s, last_attempt_gmt = %s, last_attempt_local = %s WHERE action_id = %d";
$sql = $wpdb->prepare( $sql, self::STATUS_RUNNING, current_time( 'mysql', true ), current_time( 'mysql' ), $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$status_updated = $wpdb->query( $sql );
if ( ! $status_updated ) {
throw new Exception(
sprintf(
/* translators: 1: action ID. 2: status slug. */
__( 'Unable to update the status of action %1$d to %2$s.', 'woocommerce' ),
$action_id,
self::STATUS_RUNNING
)
);
}
}
/**
* Mark an action as complete.
*
* @param int $action_id Action ID.
*
* @return void
* @throws \InvalidArgumentException Throw an exception if action was not updated.
*/
public function mark_complete( $action_id ) {
/**
* Global.
*
* @var \wpdb $wpdb
*/
global $wpdb;
$updated = $wpdb->update(
$wpdb->actionscheduler_actions,
array(
'status' => self::STATUS_COMPLETE,
'last_attempt_gmt' => current_time( 'mysql', true ),
'last_attempt_local' => current_time( 'mysql' ),
),
array( 'action_id' => $action_id ),
array( '%s' ),
array( '%d' )
);
if ( empty( $updated ) ) {
/* translators: %s is the action ID */
throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s: we were unable to mark this action as having completed. It may may have been deleted by another process.', 'woocommerce' ), $action_id ) );
}
/**
* Fires after a scheduled action has been completed.
*
* @since 3.4.2
*
* @param int $action_id Action ID.
*/
do_action( 'action_scheduler_completed_action', $action_id );
}
/**
* Get an action's status.
*
* @param int $action_id Action ID.
*
* @return string
* @throws \InvalidArgumentException Throw an exception if not status was found for action_id.
* @throws \RuntimeException Throw an exception if action status could not be retrieved.
*/
public function get_status( $action_id ) {
/**
* Global.
*
* @var \wpdb $wpdb
*/
global $wpdb;
$sql = "SELECT status FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d";
$sql = $wpdb->prepare( $sql, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$status = $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
if ( is_null( $status ) ) {
throw new \InvalidArgumentException( __( 'Invalid action ID. No status found.', 'woocommerce' ) );
} elseif ( empty( $status ) ) {
throw new \RuntimeException( __( 'Unknown status found for action.', 'woocommerce' ) );
} else {
return $status;
}
}
}