// Test for invalid characters.
if ( ! preg_match( '/^[a-z0-9-]+$/i', $sub ) ) {
/** This filter is documented in wp-includes/formatting.php */
return apply_filters( 'is_email', false, $email, 'sub_invalid_chars' );
// Congratulations, your email made it!
/** This filter is documented in wp-includes/formatting.php */
return apply_filters( 'is_email', $email, $email, null );
* Convert to ASCII from email subjects.
* @param string $string Subject line
* @return string Converted string to ASCII
function wp_iso_descrambler( $string ) {
/* this may only work with iso-8859-1, I'm afraid */
if ( ! preg_match( '#\=\?(.+)\?Q\?(.+)\?\=#i', $string, $matches ) ) {
$subject = str_replace( '_', ' ', $matches[2] );
return preg_replace_callback( '#\=([0-9a-f]{2})#i', '_wp_iso_convert', $subject );
* Helper function to convert hex encoded chars to ASCII
* @param array $match The preg_replace_callback matches array
* @return string Converted chars
function _wp_iso_convert( $match ) {
return chr( hexdec( strtolower( $match[1] ) ) );
* Given a date in the timezone of the site, returns that date in UTC.
* Requires and returns a date in the Y-m-d H:i:s format.
* Return format can be overridden using the $format parameter.
* @param string $string The date to be converted, in the timezone of the site.
* @param string $format The format string for the returned date. Default 'Y-m-d H:i:s'.
* @return string Formatted version of the date, in UTC.
function get_gmt_from_date( $string, $format = 'Y-m-d H:i:s' ) {
$datetime = date_create( $string, wp_timezone() );
if ( false === $datetime ) {
return gmdate( $format, 0 );
return $datetime->setTimezone( new DateTimeZone( 'UTC' ) )->format( $format );
* Given a date in UTC or GMT timezone, returns that date in the timezone of the site.
* Requires and returns a date in the Y-m-d H:i:s format.
* Return format can be overridden using the $format parameter.
* @param string $string The date to be converted, in UTC or GMT timezone.
* @param string $format The format string for the returned date. Default 'Y-m-d H:i:s'.
* @return string Formatted version of the date, in the site's timezone.
function get_date_from_gmt( $string, $format = 'Y-m-d H:i:s' ) {
$datetime = date_create( $string, new DateTimeZone( 'UTC' ) );
if ( false === $datetime ) {
return gmdate( $format, 0 );
return $datetime->setTimezone( wp_timezone() )->format( $format );
* Given an ISO 8601 timezone, returns its UTC offset in seconds.
* @param string $timezone Either 'Z' for 0 offset or '±hhmm'.
* @return int|float The offset in seconds.
function iso8601_timezone_to_offset( $timezone ) {
// $timezone is either 'Z' or '[+|-]hhmm'.
if ( 'Z' === $timezone ) {
$sign = ( '+' === substr( $timezone, 0, 1 ) ) ? 1 : -1;
$hours = (int) substr( $timezone, 1, 2 );
$minutes = (int) substr( $timezone, 3, 4 ) / 60;
$offset = $sign * HOUR_IN_SECONDS * ( $hours + $minutes );
* Given an ISO 8601 (Ymd\TH:i:sO) date, returns a MySQL DateTime (Y-m-d H:i:s) format used by post_date[_gmt].
* @param string $date_string Date and time in ISO 8601 format {@link https://en.wikipedia.org/wiki/ISO_8601}.
* @param string $timezone Optional. If set to 'gmt' returns the result in UTC. Default 'user'.
* @return string|false The date and time in MySQL DateTime format - Y-m-d H:i:s, or false on failure.
function iso8601_to_datetime( $date_string, $timezone = 'user' ) {
$timezone = strtolower( $timezone );
$wp_timezone = wp_timezone();
$datetime = date_create( $date_string, $wp_timezone ); // Timezone is ignored if input has one.
if ( false === $datetime ) {
if ( 'gmt' === $timezone ) {
return $datetime->setTimezone( new DateTimeZone( 'UTC' ) )->format( 'Y-m-d H:i:s' );
if ( 'user' === $timezone ) {
return $datetime->setTimezone( $wp_timezone )->format( 'Y-m-d H:i:s' );
* Strips out all characters that are not allowable in an email.
* @param string $email Email address to filter.
* @return string Filtered email address.
function sanitize_email( $email ) {
// Test for the minimum length the email can be.
if ( strlen( $email ) < 6 ) {
* Filters a sanitized email address.
* This filter is evaluated under several contexts, including 'email_too_short',
* 'email_no_at', 'local_invalid_chars', 'domain_period_sequence', 'domain_period_limits',
* 'domain_no_periods', 'domain_no_valid_subs', or no context.
* @param string $sanitized_email The sanitized email address.
* @param string $email The email address, as provided to sanitize_email().
* @param string|null $message A message to pass to the user. null if email is sanitized.
return apply_filters( 'sanitize_email', '', $email, 'email_too_short' );
// Test for an @ character after the first position.
if ( strpos( $email, '@', 1 ) === false ) {
/** This filter is documented in wp-includes/formatting.php */
return apply_filters( 'sanitize_email', '', $email, 'email_no_at' );
// Split out the local and domain parts.
list( $local, $domain ) = explode( '@', $email, 2 );
// Test for invalid characters.
$local = preg_replace( '/[^a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~\.-]/', '', $local );
/** This filter is documented in wp-includes/formatting.php */
return apply_filters( 'sanitize_email', '', $email, 'local_invalid_chars' );
// Test for sequences of periods.
$domain = preg_replace( '/\.{2,}/', '', $domain );
/** This filter is documented in wp-includes/formatting.php */
return apply_filters( 'sanitize_email', '', $email, 'domain_period_sequence' );
// Test for leading and trailing periods and whitespace.
$domain = trim( $domain, " \t\n\r\0\x0B." );
/** This filter is documented in wp-includes/formatting.php */
return apply_filters( 'sanitize_email', '', $email, 'domain_period_limits' );
// Split the domain into subs.
$subs = explode( '.', $domain );
// Assume the domain will have at least two subs.
if ( 2 > count( $subs ) ) {
/** This filter is documented in wp-includes/formatting.php */
return apply_filters( 'sanitize_email', '', $email, 'domain_no_periods' );
// Create an array that will contain valid subs.
// Loop through each sub.
foreach ( $subs as $sub ) {
// Test for leading and trailing hyphens.
$sub = trim( $sub, " \t\n\r\0\x0B-" );
// Test for invalid characters.
$sub = preg_replace( '/[^a-z0-9-]+/i', '', $sub );
// If there's anything left, add it to the valid subs.
// If there aren't 2 or more valid subs.
if ( 2 > count( $new_subs ) ) {
/** This filter is documented in wp-includes/formatting.php */
return apply_filters( 'sanitize_email', '', $email, 'domain_no_valid_subs' );
// Join valid subs into the new domain.
$domain = implode( '.', $new_subs );
// Put the email back together.
$sanitized_email = $local . '@' . $domain;
// Congratulations, your email made it!
/** This filter is documented in wp-includes/formatting.php */
return apply_filters( 'sanitize_email', $sanitized_email, $email, null );
* Determines the difference between two timestamps.
* The difference is returned in a human readable format such as "1 hour",
* @since 5.3.0 Added support for showing a difference in seconds.
* @param int $from Unix timestamp from which the difference begins.
* @param int $to Optional. Unix timestamp to end the time difference. Default becomes time() if not set.
* @return string Human readable time difference.
function human_time_diff( $from, $to = 0 ) {
$diff = (int) abs( $to - $from );
if ( $diff < MINUTE_IN_SECONDS ) {
/* translators: Time difference between two dates, in seconds. %s: Number of seconds. */
$since = sprintf( _n( '%s second', '%s seconds', $secs ), $secs );
} elseif ( $diff < HOUR_IN_SECONDS && $diff >= MINUTE_IN_SECONDS ) {
$mins = round( $diff / MINUTE_IN_SECONDS );
/* translators: Time difference between two dates, in minutes (min=minute). %s: Number of minutes. */
$since = sprintf( _n( '%s min', '%s mins', $mins ), $mins );
} elseif ( $diff < DAY_IN_SECONDS && $diff >= HOUR_IN_SECONDS ) {
$hours = round( $diff / HOUR_IN_SECONDS );
/* translators: Time difference between two dates, in hours. %s: Number of hours. */
$since = sprintf( _n( '%s hour', '%s hours', $hours ), $hours );
} elseif ( $diff < WEEK_IN_SECONDS && $diff >= DAY_IN_SECONDS ) {
$days = round( $diff / DAY_IN_SECONDS );
/* translators: Time difference between two dates, in days. %s: Number of days. */
$since = sprintf( _n( '%s day', '%s days', $days ), $days );
} elseif ( $diff < MONTH_IN_SECONDS && $diff >= WEEK_IN_SECONDS ) {
$weeks = round( $diff / WEEK_IN_SECONDS );
/* translators: Time difference between two dates, in weeks. %s: Number of weeks. */
$since = sprintf( _n( '%s week', '%s weeks', $weeks ), $weeks );
} elseif ( $diff < YEAR_IN_SECONDS && $diff >= MONTH_IN_SECONDS ) {
$months = round( $diff / MONTH_IN_SECONDS );
/* translators: Time difference between two dates, in months. %s: Number of months. */
$since = sprintf( _n( '%s month', '%s months', $months ), $months );
} elseif ( $diff >= YEAR_IN_SECONDS ) {
$years = round( $diff / YEAR_IN_SECONDS );
/* translators: Time difference between two dates, in years. %s: Number of years. */
$since = sprintf( _n( '%s year', '%s years', $years ), $years );
* Filters the human readable difference between two timestamps.
* @param string $since The difference in human readable text.
* @param int $diff The difference in seconds.
* @param int $from Unix timestamp from which the difference begins.
* @param int $to Unix timestamp to end the time difference.
return apply_filters( 'human_time_diff', $since, $diff, $from, $to );
* Generates an excerpt from the content, if needed.
* Returns a maximum of 55 words with an ellipsis appended if necessary.
* The 55 word limit can be modified by plugins/themes using the {@see 'excerpt_length'} filter
* The ' […]' string can be modified by plugins/themes using the {@see 'excerpt_more'} filter
* @since 5.2.0 Added the `$post` parameter.
* @param string $text Optional. The excerpt. If set to empty, an excerpt is generated.
* @param WP_Post|object|int $post Optional. WP_Post instance or Post ID/object. Default null.
* @return string The excerpt.
function wp_trim_excerpt( $text = '', $post = null ) {
if ( '' === trim( $text ) ) {
$post = get_post( $post );
$text = get_the_content( '', false, $post );
$text = strip_shortcodes( $text );
$text = excerpt_remove_blocks( $text );
/** This filter is documented in wp-includes/post-template.php */
$text = apply_filters( 'the_content', $text );
$text = str_replace( ']]>', ']]>', $text );
/* translators: Maximum number of words used in a post excerpt. */
$excerpt_length = (int) _x( '55', 'excerpt_length' );
* Filters the maximum number of words in a post excerpt.
* @param int $number The maximum number of words. Default 55.
$excerpt_length = (int) apply_filters( 'excerpt_length', $excerpt_length );
* Filters the string in the "more" link displayed after a trimmed excerpt.
* @param string $more_string The string shown within the more link.
$excerpt_more = apply_filters( 'excerpt_more', ' ' . '[…]' );
$text = wp_trim_words( $text, $excerpt_length, $excerpt_more );
* Filters the trimmed excerpt string.
* @param string $text The trimmed text.
* @param string $raw_excerpt The text prior to trimming.
return apply_filters( 'wp_trim_excerpt', $text, $raw_excerpt );
* Trims text to a certain number of words.
* This function is localized. For languages that count 'words' by the individual
* character (such as East Asian languages), the $num_words argument will apply
* to the number of individual characters.
* @param string $text Text to trim.
* @param int $num_words Number of words. Default 55.
* @param string $more Optional. What to append if $text needs to be trimmed. Default '…'.
* @return string Trimmed text.
function wp_trim_words( $text, $num_words = 55, $more = null ) {
$more = __( '…' );
$text = wp_strip_all_tags( $text );
$num_words = (int) $num_words;
* translators: If your word count is based on single characters (e.g. East Asian characters),
* enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'.
* Do not translate into your own language.
if ( strpos( _x( 'words', 'Word count type. Do not translate!' ), 'characters' ) === 0 && preg_match( '/^utf\-?8$/i', get_option( 'blog_charset' ) ) ) {
$text = trim( preg_replace( "/[\n\r\t ]+/", ' ', $text ), ' ' );
preg_match_all( '/./u', $text, $words_array );
$words_array = array_slice( $words_array[0], 0, $num_words + 1 );
$words_array = preg_split( "/[\n\r\t ]+/", $text, $num_words + 1, PREG_SPLIT_NO_EMPTY );
if ( count( $words_array ) > $num_words ) {
array_pop( $words_array );
$text = implode( $sep, $words_array );
$text = implode( $sep, $words_array );
* Filters the text content after words have been trimmed.
* @param string $text The trimmed text.
* @param int $num_words The number of words to trim the text to. Default 55.
* @param string $more An optional string to append to the end of the trimmed text, e.g. ….
* @param string $original_text The text before it was trimmed.
return apply_filters( 'wp_trim_words', $text, $num_words, $more, $original_text );
* Converts named entities into numbered entities.
* @param string $text The text within which entities will be converted.
* @return string Text with converted entities.
function ent2ncr( $text ) {
* Filters text before named entities are converted into numbered entities.
* A non-null string must be returned for the filter to be evaluated.
* @param string|null $converted_text The text to be converted. Default null.
* @param string $text The text prior to entity conversion.
$filtered = apply_filters( 'pre_ent2ncr', null, $text );
if ( null !== $filtered ) {