* Utility helpers for LiteSpeed Cache.
defined( 'WPINC' ) || exit();
* Miscellaneous utility methods used across the plugin.
class Utility extends Root {
* Cached list of extra internal domains.
* @var array<int,string>|null
private static $_internal_domains;
* Validate a list of regex rules by attempting to compile them.
* @since 3.0 Moved here from admin-settings.cls
* @param array<int,string> $rules Regex fragments (without delimiters).
* @return bool True for valid rules, false otherwise.
public static function syntax_checker( $rules ) {
return false !== preg_match( self::arr2regex( $rules ), '' );
* Combine an array of strings into a single alternation regex.
* @param array<int,string> $arr List of strings.
* @param bool $drop_delimiter When true, return without regex delimiters.
* @return string Regex pattern.
public static function arr2regex( $arr, $drop_delimiter = false ) {
$arr = self::sanitize_lines( $arr );
$new_arr[] = preg_quote( $v, '#' );
$regex = implode( '|', $new_arr );
$regex = str_replace( ' ', '\\ ', $regex );
return '#' . $regex . '#';
* Replace wildcard characters in a string/array with their regex equivalents.
* @param string|array<int,string> $value String or list of strings.
* @return string|array<int,string>
public static function wildcard2regex( $value ) {
if ( is_array( $value ) ) {
return array_map( __CLASS__ . '::wildcard2regex', $value );
if ( false !== strpos( $value, '*' ) ) {
$value = preg_quote( $value, '#' );
$value = str_replace( '\*', '.*', $value );
* Get current page type string.
* @return string Page type.
public static function page_type() {
if ( $wp_query->is_page ) {
$page_type = is_front_page() ? 'front' : 'page';
} elseif ( $wp_query->is_home ) {
} elseif ( $wp_query->is_single ) {
$page_type = get_post_type();
} elseif ( $wp_query->is_category ) {
} elseif ( $wp_query->is_tag ) {
} elseif ( $wp_query->is_tax ) {
} elseif ( $wp_query->is_archive ) {
if ( $wp_query->is_day ) {
} elseif ( $wp_query->is_month ) {
} elseif ( $wp_query->is_year ) {
} elseif ( $wp_query->is_author ) {
} elseif ( $wp_query->is_search ) {
} elseif ( $wp_query->is_404 ) {
* Get ping speed to a domain via HTTP HEAD timing.
* @param string $domain Domain or URL.
* @return int Milliseconds (99999 on error).
public static function ping( $domain ) {
if ( false !== strpos( $domain, ':' ) ) {
$host = wp_parse_url( $domain, PHP_URL_HOST );
$domain = $host ? $host : $domain;
$starttime = microtime(true);
$file = fsockopen($domain, 443, $errno, $errstr, 10); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fsockopen
$stoptime = microtime(true);
fclose($file); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
$status = ($stoptime - $starttime) * 1000;
$status = floor($status);
Debug2::debug("[Util] ping [Domain] $domain \t[Speed] $status");
* Convert seconds/timestamp to a readable relative time.
* @param int $seconds_or_timestamp Seconds or 10-digit timestamp.
* @param int $timeout If older than this, show absolute time.
* @param bool $forward When true, omit "ago".
* @return string Human readable time.
public static function readable_time( $seconds_or_timestamp, $timeout = 3600, $forward = false ) {
if ( 10 === strlen( (string) $seconds_or_timestamp ) ) {
$seconds = time() - (int) $seconds_or_timestamp;
if ( $seconds > $timeout ) {
return gmdate( 'm/d/Y H:i:s', (int) $seconds_or_timestamp + (int) LITESPEED_TIME_OFFSET );
$seconds = (int) $seconds_or_timestamp;
if ( $seconds > 86400 ) {
$num = (int) floor( $seconds / 86400 );
$num = (int) floor( $seconds / 3600 );
$num = (int) floor( $seconds / 60 );
return $forward ? __( 'right now', 'litespeed-cache' ) : __( 'just now', 'litespeed-cache' );
return $forward ? $res : sprintf( __( ' %s ago', 'litespeed-cache' ), $res );
* Convert array to a compact base64 JSON string.
* @param mixed $arr Input array or scalar.
* @return string|mixed Encoded string or original value.
public static function arr2str( $arr ) {
if ( ! is_array( $arr ) ) {
return base64_encode( wp_json_encode( $arr ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
* Convert size in bytes to human readable form.
* @param int $filesize Bytes.
* @param bool $is_1000 When true, use 1000-based units.
public static function real_size( $filesize, $is_1000 = false ) {
$unit = $is_1000 ? 1000 : 1024;
if ( $filesize >= pow( $unit, 3 ) ) {
$filesize = round( ( $filesize / pow( $unit, 3 ) ) * 100 ) / 100 . 'G';
} elseif ( $filesize >= pow( $unit, 2 ) ) {
$filesize = round( ( $filesize / pow( $unit, 2 ) ) * 100 ) / 100 . 'M';
} elseif ( $filesize >= $unit ) {
$filesize = round( ( $filesize / $unit ) * 100 ) / 100 . 'K';
$filesize = $filesize . 'B';
* Parse HTML attribute string into an array.
* @since 1.4 Moved from optimize to utility
* @param string $str Raw attribute string.
* @return array<string,string> Attributes.
public static function parse_attr( $str ) {
$parsed = wp_kses_hair( $str, self::_kses_protocols() );
foreach ( $parsed as $name => $data ) {
$attrs[ $name ] = $data['value'];
* Remove an attribute from an HTML attribute string using wp_kses_hair.
* @param string $attr_str Raw attribute string (e.g. ' type="text/javascript" src="..."').
* @param string $attr_name Attribute name to remove (e.g. 'type', 'async').
* @return string Attribute string with the named attribute removed.
public static function remove_attr( $attr_str, $attr_name ) {
$parsed = wp_kses_hair( $attr_str, self::_kses_protocols() );
if ( ! isset( $parsed[ $attr_name ] ) ) {
$whole = $parsed[ $attr_name ]['whole'];
// For valueless attrs (e.g. async), use word boundary to avoid partial match (e.g. async-fallback)
if ( 'y' === $parsed[ $attr_name ]['vless'] ) {
return preg_replace( '# ' . preg_quote( $whole, '#' ) . '(?=\s|>|/|$)#i', '', $attr_str, 1 );
// For attrs with value (e.g. type="text/javascript"), straight replace is safe
$result = str_replace( ' ' . $whole, '', $attr_str );
// Handle edge case: attr at the very start of string (no leading space)
if ( $result === $attr_str && 0 === strpos( $attr_str, $whole ) ) {
$result = ltrim( substr( $attr_str, strlen( $whole ) ) );
* Return allowed protocols including data: for attribute parsing.
* WordPress wp_allowed_protocols() does not include data:, but our parse/remove
* helpers must preserve data: URIs (e.g. base64 placeholder images).
private static function _kses_protocols() {
if ( null === $protocols ) {
$protocols = array_merge( wp_allowed_protocols(), [ 'data' ] );
* Search for a hit within an array of strings/rules.
* Supports ^prefix, suffix$, ^exact$, and substring.
* @param string $needle The string to compare.
* @param array $haystack Array of rules/strings.
* @param bool $has_ttl When true, support "rule TTL" format.
* @return bool|string|array False if not found; matched item or [item, ttl] if has_ttl.
public static function str_hit_array( $needle, $haystack, $has_ttl = false ) {
if ( ! is_array( $haystack ) ) {
Debug2::debug( '[Util] ❌ bad param in str_hit_array()!' );
foreach ( $haystack as $item ) {
$item = explode( ' ', $item );
if ( ! empty( $item[1] ) ) {
if ( '^' === substr( $item, 0, 1 ) && '$' === substr( $item, -1 ) ) {
if ( substr( $item, 1, -1 ) === $needle ) {
} elseif ( '$' === substr( $item, -1 ) ) {
if ( substr( $item, 0, -1 ) === substr( $needle, -strlen( $item ) + 1 ) ) {
} elseif ( '^' === substr( $item, 0, 1 ) ) {
if ( substr( $item, 1 ) === substr( $needle, 0, strlen( $item ) - 1 ) ) {
} elseif ( false !== strpos( $needle, $item ) ) {
return $has_ttl ? [ $hit, $this_ttl ] : $hit;
* Load PHP-compat library.
public static function compatibility() {
require_once LSCWP_DIR . 'lib/php-compatibility.func.php';
* Convert URI path to absolute URL.
* @param string $uri Relative path `/a/b.html` or `a/b.html`.
* @return string Absolute URL.
public static function uri2url( $uri ) {
if ( '/' === substr( $uri, 0, 1 ) ) {
$url = LSCWP_DOMAIN . $uri;
$url = home_url( '/' ) . $uri;
* @param string $url URL.
* @return string Basename.
public static function basename( $url ) {
$uri = wp_parse_url( $url, PHP_URL_PATH );
$basename = pathinfo( (string) $uri, PATHINFO_BASENAME );
* Drop .webp and .avif suffix from a filename.
* @param string $filename Filename.
* @return string Cleaned filename.
public static function drop_webp( $filename ) {
if ( in_array( substr( $filename, -5 ), [ '.webp', '.avif' ], true ) ) {
$filename = substr( $filename, 0, -5 );
* Convert URL to URI (optionally keep query).
* @since 1.6.2.1 Added 2nd param keep_qs
* @param string $url URL.
* @param bool $keep_qs Keep query string.
public static function url2uri( $url, $keep_qs = false ) {
$uri = wp_parse_url( $url, PHP_URL_PATH );
$qs = wp_parse_url( $url, PHP_URL_QUERY );
if ( ! $keep_qs || ! $qs ) {
return (string) $uri . '?' . $qs;
* Get attachment relative path to upload folder.
* @param string $url Full attachment URL.
* @return string Relative upload path like `2018/08/file.jpg`.
public static function att_short_path( $url ) {
if ( ! defined( 'LITESPEED_UPLOAD_PATH' ) ) {
$_wp_upload_dir = wp_upload_dir();
$upload_path = self::url2uri( $_wp_upload_dir['baseurl'] );
define( 'LITESPEED_UPLOAD_PATH', $upload_path );
$local_file = self::url2uri( $url );
$short_path = substr( $local_file, strlen( LITESPEED_UPLOAD_PATH ) + 1 );