'wp_print_styles'} action has been called, the CSS link will
* be printed. Printing may be forced by passing true as the $force_echo
* (second) parameter.
*
* For backward compatibility with WordPress 2.3 calling method: If the $file
* (first) parameter does not correspond to a registered CSS file, we assume
* $file is a file relative to wp-admin/ without its ".css" extension. A
* stylesheet link to that generated URL is printed.
*
* @since 2.3.0
*
* @param string $file Optional. Style handle name or file name (without ".css" extension) relative
* to wp-admin/. Defaults to 'wp-admin'.
* @param bool $force_echo Optional. Force the stylesheet link to be printed rather than enqueued.
*/
function wp_admin_css( $file = 'wp-admin', $force_echo = false ) {
// For backward compatibility.
$handle = str_starts_with( $file, 'css/' ) ? substr( $file, 4 ) : $file;
if ( wp_styles()->query( $handle ) ) {
if ( $force_echo || did_action( 'wp_print_styles' ) ) {
// We already printed the style queue. Print this one immediately.
wp_print_styles( $handle );
} else {
// Add to style queue.
wp_enqueue_style( $handle );
}
return;
}
$stylesheet_link = sprintf(
"\n",
esc_url( wp_admin_css_uri( $file ) )
);
/**
* Filters the stylesheet link to the specified CSS file.
*
* If the site is set to display right-to-left, the RTL stylesheet link
* will be used instead.
*
* @since 2.3.0
* @param string $stylesheet_link HTML link element for the stylesheet.
* @param string $file Style handle name or filename (without ".css" extension)
* relative to wp-admin/. Defaults to 'wp-admin'.
*/
echo apply_filters( 'wp_admin_css', $stylesheet_link, $file );
if ( function_exists( 'is_rtl' ) && is_rtl() ) {
$rtl_stylesheet_link = sprintf(
"\n",
esc_url( wp_admin_css_uri( "$file-rtl" ) )
);
/** This filter is documented in wp-includes/general-template.php */
echo apply_filters( 'wp_admin_css', $rtl_stylesheet_link, "$file-rtl" );
}
}
/**
* Enqueues the default ThickBox js and css.
*
* If any of the settings need to be changed, this can be done with another js
* file similar to media-upload.js. That file should
* require array('thickbox') to ensure it is loaded after.
*
* @since 2.5.0
*/
function add_thickbox() {
wp_enqueue_script( 'thickbox' );
wp_enqueue_style( 'thickbox' );
if ( is_network_admin() ) {
add_action( 'admin_head', '_thickbox_path_admin_subfolder' );
}
}
/**
* Displays the XHTML generator that is generated on the wp_head hook.
*
* See {@see 'wp_head'}.
*
* @since 2.5.0
*/
function wp_generator() {
/**
* Filters the output of the XHTML generator tag.
*
* @since 2.5.0
*
* @param string $generator_type The XHTML generator.
*/
the_generator( apply_filters( 'wp_generator_type', 'xhtml' ) );
}
/**
* Displays the generator XML or Comment for RSS, ATOM, etc.
*
* Returns the correct generator type for the requested output format. Allows
* for a plugin to filter generators overall the {@see 'the_generator'} filter.
*
* @since 2.5.0
*
* @param string $type The type of generator to output - (html|xhtml|atom|rss2|rdf|comment|export).
*/
function the_generator( $type ) {
/**
* Filters the output of the XHTML generator tag for display.
*
* @since 2.5.0
*
* @param string $generator_type The generator output.
* @param string $type The type of generator to output. Accepts 'html',
* 'xhtml', 'atom', 'rss2', 'rdf', 'comment', 'export'.
*/
echo apply_filters( 'the_generator', get_the_generator( $type ), $type ) . "\n";
}
/**
* Creates the generator XML or Comment for RSS, ATOM, etc.
*
* Returns the correct generator type for the requested output format. Allows
* for a plugin to filter generators on an individual basis using the
* {@see 'get_the_generator_$type'} filter.
*
* @since 2.5.0
*
* @param string $type The type of generator to return - (html|xhtml|atom|rss2|rdf|comment|export).
* @return string|void The HTML content for the generator.
*/
function get_the_generator( $type = '' ) {
if ( empty( $type ) ) {
$current_filter = current_filter();
if ( empty( $current_filter ) ) {
return;
}
switch ( $current_filter ) {
case 'rss2_head':
case 'commentsrss2_head':
$type = 'rss2';
break;
case 'rss_head':
case 'opml_head':
$type = 'comment';
break;
case 'rdf_header':
$type = 'rdf';
break;
case 'atom_head':
case 'comments_atom_head':
case 'app_head':
$type = 'atom';
break;
}
}
switch ( $type ) {
case 'html':
$gen = '';
break;
case 'xhtml':
$gen = '';
break;
case 'atom':
$gen = 'WordPress';
break;
case 'rss2':
$gen = '' . sanitize_url( 'https://wordpress.org/?v=' . get_bloginfo_rss( 'version' ) ) . '';
break;
case 'rdf':
$gen = '';
break;
case 'comment':
$gen = '';
break;
case 'export':
$gen = '';
break;
}
/**
* Filters the HTML for the retrieved generator type.
*
* The dynamic portion of the hook name, `$type`, refers to the generator type.
*
* Possible hook names include:
*
* - `get_the_generator_atom`
* - `get_the_generator_comment`
* - `get_the_generator_export`
* - `get_the_generator_html`
* - `get_the_generator_rdf`
* - `get_the_generator_rss2`
* - `get_the_generator_xhtml`
*
* @since 2.5.0
*
* @param string $gen The HTML markup output to wp_head().
* @param string $type The type of generator. Accepts 'html', 'xhtml', 'atom',
* 'rss2', 'rdf', 'comment', 'export'.
*/
return apply_filters( "get_the_generator_{$type}", $gen, $type );
}
/**
* Outputs the HTML checked attribute.
*
* Compares the first two arguments and if identical marks as checked.
*
* @since 1.0.0
*
* @param mixed $checked One of the values to compare.
* @param mixed $current Optional. The other value to compare if not just true.
* Default true.
* @param bool $display Optional. Whether to echo or just return the string.
* Default true.
* @return string HTML attribute or empty string.
*/
function checked( $checked, $current = true, $display = true ) {
return __checked_selected_helper( $checked, $current, $display, 'checked' );
}
/**
* Outputs the HTML selected attribute.
*
* Compares the first two arguments and if identical marks as selected.
*
* @since 1.0.0
*
* @param mixed $selected One of the values to compare.
* @param mixed $current Optional. The other value to compare if not just true.
* Default true.
* @param bool $display Optional. Whether to echo or just return the string.
* Default true.
* @return string HTML attribute or empty string.
*/
function selected( $selected, $current = true, $display = true ) {
return __checked_selected_helper( $selected, $current, $display, 'selected' );
}
/**
* Outputs the HTML disabled attribute.
*
* Compares the first two arguments and if identical marks as disabled.
*
* @since 3.0.0
*
* @param mixed $disabled One of the values to compare.
* @param mixed $current Optional. The other value to compare if not just true.
* Default true.
* @param bool $display Optional. Whether to echo or just return the string.
* Default true.
* @return string HTML attribute or empty string.
*/
function disabled( $disabled, $current = true, $display = true ) {
return __checked_selected_helper( $disabled, $current, $display, 'disabled' );
}
/**
* Outputs the HTML readonly attribute.
*
* Compares the first two arguments and if identical marks as readonly.
*
* @since 5.9.0
*
* @param mixed $readonly_value One of the values to compare.
* @param mixed $current Optional. The other value to compare if not just true.
* Default true.
* @param bool $display Optional. Whether to echo or just return the string.
* Default true.
* @return string HTML attribute or empty string.
*/
function wp_readonly( $readonly_value, $current = true, $display = true ) {
return __checked_selected_helper( $readonly_value, $current, $display, 'readonly' );
}
/*
* Include a compat `readonly()` function on PHP < 8.1. Since PHP 8.1,
* `readonly` is a reserved keyword and cannot be used as a function name.
* In order to avoid PHP parser errors, this function was extracted
* to a separate file and is only included conditionally on PHP < 8.1.
*/
if ( PHP_VERSION_ID < 80100 ) {
require_once __DIR__ . '/php-compat/readonly.php';
}
/**
* Private helper function for checked, selected, disabled and readonly.
*
* Compares the first two arguments and if identical marks as `$type`.
*
* @since 2.8.0
* @access private
*
* @param mixed $helper One of the values to compare.
* @param mixed $current The other value to compare if not just true.
* @param bool $display Whether to echo or just return the string.
* @param string $type The type of checked|selected|disabled|readonly we are doing.
* @return string HTML attribute or empty string.
*/
function __checked_selected_helper( $helper, $current, $display, $type ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.FunctionDoubleUnderscore
if ( (string) $helper === (string) $current ) {
$result = " $type='$type'";
} else {
$result = '';
}
if ( $display ) {
echo $result;
}
return $result;
}
/**
* Assigns a visual indicator for required form fields.
*
* @since 6.1.0
*
* @return string Indicator glyph wrapped in a `span` tag.
*/
function wp_required_field_indicator() {
/* translators: Character to identify required form fields. */
$glyph = __( '*' );
$indicator = '' . esc_html( $glyph ) . '';
/**
* Filters the markup for a visual indicator of required form fields.
*
* @since 6.1.0
*
* @param string $indicator Markup for the indicator element.
*/
return apply_filters( 'wp_required_field_indicator', $indicator );
}
/**
* Creates a message to explain required form fields.
*
* @since 6.1.0
*
* @return string Message text and glyph wrapped in a `span` tag.
*/
function wp_required_field_message() {
$message = sprintf(
'%s',
/* translators: %s: Asterisk symbol (*). */
sprintf( __( 'Required fields are marked %s' ), wp_required_field_indicator() )
);
/**
* Filters the message to explain required form fields.
*
* @since 6.1.0
*
* @param string $message Message text and glyph wrapped in a `span` tag.
*/
return apply_filters( 'wp_required_field_message', $message );
}
/**
* Default settings for heartbeat.
*
* Outputs the nonce used in the heartbeat XHR.
*
* @since 3.6.0
*
* @param array $settings
* @return array Heartbeat settings.
*/
function wp_heartbeat_settings( $settings ) {
if ( ! is_admin() ) {
$settings['ajaxurl'] = admin_url( 'admin-ajax.php', 'relative' );
}
if ( is_user_logged_in() ) {
$settings['nonce'] = wp_create_nonce( 'heartbeat-nonce' );
}
return $settings;
}
if ( true === $value ) {
$updated_attribute = $name;
} else {
$escaped_new_value = esc_attr( $value );
$updated_attribute = "{$name}=\"{$escaped_new_value}\"";
}
/*
* > There must never be two or more attributes on
* > the same start tag whose names are an ASCII
* > case-insensitive match for each other.
* - HTML 5 spec
*
* @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive
*/
$comparable_name = strtolower( $name );
if ( isset( $this->attributes[ $comparable_name ] ) ) {
/*
* Update an existing attribute.
*
* Example – set attribute id to "new" in
:
*
*
* ^-------------^
* start end
* replacement: `id="new"`
*
* Result:
*/
$existing_attribute = $this->attributes[ $comparable_name ];
$this->lexical_updates[ $comparable_name ] = new WP_HTML_Text_Replacement(
$existing_attribute->start,
$existing_attribute->end,
$updated_attribute
);
} else {
/*
* Create a new attribute at the tag's name end.
*
* Example – add attribute id="new" to :
*
*
* ^
* start and end
* replacement: ` id="new"`
*
* Result:
*/
$this->lexical_updates[ $comparable_name ] = new WP_HTML_Text_Replacement(
$this->tag_name_starts_at + $this->tag_name_length,
$this->tag_name_starts_at + $this->tag_name_length,
' ' . $updated_attribute
);
}
/*
* Any calls to update the `class` attribute directly should wipe out any
* enqueued class changes from `add_class` and `remove_class`.
*/
if ( 'class' === $comparable_name && ! empty( $this->classname_updates ) ) {
$this->classname_updates = array();
}
return true;
}
/**
* Remove an attribute from the currently-matched tag.
*
* @since 6.2.0
*
* @param string $name The attribute name to remove.
* @return bool Whether an attribute was removed.
*/
public function remove_attribute( $name ) {
if ( $this->is_closing_tag ) {
return false;
}
/*
* > There must never be two or more attributes on
* > the same start tag whose names are an ASCII
* > case-insensitive match for each other.
* - HTML 5 spec
*
* @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive
*/
$name = strtolower( $name );
/*
* Any calls to update the `class` attribute directly should wipe out any
* enqueued class changes from `add_class` and `remove_class`.
*/
if ( 'class' === $name && count( $this->classname_updates ) !== 0 ) {
$this->classname_updates = array();
}
/*
* If updating an attribute that didn't exist in the input
* document, then remove the enqueued update and move on.
*
* For example, this might occur when calling `remove_attribute()`
* after calling `set_attribute()` for the same attribute
* and when that attribute wasn't originally present.
*/
if ( ! isset( $this->attributes[ $name ] ) ) {
if ( isset( $this->lexical_updates[ $name ] ) ) {
unset( $this->lexical_updates[ $name ] );
}
return false;
}
/*
* Removes an existing tag attribute.
*
* Example – remove the attribute id from :
*
* ^-------------^
* start end
* replacement: ``
*
* Result:
*/
$this->lexical_updates[ $name ] = new WP_HTML_Text_Replacement(
$this->attributes[ $name ]->start,
$this->attributes[ $name ]->end,
''
);
// Removes any duplicated attributes if they were also present.
if ( null !== $this->duplicate_attributes && array_key_exists( $name, $this->duplicate_attributes ) ) {
foreach ( $this->duplicate_attributes[ $name ] as $attribute_token ) {
$this->lexical_updates[] = new WP_HTML_Text_Replacement(
$attribute_token->start,
$attribute_token->end,
''
);
}
}
return true;
}
/**
* Adds a new class name to the currently matched tag.
*
* @since 6.2.0
*
* @param string $class_name The class name to add.
* @return bool Whether the class was set to be added.
*/
public function add_class( $class_name ) {
if ( $this->is_closing_tag ) {
return false;
}
if ( null !== $this->tag_name_starts_at ) {
$this->classname_updates[ $class_name ] = self::ADD_CLASS;
}
return true;
}
/**
* Removes a class name from the currently matched tag.
*
* @since 6.2.0
*
* @param string $class_name The class name to remove.
* @return bool Whether the class was set to be removed.
*/
public function remove_class( $class_name ) {
if ( $this->is_closing_tag ) {
return false;
}
if ( null !== $this->tag_name_starts_at ) {
$this->classname_updates[ $class_name ] = self::REMOVE_CLASS;
}
return true;
}
/**
* Returns the string representation of the HTML Tag Processor.
*
* @since 6.2.0
*
* @see WP_HTML_Tag_Processor::get_updated_html()
*
* @return string The processed HTML.
*/
public function __toString() {
return $this->get_updated_html();
}
/**
* Returns the string representation of the HTML Tag Processor.
*
* @since 6.2.0
* @since 6.2.1 Shifts the internal cursor corresponding to the applied updates.
*
* @return string The processed HTML.
*/
public function get_updated_html() {
$requires_no_updating = 0 === count( $this->classname_updates ) && 0 === count( $this->lexical_updates );
/*
* When there is nothing more to update and nothing has already been
* updated, return the original document and avoid a string copy.
*/
if ( $requires_no_updating ) {
return $this->html;
}
/*
* Keep track of the position right before the current tag. This will
* be necessary for reparsing the current tag after updating the HTML.
*/
$before_current_tag = $this->tag_name_starts_at - 1;
/*
* 1. Apply the enqueued edits and update all the pointers to reflect those changes.
*/
$this->class_name_updates_to_attributes_updates();
$before_current_tag += $this->apply_attributes_updates( $before_current_tag );
/*
* 2. Rewind to before the current tag and reparse to get updated attributes.
*
* At this point the internal cursor points to the end of the tag name.
* Rewind before the tag name starts so that it's as if the cursor didn't
* move; a call to `next_tag()` will reparse the recently-updated attributes
* and additional calls to modify the attributes will apply at this same
* location.
*
*
Previous HTMLMore HTML
* ^ | back up by the length of the tag name plus the opening <
* \<-/ back up by strlen("em") + 1 ==> 3
*/
// Store existing state so it can be restored after reparsing.
$previous_parsed_byte_count = $this->bytes_already_parsed;
$previous_query = $this->last_query;
// Reparse attributes.
$this->bytes_already_parsed = $before_current_tag;
$this->next_tag();
// Restore previous state.
$this->bytes_already_parsed = $previous_parsed_byte_count;
$this->parse_query( $previous_query );
return $this->html;
}
/**
* Parses tag query input into internal search criteria.
*
* @since 6.2.0
*
* @param array|string|null $query {
* Optional. Which tag name to find, having which class, etc. Default is to find any tag.
*
* @type string|null $tag_name Which tag to find, or `null` for "any tag."
* @type int|null $match_offset Find the Nth tag matching all search criteria.
* 1 for "first" tag, 3 for "third," etc.
* Defaults to first tag.
* @type string|null $class_name Tag must contain this class name to match.
* @type string $tag_closers "visit" or "skip": whether to stop on tag closers, e.g. .
* }
*/
private function parse_query( $query ) {
if ( null !== $query && $query === $this->last_query ) {
return;
}
$this->last_query = $query;
$this->sought_tag_name = null;
$this->sought_class_name = null;
$this->sought_match_offset = 1;
$this->stop_on_tag_closers = false;
// A single string value means "find the tag of this name".
if ( is_string( $query ) ) {
$this->sought_tag_name = $query;
return;
}
// An empty query parameter applies no restrictions on the search.
if ( null === $query ) {
return;
}
// If not using the string interface, an associative array is required.
if ( ! is_array( $query ) ) {
_doing_it_wrong(
__METHOD__,
__( 'The query argument must be an array or a tag name.' ),
'6.2.0'
);
return;
}
if ( isset( $query['tag_name'] ) && is_string( $query['tag_name'] ) ) {
$this->sought_tag_name = $query['tag_name'];
}
if ( isset( $query['class_name'] ) && is_string( $query['class_name'] ) ) {
$this->sought_class_name = $query['class_name'];
}
if ( isset( $query['match_offset'] ) && is_int( $query['match_offset'] ) && 0 < $query['match_offset'] ) {
$this->sought_match_offset = $query['match_offset'];
}
if ( isset( $query['tag_closers'] ) ) {
$this->stop_on_tag_closers = 'visit' === $query['tag_closers'];
}
}
/**
* Checks whether a given tag and its attributes match the search criteria.
*
* @since 6.2.0
*
* @return boolean Whether the given tag and its attribute match the search criteria.
*/
private function matches() {
if ( $this->is_closing_tag && ! $this->stop_on_tag_closers ) {
return false;
}
// Does the tag name match the requested tag name in a case-insensitive manner?
if ( null !== $this->sought_tag_name ) {
/*
* String (byte) length lookup is fast. If they aren't the
* same length then they can't be the same string values.
*/
if ( strlen( $this->sought_tag_name ) !== $this->tag_name_length ) {
return false;
}
/*
* Check each character to determine if they are the same.
* Defer calls to `strtoupper()` to avoid them when possible.
* Calling `strcasecmp()` here tested slowed than comparing each
* character, so unless benchmarks show otherwise, it should
* not be used.
*
* It's expected that most of the time that this runs, a
* lower-case tag name will be supplied and the input will
* contain lower-case tag names, thus normally bypassing
* the case comparison code.
*/
for ( $i = 0; $i < $this->tag_name_length; $i++ ) {
$html_char = $this->html[ $this->tag_name_starts_at + $i ];
$tag_char = $this->sought_tag_name[ $i ];
if ( $html_char !== $tag_char && strtoupper( $html_char ) !== $tag_char ) {
return false;
}
}
}
$needs_class_name = null !== $this->sought_class_name;
if ( $needs_class_name && ! isset( $this->attributes['class'] ) ) {
return false;
}
/*
* Match byte-for-byte (case-sensitive and encoding-form-sensitive) on the class name.
*
* This will overlook certain classes that exist in other lexical variations
* than was supplied to the search query, but requires more complicated searching.
*/
if ( $needs_class_name ) {
$class_start = $this->attributes['class']->value_starts_at;
$class_end = $class_start + $this->attributes['class']->value_length;
$class_at = $class_start;
/*
* Ensure that boundaries surround the class name to avoid matching on
* substrings of a longer name. For example, the sequence "not-odd"
* should not match for the class "odd" even though "odd" is found
* within the class attribute text.
*
* See https://html.spec.whatwg.org/#attributes-3
* See https://html.spec.whatwg.org/#space-separated-tokens
*/
while (
// phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
false !== ( $class_at = strpos( $this->html, $this->sought_class_name, $class_at ) ) &&
$class_at < $class_end
) {
/*
* Verify this class starts at a boundary.
*/
if ( $class_at > $class_start ) {
$character = $this->html[ $class_at - 1 ];
if ( ' ' !== $character && "\t" !== $character && "\f" !== $character && "\r" !== $character && "\n" !== $character ) {
$class_at += strlen( $this->sought_class_name );
continue;
}
}
/*
* Verify this class ends at a boundary as well.
*/
if ( $class_at + strlen( $this->sought_class_name ) < $class_end ) {
$character = $this->html[ $class_at + strlen( $this->sought_class_name ) ];
if ( ' ' !== $character && "\t" !== $character && "\f" !== $character && "\r" !== $character && "\n" !== $character ) {
$class_at += strlen( $this->sought_class_name );
continue;
}
}
return true;
}
return false;
}
return true;
}
}