* Converts a duration to human readable format.
* @param string $duration Duration will be in string format (HH:ii:ss) OR (ii:ss),
* with a possible prepended negative sign (-).
* @return string|false A human readable duration string, false on failure.
function human_readable_duration( $duration = '' ) {
if ( ( empty( $duration ) || ! is_string( $duration ) ) ) {
$duration = trim( $duration );
// Remove prepended negative sign.
if ( str_starts_with( $duration, '-' ) ) {
$duration = substr( $duration, 1 );
// Extract duration parts.
$duration_parts = array_reverse( explode( ':', $duration ) );
$duration_count = count( $duration_parts );
if ( 3 === $duration_count ) {
// Validate HH:ii:ss duration format.
if ( ! ( (bool) preg_match( '/^([0-9]+):([0-5]?[0-9]):([0-5]?[0-9])$/', $duration ) ) ) {
// Three parts: hours, minutes & seconds.
list( $second, $minute, $hour ) = $duration_parts;
} elseif ( 2 === $duration_count ) {
// Validate ii:ss duration format.
if ( ! ( (bool) preg_match( '/^([0-5]?[0-9]):([0-5]?[0-9])$/', $duration ) ) ) {
// Two parts: minutes & seconds.
list( $second, $minute ) = $duration_parts;
$human_readable_duration = array();
// Add the hour part to the string.
if ( is_numeric( $hour ) ) {
/* translators: %s: Time duration in hour or hours. */
$human_readable_duration[] = sprintf( _n( '%s hour', '%s hours', $hour ), (int) $hour );
// Add the minute part to the string.
if ( is_numeric( $minute ) ) {
/* translators: %s: Time duration in minute or minutes. */
$human_readable_duration[] = sprintf( _n( '%s minute', '%s minutes', $minute ), (int) $minute );
// Add the second part to the string.
if ( is_numeric( $second ) ) {
/* translators: %s: Time duration in second or seconds. */
$human_readable_duration[] = sprintf( _n( '%s second', '%s seconds', $second ), (int) $second );
return implode( ', ', $human_readable_duration );
* Gets the week start and end from the datetime or date string from MySQL.
* @param string $mysqlstring Date or datetime field type from MySQL.
* @param int|string $start_of_week Optional. Start of the week as an integer. Default empty string.
* Week start and end dates as Unix timestamps.
* @type int $start The week start date as a Unix timestamp.
* @type int $end The week end date as a Unix timestamp.
function get_weekstartend( $mysqlstring, $start_of_week = '' ) {
$my = substr( $mysqlstring, 0, 4 );
$mm = substr( $mysqlstring, 8, 2 );
$md = substr( $mysqlstring, 5, 2 );
// The timestamp for MySQL string day.
$day = mktime( 0, 0, 0, $md, $mm, $my );
// The day of the week from the timestamp.
$weekday = (int) gmdate( 'w', $day );
if ( ! is_numeric( $start_of_week ) ) {
$start_of_week = (int) get_option( 'start_of_week' );
if ( $weekday < $start_of_week ) {
// The most recent week start day on or before $day.
$start = $day - DAY_IN_SECONDS * ( $weekday - $start_of_week );
// $start + 1 week - 1 second.
$end = $start + WEEK_IN_SECONDS - 1;
return compact( 'start', 'end' );
* Serializes data, if needed.
* @param string|array|object $data Data that might be serialized.
* @return mixed A scalar data.
function maybe_serialize( $data ) {
if ( is_array( $data ) || is_object( $data ) ) {
return serialize( $data );
* Double serialization is required for backward compatibility.
* See https://core.trac.wordpress.org/ticket/12930
* Also the world will end. See WP 3.6.1.
if ( is_serialized( $data, false ) ) {
return serialize( $data );
* Unserializes data only if it was serialized.
* @param string $data Data that might be unserialized.
* @return mixed Unserialized data can be any type.
function maybe_unserialize( $data ) {
if ( is_serialized( $data ) ) { // Don't attempt to unserialize data that wasn't serialized going in.
return @unserialize( trim( $data ) );
* Checks value to find if it was serialized.
* If $data is not a string, then returned value will always be false.
* Serialized data is always a string.
* @since 6.1.0 Added Enum support.
* @param string $data Value to check to see if was serialized.
* @param bool $strict Optional. Whether to be strict about the end of the string. Default true.
* @return bool False if not serialized and true if it was.
function is_serialized( $data, $strict = true ) {
// If it isn't a string, it isn't serialized.
if ( ! is_string( $data ) ) {
if ( strlen( $data ) < 4 ) {
if ( ':' !== $data[1] ) {
$lastc = substr( $data, -1 );
if ( ';' !== $lastc && '}' !== $lastc ) {
$semicolon = strpos( $data, ';' );
$brace = strpos( $data, '}' );
// Either ; or } must exist.
if ( false === $semicolon && false === $brace ) {
// But neither must be in the first X characters.
if ( false !== $semicolon && $semicolon < 3 ) {
if ( false !== $brace && $brace < 4 ) {
if ( '"' !== substr( $data, -2, 1 ) ) {
} elseif ( ! str_contains( $data, '"' ) ) {
return (bool) preg_match( "/^{$token}:[0-9]+:/s", $data );
$end = $strict ? '$' : '';
return (bool) preg_match( "/^{$token}:[0-9.E+-]+;$end/", $data );
* Checks whether serialized data is of string type.
* @param string $data Serialized data.
* @return bool False if not a serialized string, true if it is.
function is_serialized_string( $data ) {
// if it isn't a string, it isn't a serialized string.
if ( ! is_string( $data ) ) {
if ( strlen( $data ) < 4 ) {
} elseif ( ':' !== $data[1] ) {
} elseif ( ! str_ends_with( $data, ';' ) ) {
} elseif ( 's' !== $data[0] ) {
} elseif ( '"' !== substr( $data, -2, 1 ) ) {
* Retrieves post title from XML-RPC XML.
* If the `title` element is not found in the XML, the default post title
* from the `$post_default_title` global will be used instead.
* @global string $post_default_title Default XML-RPC post title.
* @param string $content XML-RPC XML Request content.
* @return string Post title.
function xmlrpc_getposttitle( $content ) {
global $post_default_title;
if ( preg_match( '/<title>(.+?)<\/title>/is', $content, $matchtitle ) ) {
$post_title = $matchtitle[1];
$post_title = $post_default_title;
* Retrieves the post category or categories from XML-RPC XML.
* If the `category` element is not found in the XML, the default post category
* from the `$post_default_category` global will be used instead.
* The return type will then be a string.
* If the `category` element is found, the return type will be an array.
* @global string $post_default_category Default XML-RPC post category.
* @param string $content XML-RPC XML Request content.
* @return string[]|string An array of category names or default category name.
function xmlrpc_getpostcategory( $content ) {
global $post_default_category;
if ( preg_match( '/<category>(.+?)<\/category>/is', $content, $matchcat ) ) {
$post_category = trim( $matchcat[1], ',' );
$post_category = explode( ',', $post_category );
$post_category = $post_default_category;
* XML-RPC XML content without title and category elements.
* @param string $content XML-RPC XML Request content.
* @return string XML-RPC XML Request content without title and category elements.
function xmlrpc_removepostdata( $content ) {
$content = preg_replace( '/<title>(.+?)<\/title>/si', '', $content );
$content = preg_replace( '/<category>(.+?)<\/category>/si', '', $content );
$content = trim( $content );
* Uses RegEx to extract URLs from arbitrary content.
* @since 6.0.0 Fixes support for HTML entities (Trac 30580).
* @param string $content Content to extract URLs from.
* @return string[] Array of URLs found in passed string.
function wp_extract_urls( $content ) {
. "[^`!()\[\]{}:'\".,<>«»“”‘’\s]|"
$post_links = array_unique(
static function ( $link ) {
// Decode to replace valid entities, like &.
$link = html_entity_decode( $link );
// Maintain backward compatibility by removing extraneous semi-colons (`;`).
return str_replace( ';', '', $link );
return array_values( $post_links );
* Checks content for video and audio links to add as enclosures.
* Will not add enclosures that have already been added and will
* remove enclosures that are no longer in the post. This is called as
* pingbacks and trackbacks.
* @since 5.3.0 The `$content` parameter was made optional, and the `$post` parameter was
* updated to accept a post ID or a WP_Post object.
* @since 5.6.0 The `$content` parameter is no longer optional, but passing `null` to skip it
* @global wpdb $wpdb WordPress database abstraction object.
* @param string|null $content Post content. If `null`, the `post_content` field from `$post` is used.
* @param int|WP_Post $post Post ID or post object.
* @return void|false Void on success, false if the post is not found.
function do_enclose( $content, $post ) {
// @todo Tidy this code and make the debug code optional.
require_once ABSPATH . WPINC . '/class-IXR.php';
$post = get_post( $post );
if ( null === $content ) {
$content = $post->post_content;
$pung = get_enclosed( $post->ID );
$post_links_temp = wp_extract_urls( $content );
foreach ( $pung as $link_test ) {
// Link is no longer in post.
if ( ! in_array( $link_test, $post_links_temp, true ) ) {
$mids = $wpdb->get_col( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE post_id = %d AND meta_key = 'enclosure' AND meta_value LIKE %s", $post->ID, $wpdb->esc_like( $link_test ) . '%' ) );
foreach ( $mids as $mid ) {
delete_metadata_by_mid( 'post', $mid );
foreach ( (array) $post_links_temp as $link_test ) {
// If we haven't pung it already.
if ( ! in_array( $link_test, $pung, true ) ) {
$test = parse_url( $link_test );
if ( isset( $test['query'] ) ) {
$post_links[] = $link_test;
} elseif ( isset( $test['path'] ) && ( '/' !== $test['path'] ) && ( '' !== $test['path'] ) ) {
$post_links[] = $link_test;
* Filters the list of enclosure links before querying the database.
* Allows for the addition and/or removal of potential enclosures to save
* to postmeta before checking the database for existing enclosures.
* @param string[] $post_links An array of enclosure links.
* @param int $post_id Post ID.
$post_links = apply_filters( 'enclosure_links', $post_links, $post->ID );
foreach ( (array) $post_links as $url ) {
$url = strip_fragment_from_url( $url );
if ( '' !== $url && ! $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE post_id = %d AND meta_key = 'enclosure' AND meta_value LIKE %s", $post->ID, $wpdb->esc_like( $url ) . '%' ) ) ) {
$headers = wp_get_http_headers( $url );
$len = isset( $headers['Content-Length'] ) ? (int) $headers['Content-Length'] : 0;
$type = isset( $headers['Content-Type'] ) ? $headers['Content-Type'] : '';
$allowed_types = array( 'video', 'audio' );
// Check to see if we can figure out the mime type from the extension.
$url_parts = parse_url( $url );
if ( false !== $url_parts && ! empty( $url_parts['path'] ) ) {
$extension = pathinfo( $url_parts['path'], PATHINFO_EXTENSION );
if ( ! empty( $extension ) ) {
foreach ( wp_get_mime_types() as $exts => $mime ) {
if ( preg_match( '!^(' . $exts . ')$!i', $extension ) ) {
if ( in_array( substr( $type, 0, strpos( $type, '/' ) ), $allowed_types, true ) ) {
add_post_meta( $post->ID, 'enclosure', "$url\n$len\n$mime\n" );
* Retrieves HTTP Headers from URL.
* @param string $url URL to retrieve HTTP headers from.
* @param bool $deprecated Not Used.
* @return \WpOrg\Requests\Utility\CaseInsensitiveDictionary|false Headers on success, false on failure.
function wp_get_http_headers( $url, $deprecated = false ) {
if ( ! empty( $deprecated ) ) {
_deprecated_argument( __FUNCTION__, '2.7.0' );
$response = wp_safe_remote_head( $url );
if ( is_wp_error( $response ) ) {
return wp_remote_retrieve_headers( $response );