$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' ); } } 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; } } }