lesheet'; $title = $obj->extra['title'] ?? ''; $tag = sprintf( "\n", $rel, esc_attr( $handle ), $title ? sprintf( " title='%s'", esc_attr( $title ) ) : '', $href, esc_attr( $media ) ); /** * Filters the HTML link tag of an enqueued style. * * @since 2.6.0 * @since 4.3.0 Introduced the `$href` parameter. * @since 4.5.0 Introduced the `$media` parameter. * * @param string $tag The link tag for the enqueued style. * @param string $handle The style's registered handle. * @param string $href The stylesheet's source URL. * @param string $media The stylesheet's media attribute. */ $tag = apply_filters( 'style_loader_tag', $tag, $handle, $href, $media ); if ( 'rtl' === $this->text_direction && isset( $obj->extra['rtl'] ) && $obj->extra['rtl'] ) { if ( is_bool( $obj->extra['rtl'] ) || 'replace' === $obj->extra['rtl'] ) { $suffix = $obj->extra['suffix'] ?? ''; $rtl_href = str_replace( "{$suffix}.css", "-rtl{$suffix}.css", $this->_css_href( $src, $ver, "$handle-rtl" ) ); } else { $rtl_href = $this->_css_href( $obj->extra['rtl'], $ver, "$handle-rtl" ); } $rtl_tag = sprintf( "\n", $rel, esc_attr( $handle ), $title ? sprintf( " title='%s'", esc_attr( $title ) ) : '', $rtl_href, esc_attr( $media ) ); /** This filter is documented in wp-includes/class-wp-styles.php */ $rtl_tag = apply_filters( 'style_loader_tag', $rtl_tag, $handle, $rtl_href, $media ); if ( 'replace' === $obj->extra['rtl'] ) { $tag = $rtl_tag; } else { $tag .= $rtl_tag; } } if ( $this->do_concat ) { $this->print_html .= $tag; if ( $inline_style_tag ) { $this->print_html .= $inline_style_tag; } } else { echo $tag; $this->print_inline_style( $handle ); } return true; } /** * Adds extra CSS styles to a registered stylesheet. * * @since 3.3.0 * * @param string $handle The style's registered handle. * @param string $code String containing the CSS styles to be added. * @return bool True on success, false on failure. */ public function add_inline_style( $handle, $code ) { if ( ! $code ) { return false; } $after = $this->get_data( $handle, 'after' ); if ( ! $after ) { $after = array(); } $after[] = $code; return $this->add_data( $handle, 'after', $after ); } /** * Prints extra CSS styles of a registered stylesheet. * * @since 3.3.0 * * @param string $handle The style's registered handle. * @param bool $display Optional. Whether to print the inline style * instead of just returning it. Default true. * @return string|bool False if no data exists, inline styles if `$display` is true, * true otherwise. */ public function print_inline_style( $handle, $display = true ) { $output = $this->get_data( $handle, 'after' ); if ( empty( $output ) || ! is_array( $output ) ) { return false; } if ( ! $this->do_concat ) { // Obtain the original `src` for a stylesheet possibly inlined by wp_maybe_inline_styles(). $inlined_src = $this->get_data( $handle, 'inlined_src' ); // If there's only one `after` inline style, and that inline style had been inlined, then use the $inlined_src // as the sourceURL. Otherwise, if there is more than one inline `after` style associated with the handle, // then resort to using the handle to construct the sourceURL since there isn't a single source. if ( count( $output ) === 1 && is_string( $inlined_src ) && strlen( $inlined_src ) > 0 ) { $source_url = esc_url_raw( $inlined_src ); } else { $source_url = rawurlencode( "{$handle}-inline-css" ); } $output[] = sprintf( '/*# sourceURL=%s */', $source_url ); } $output = implode( "\n", $output ); if ( ! $display ) { return $output; } $processor = new WP_HTML_Tag_Processor( '' ); $processor->next_tag(); $processor->set_attribute( 'id', "{$handle}-inline-css" ); $processor->set_modifiable_text( "\n{$output}\n" ); echo "{$processor->get_updated_html()}\n"; return true; } /** * Overrides the add_data method from WP_Dependencies, to allow unsetting dependencies for conditional styles. * * @since 6.9.0 * * @param string $handle Name of the item. Should be unique. * @param string $key The data key. * @param mixed $value The data value. * @return bool True on success, false on failure. */ public function add_data( $handle, $key, $value ) { if ( ! isset( $this->registered[ $handle ] ) ) { return false; } if ( 'conditional' === $key ) { $this->registered[ $handle ]->deps = array(); } return parent::add_data( $handle, $key, $value ); } /** * Determines style dependencies. * * @since 2.6.0 * * @see WP_Dependencies::all_deps() * * @param string|string[] $handles Item handle (string) or item handles (array of strings). * @param bool $recursion Optional. Internal flag that function is calling itself. * Default false. * @param int|false $group Optional. Group level: level (int), no groups (false). * Default false. * @return bool True on success, false on failure. */ public function all_deps( $handles, $recursion = false, $group = false ) { $result = parent::all_deps( $handles, $recursion, $group ); if ( ! $recursion ) { /** * Filters the array of enqueued styles before processing for output. * * @since 2.6.0 * * @param string[] $to_do The list of enqueued style handles about to be processed. */ $this->to_do = apply_filters( 'print_styles_array', $this->to_do ); } return $result; } /** * Generates an enqueued style's fully-qualified URL. * * @since 2.6.0 * * @param string $src The source of the enqueued style. * @param string|false|null $ver The version of the enqueued style. * @param string $handle The style's registered handle. * @return string Style's fully-qualified URL. */ public function _css_href( $src, $ver, $handle ) { if ( ! is_bool( $src ) && ! preg_match( '|^(https?:)?//|', $src ) && ! ( $this->content_url && str_starts_with( $src, $this->content_url ) ) ) { $src = $this->base_url . $src; } $ver_to_add = ''; if ( empty( $ver ) && null !== $ver && is_string( $this->default_version ) ) { $ver_to_add = $this->default_version; } elseif ( is_scalar( $ver ) ) { $ver_to_add = (string) $ver; } $added_args = (string) ( $this->args[ $handle ] ?? '' ); if ( '' !== $ver_to_add || '' !== $added_args ) { $fragment = strstr( $src, '#' ); if ( false !== $fragment ) { $src = substr( $src, 0, -strlen( $fragment ) ); } if ( '' !== $ver_to_add ) { $src .= ( str_contains( $src, '?' ) ? '&' : '?' ) . 'ver=' . rawurlencode( $ver_to_add ); } if ( '' !== $added_args ) { $src .= ( str_contains( $src, '?' ) ? '&' : '?' ) . $added_args; } if ( false !== $fragment ) { $src .= $fragment; } } /** * Filters an enqueued style's fully-qualified URL. * * @since 2.6.0 * * @param string $src The source URL of the enqueued style. * @param string $handle The style's registered handle. */ $src = apply_filters( 'style_loader_src', $src, $handle ); return esc_url( $src ); } /** * Whether a handle's source is in a default directory. * * @since 2.8.0 * * @param string $src The source of the enqueued style. * @return bool True if found, false if not. */ public function in_default_dir( $src ) { if ( ! $this->default_dirs ) { return true; } return array_any( (array) $this->default_dirs, fn( $test ) => str_starts_with( $src, $test ) ); } /** * Processes items and dependencies for the footer group. * * HTML 5 allows styles in the body, grab late enqueued items and output them in the footer. * * @since 3.3.0 * * @see WP_Dependencies::do_items() * * @return string[] Handles of items that have been processed. */ public function do_footer_items() { $this->do_items( false, 1 ); return $this->done; } /** * Resets class properties. * * @since 3.3.0 */ public function reset() { $this->do_concat = false; $this->concat = ''; $this->concat_version = ''; $this->print_html = ''; } /** * Gets a style-specific dependency warning message. * * @since 6.9.1 * * @param string $handle Style handle with missing dependencies. * @param string[] $missing_dependency_handles Missing dependency handles. * @return string Formatted, localized warning message. */ protected function get_dependency_warning_message( $handle, $missing_dependency_handles ) { return sprintf( /* translators: 1: Style handle, 2: List of missing dependency handles. */ __( 'The style with the handle "%1$s" was enqueued with dependencies that are not registered: %2$s.' ), $handle, implode( wp_get_list_item_separator(), $missing_dependency_handles ) ); } } y abilities that were specified in the constructor are allowed to be * executed. If the ability is not in the allowed list, an error response * with code `ability_not_allowed` is returned. * * @since 7.0.0 * * @param FunctionCall $call The function call to execute. * @return FunctionResponse The response from executing the ability. */ public function execute_ability( FunctionCall $call ): FunctionResponse { $function_name = $call->getName() ?? 'unknown'; $function_id = $call->getId() ?? 'unknown'; if ( ! $this->is_ability_call( $call ) ) { return new FunctionResponse( $function_id, $function_name, array( 'error' => __( 'Not an ability function call' ), 'code' => 'invalid_ability_call', ) ); } $ability_name = self::function_name_to_ability_name( $function_name ); if ( ! isset( $this->allowed_abilities[ $ability_name ] ) ) { return new FunctionResponse( $function_id, $function_name, array( /* translators: %s: ability name */ 'error' => sprintf( __( 'Ability "%s" was not specified in the allowed abilities list.' ), $ability_name ), 'code' => 'ability_not_allowed', ) ); } $ability = wp_get_ability( $ability_name ); if ( ! $ability instanceof WP_Ability ) { return new FunctionResponse( $function_id, $function_name, array( /* translators: %s: ability name */ 'error' => sprintf( __( 'Ability "%s" not found' ), $ability_name ), 'code' => 'ability_not_found', ) ); } $args = $call->getArgs(); $result = $ability->execute( ! empty( $args ) ? $args : null ); if ( is_wp_error( $result ) ) { return new FunctionResponse( $function_id, $function_name, array( 'error' => $result->get_error_message(), 'code' => $result->get_error_code(), 'data' => $result->get_error_data(), ) ); } return new FunctionResponse( $function_id, $function_name, $result ); } /** * Checks if a message contains any ability function calls. * * @since 7.0.0 * * @param Message $message The message to check. * @return bool True if the message contains ability calls, false otherwise. */ public function has_ability_calls( Message $message ): bool { foreach ( $message->getParts() as $part ) { if ( $part->getType()->isFunctionCall() ) { $function_call = $part->getFunctionCall(); if ( $function_call instanceof FunctionCall && $this->is_ability_call( $function_call ) ) { return true; } } } return false; } /** * Executes all ability function calls in a message. * * @since 7.0.0 * * @param Message $message The message containing function calls. * @return Message A new message with function responses. */ public function execute_abilities( Message $message ): Message { $response_parts = array(); foreach ( $message->getParts() as $part ) { if ( $part->getType()->isFunctionCall() ) { $function_call = $part->getFunctionCall(); if ( $function_call instanceof FunctionCall && $this->is_ability_call( $function_call ) ) { $function_response = $this->execute_ability( $function_call ); $response_parts[] = new MessagePart( $function_response ); } } } return new UserMessage( $response_parts ); } /** * Converts an ability name to a function name. * * Transforms "tec/create_event" to "wpab__tec__create_event". * * @since 7.0.0 * * @param string $ability_name The ability name to convert. * @return string The function name. */ public static function ability_name_to_function_name( string $ability_name ): string { return self::ABILITY_PREFIX . str_replace( '/', '__', $ability_name ); } /** * Converts a function name to an ability name. * * Transforms "wpab__tec__create_event" to "tec/create_event". * * @since 7.0.0 * * @param string $function_name The function name to convert. * @return string The ability name. */ public static function function_name_to_ability_name( string $function_name ): string { $without_prefix = substr( $function_name, strlen( self::ABILITY_PREFIX ) ); return str_replace( '__', '/', $without_prefix ); } } object on failure. */ public function get_items( $request ) { $collection = $request->get_param( 'collection' ); if ( null !== $collection && ! WP_Icon_Collections_Registry::get_instance()->is_registered( $collection ) ) { return new WP_Error( 'rest_icon_collection_not_found', sprintf( /* translators: %s: Icon collection slug. */ __( 'Icon collection not found: "%s".' ), $collection ), array( 'status' => 404 ) ); } $response = array(); $search = $request->get_param( 'search' ); $icons = WP_Icons_Registry::get_instance()->get_registered_icons( $search ); foreach ( $icons as $icon ) { if ( null !== $collection && ( ! isset( $icon['collection'] ) || $icon['collection'] !== $collection ) ) { continue; } $prepared_icon = $this->prepare_item_for_response( $icon, $request ); $response[] = $this->prepare_response_for_collection( $prepared_icon ); } return rest_ensure_response( $response ); } /** * Retrieves a specific icon. * * @since 7.0.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_item( $request ) { $icon = $this->get_icon( $request['name'] ); if ( is_wp_error( $icon ) ) { return $icon; } $data = $this->prepare_item_for_response( $icon, $request ); return rest_ensure_response( $data ); } /** * Retrieves a specific icon from the registry. * * @since 7.0.0 * * @param string $name Icon name. * @return array|WP_Error Icon data on success, or WP_Error object on failure. */ public function get_icon( $name ) { $registry = WP_Icons_Registry::get_instance(); $icon = $registry->get_registered_icon( $name ); if ( null === $icon ) { return new WP_Error( 'rest_icon_not_found', sprintf( // translators: %s is the name of any user-provided name __( 'Icon not found: "%s".' ), $name ), array( 'status' => 404 ) ); } return $icon; } /** * Prepare a raw icon before it gets output in a REST API response. * * @since 7.0.0 * @since 7.1.0 Added the `collection` field. * * @param array $item Raw icon as registered, before any changes. * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function prepare_item_for_response( $item, $request ) { $fields = $this->get_fields_for_response( $request ); $keys = array( 'name' => 'name', 'label' => 'label', 'content' => 'content', 'collection' => 'collection', ); $data = array(); foreach ( $keys as $item_key => $rest_key ) { if ( isset( $item[ $item_key ] ) && rest_is_field_included( $rest_key, $fields ) ) { $data[ $rest_key ] = $item[ $item_key ]; } } $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); return rest_ensure_response( $data ); } /** * Retrieves the icon schema, conforming to JSON Schema. * * @since 7.0.0 * @since 7.1.0 Added the `collection` property. * * @return array Item schema data. */ public function get_item_schema() { if ( $this->schema ) { return $this->add_additional_fields_schema( $this->schema ); } $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'icon', 'type' => 'object', 'properties' => array( 'name' => array( 'description' => __( 'The icon name.' ), 'type' => 'string', 'readonly' => true, 'context' => array( 'view', 'edit', 'embed' ), ), 'label' => array( 'description' => __( 'The icon label.' ), 'type' => 'string', 'readonly' => true, 'context' => array( 'view', 'edit', 'embed' ), ), 'content' => array( 'description' => __( 'The icon content (SVG markup).' ), 'type' => 'string', 'readonly' => true, 'context' => array( 'view', 'edit', 'embed' ), ), 'collection' => array( 'description' => __( 'The slug of the collection this icon belongs to.' ), 'type' => 'string', 'readonly' => true, 'context' => array( 'view', 'edit', 'embed' ), ), ), ); $this->schema = $schema; return $this->add_additional_fields_schema( $this->schema ); } /** * Retrieves the query params for the icons collection. * * @since 7.0.0 * @since 7.1.0 Added the `collection` parameter. * * @return array Collection parameters. */ public function get_collection_params() { $query_params = parent::get_collection_params(); $query_params['context']['default'] = 'view'; $query_params['collection'] = array( 'description' => __( 'Limit results to icons belonging to the given collection slug.' ), 'type' => 'string', 'pattern' => '^[a-z0-9]([a-z0-9_-]*[a-z0-9])?$', ); return $query_params; } } } if ( in_array( WP_REST_Search_Controller::PROP_URL, $fields, true ) ) { $data[ WP_REST_Search_Controller::PROP_URL ] = get_permalink( $post->ID ); } if ( in_array( WP_REST_Search_Controller::PROP_TYPE, $fields, true ) ) { $data[ WP_REST_Search_Controller::PROP_TYPE ] = $this->type; } if ( in_array( WP_REST_Search_Controller::PROP_SUBTYPE, $fields, true ) ) { $data[ WP_REST_Search_Controller::PROP_SUBTYPE ] = $post->post_type; } return $data; } /** * Prepares links for the search result of a given ID. * * @since 5.0.0 * * @param int $id Item ID. * @return array Links for the given item. */ public function prepare_item_links( $id ) { $post = get_post( $id ); $links = array(); $item_route = rest_get_route_for_post( $post ); if ( ! empty( $item_route ) ) { $links['self'] = array( 'href' => rest_url( $item_route ), 'embeddable' => true, ); } $links['about'] = array( 'href' => rest_url( 'wp/v2/types/' . $post->post_type ), ); return $links; } /** * Overwrites the default protected and private title format. * * By default, WordPress will show password protected or private posts with a title of * "Protected: %s" or "Private: %s", as the REST API communicates the status of a post * in a machine-readable format, we remove the prefix. * * @since 5.0.0 * * @return string Title format. */ public function protected_title_format() { return '%s'; } /** * Attempts to detect the route to access a single item. * * @since 5.0.0 * @deprecated 5.5.0 Use rest_get_route_for_post() * @see rest_get_route_for_post() * * @param WP_Post $post Post object. * @return string REST route relative to the REST base URI, or empty string if unknown. */ protected function detect_rest_item_route( $post ) { _deprecated_function( __METHOD__, '5.5.0', 'rest_get_route_for_post()' ); return rest_get_route_for_post( $post ); } } $matched_class_name = $class_token; break; } $class_token = strtok( $token_delimiter ); } if ( null === $matched_class_name ) { return $block_content; } $tags = new WP_HTML_Tag_Processor( $block_content ); if ( $tags->next_tag() ) { $tags->add_class( 'has-custom-css' ); $tags->add_class( $matched_class_name ); } return $tags->get_updated_html(); } add_filter( 'render_block', 'wp_render_custom_css_class_name', 10, 2 ); add_filter( 'render_block_data', 'wp_render_custom_css_support_styles', 10, 1 ); add_action( 'wp_enqueue_scripts', 'wp_enqueue_block_custom_css', 1 ); /** * Registers the style block attribute for block types that support it. * * @param WP_Block_Type $block_type Block Type. */ function wp_register_custom_css_support( $block_type ) { // Setup attributes and styles within that if needed. if ( ! $block_type->attributes ) { $block_type->attributes = array(); } // Check for existing style attribute definition e.g. from block.json. if ( array_key_exists( 'style', $block_type->attributes ) ) { return; } $has_custom_css_support = block_has_support( $block_type, array( 'customCSS' ), true ); if ( $has_custom_css_support ) { $block_type->attributes['style'] = array( 'type' => 'object', ); } } /** * Strips custom CSS (`style.css` in attributes) from all blocks in post content. * * Uses {@see WP_Block_Parser::next_token()} to scan block tokens and surgically * replace only the attribute JSON that changed — no parse_blocks() + * serialize_blocks() round-trip needed. * * @since 7.0.0 * @access private * * @param string $content Post content to filter, expected to be escaped with slashes. * @return string Filtered post content with block custom CSS removed. */ function wp_strip_custom_css_from_blocks( $content ) { if ( ! has_blocks( $content ) ) { return $content; } $unslashed = stripslashes( $content ); $parser = new WP_Block_Parser(); $parser->document = $unslashed; $parser->offset = 0; $end = strlen( $unslashed ); $replacements = array(); while ( $parser->offset < $end ) { $next_token = $parser->next_token(); if ( 'no-more-tokens' === $next_token[0] ) { break; } list( $token_type, , $attrs, $start_offset, $token_length ) = $next_token; $parser->offset = $start_offset + $token_length; if ( 'block-opener' !== $token_type && 'void-block' !== $token_type ) { continue; } if ( ! isset( $attrs['style']['css'] ) ) { continue; } // Remove css and clean up empty style. unset( $attrs['style']['css'] ); if ( empty( $attrs['style'] ) ) { unset( $attrs['style'] ); } // Locate the JSON portion within the token. $token_string = substr( $unslashed, $start_offset, $token_length ); $json_rel_start = strcspn( $token_string, '{' ); $json_rel_end = strrpos( $token_string, '}' ); $json_start = $start_offset + $json_rel_start; $json_length = $json_rel_end - $json_rel_start + 1; // Re-encode attributes. If attrs is now empty, remove JSON and trailing space. if ( empty( $attrs ) ) { // Remove the trailing space after JSON. $replacements[] = array( $json_start, $json_length + 1, '' ); } else { $replacements[] = array( $json_start, $json_length, serialize_block_attributes( $attrs ) ); } } if ( empty( $replacements ) ) { return $content; } // Build the result by splicing replacements into the original string. $result = ''; $was_at = 0; foreach ( $replacements as $replacement ) { list( $offset, $length, $new_json ) = $replacement; $result .= substr( $unslashed, $was_at, $offset - $was_at ) . $new_json; $was_at = $offset + $length; } if ( $was_at < $end ) { $result .= substr( $unslashed, $was_at ); } return addslashes( $result ); } /** * Adds the filters to strip custom CSS from block content on save. * Priority of 8 to run before wp_filter_global_styles_post (priority 9) and wp_filter_post_kses (priority 10). * * @since 7.0.0 * @access private */ function wp_custom_css_kses_init_filters() { add_filter( 'content_save_pre', 'wp_strip_custom_css_from_blocks', 8 ); add_filter( 'content_filtered_save_pre', 'wp_strip_custom_css_from_blocks', 8 ); } /** * Removes the filters that strip custom CSS from block content on save. * Priority of 8 to run before wp_filter_global_styles_post (priority 9) and wp_filter_post_kses (priority 10). * * @since 7.0.0 * @access private */ function wp_custom_css_remove_filters() { remove_filter( 'content_save_pre', 'wp_strip_custom_css_from_blocks', 8 ); remove_filter( 'content_filtered_save_pre', 'wp_strip_custom_css_from_blocks', 8 ); } /** * Registers the custom CSS content filters if the user does not have the edit_css capability. * * @since 7.0.0 * @access private */ function wp_custom_css_kses_init() { wp_custom_css_remove_filters(); if ( ! current_user_can( 'edit_css' ) ) { wp_custom_css_kses_init_filters(); } } /** * Initializes custom CSS content filters when imported data should be filtered. * * Runs at priority 999 on {@see 'force_filtered_html_on_import'} to ensure it * fires after general KSES initialization, independently of user capabilities. * If the input of the filter is true it means we are in an import situation and should * enable the custom CSS filters, independently of the user capabilities. * * @since 7.0.0 * @access private * * @param mixed $arg Input argument of the filter. * @return mixed Input argument of the filter. */ function wp_custom_css_force_filtered_html_on_import_filter( $arg ) { if ( $arg ) { wp_custom_css_kses_init_filters(); } return $arg; } // Run before wp_filter_global_styles_post (priority 9) and wp_filter_post_kses (priority 10). add_action( 'init', 'wp_custom_css_kses_init', 20 ); add_action( 'set_current_user', 'wp_custom_css_kses_init' ); add_filter( 'force_filtered_html_on_import', 'wp_custom_css_force_filtered_html_on_import_filter', 999 ); // Register the block support. WP_Block_Supports::get_instance()->register( 'custom-css', array( 'register_attribute' => 'wp_register_custom_css_support', ) );