* 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 ) {
preg_match_all( '#([\w-]+)=(["\'])([^\2]*)\2#isU', $str, $matches, PREG_SET_ORDER );
foreach ( $matches as $match ) {
$attrs[ $match[1] ] = trim( $match[3] );
* 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 );
* Make URL relative to the site root (preserves subdir).
* @param string $url Absolute URL.
* @return string Relative URL starting with '/'.
public static function make_relative( $url ) {
if ( 0 === strpos( $url, LSCWP_DOMAIN ) ) {
$url = substr( $url, strlen( LSCWP_DOMAIN ) );
* Extract just the scheme+host portion from a URL.
* @param string $url URL.
* @return string Host-only URL (with scheme if available).
public static function parse_domain( $url ) {
$parsed = wp_parse_url( $url );
if ( empty( $parsed['host'] ) ) {
if ( ! empty( $parsed['scheme'] ) ) {
return $parsed['scheme'] . '://' . $parsed['host'];
return '//' . $parsed['host'];
* Drop protocol from URL (e.g., https://example.com -> //example.com).
* @param string $url URL.
* @return string Protocol-relative URL.
public static function noprotocol( $url ) {
$tmp = wp_parse_url( trim( $url ) );
if ( ! empty( $tmp['scheme'] ) ) {
$url = str_replace( $tmp['scheme'] . ':', '', $url );