* 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 );
* Validate IPv4 public address.
* @param string $ip IP address.
* @return string|false IP or false when invalid.
public static function valid_ipv4( $ip ) {
return filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE );
* Define LSCWP_DOMAIN using the home URL (no trailing slash).
public static function domain_const() {
if ( defined( 'LSCWP_DOMAIN' ) ) {
$domain = http_build_url( get_home_url(), [], HTTP_URL_STRIP_ALL ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
define( 'LSCWP_DOMAIN', $domain );
* Sanitize lines based on requested transforms.
* @param array|string $arr Lines as array or newline-separated string.
* @param string|null $type Comma-separated transforms: uri,basename,drop_webp,relative,domain,noprotocol,trailingslash,string.
* @return array|string Sanitized list or string.
public static function sanitize_lines( $arr, $type = null ) {
$types = $type ? explode( ',', $type ) : [];
if ( 'string' === $type ) {
if ( ! is_array( $arr ) ) {
$arr = explode( "\n", $arr );
$arr = array_map( 'trim', $arr );
if ( in_array( 'uri', $types, true ) ) {
$arr = array_map( __CLASS__ . '::url2uri', $arr );
if ( in_array( 'basename', $types, true ) ) {
$arr = array_map( __CLASS__ . '::basename', $arr );
if ( in_array( 'drop_webp', $types, true ) ) {
$arr = array_map( __CLASS__ . '::drop_webp', $arr );
if ( in_array( 'relative', $types, true ) ) {
$arr = array_map( __CLASS__ . '::make_relative', $arr );
if ( in_array( 'domain', $types, true ) ) {
$arr = array_map( __CLASS__ . '::parse_domain', $arr );
if ( in_array( 'noprotocol', $types, true ) ) {
$arr = array_map( __CLASS__ . '::noprotocol', $arr );
if ( in_array( 'trailingslash', $types, true ) ) {
$arr = array_map( 'trailingslashit', $arr );
$arr = array_map( 'trim', $arr );
$arr = array_unique( $arr );
$arr = array_filter( $arr );
if ( in_array( 'string', $types, true ) ) {
return implode( "\n", $arr );
* Build an admin URL with action & nonce.
* Assumes user capabilities are already checked.
* @since 1.6 Changed order of 2nd&3rd param, changed 3rd param `append_str` to 2nd `type`
* @param string $action Action name.
* @param string|false $type Optional type query value.
* @param bool $is_ajax Whether to build for admin-ajax.php.
* @param string|null|bool $page Page filename or true for admin.php.
* @param array<string,string> $append_arr Extra query parameters.
* @param bool $unescape Return unescaped URL.
* @return string Built URL.
public static function build_url( $action, $type = false, $is_ajax = false, $page = null, $append_arr = [], $unescape = false ) {
if ( '_ori' === $page ) {
$append_arr['_litespeed_ori'] = 1;
} elseif ( false !== strpos( $page, '?' ) ) {
$combined = $page . $prefix . Router::ACTION . '=' . $action;
// Current page rebuild URL.
$params = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! empty( $params ) ) {
if ( isset( $params[ Router::ACTION ] ) ) {
unset( $params[ Router::ACTION ] );
if ( isset( $params['_wpnonce'] ) ) {
unset( $params['_wpnonce'] );
if ( ! empty( $params ) ) {
$prefix .= http_build_query( $params ) . '&';
$combined = $pagenow . $prefix . Router::ACTION . '=' . $action;
$combined = 'admin-ajax.php?action=litespeed_ajax&' . Router::ACTION . '=' . $action;
$prenonce = is_network_admin() ? network_admin_url( $combined ) : admin_url( $combined );
$url = wp_nonce_url( $prenonce, $action, Router::NONCE );
// Remove potential param `type` from url.
$parsed = wp_parse_url( htmlspecialchars_decode( $url ) );
if ( isset( $parsed['query'] ) ) {
parse_str( $parsed['query'], $query );
$built_arr = array_merge( $query, [ Router::TYPE => $type ] );
$parsed['query'] = http_build_query( array_merge( $built_arr, (array) $append_arr ) );
$url = http_build_url( $parsed ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
$url = htmlspecialchars( $url, ENT_QUOTES, 'UTF-8' );
$url = wp_specialchars_decode( $url );
* Check if a host is internal (same as site host or filtered list).
* @param string $host Host to test.
* @return bool True if internal.
public static function internal( $host ) {
if ( ! defined( 'LITESPEED_FRONTEND_HOST' ) ) {
if ( defined( 'WP_HOME' ) ) {
$home_host = constant( 'WP_HOME' );
$home_host = get_option( 'home' );
define( 'LITESPEED_FRONTEND_HOST', (string) wp_parse_url( $home_host, PHP_URL_HOST ) );
if ( LITESPEED_FRONTEND_HOST === $host ) {
if ( ! isset( self::$_internal_domains ) ) {
self::$_internal_domains = apply_filters( 'litespeed_internal_domains', [] );
if ( self::$_internal_domains ) {
return in_array( $host, self::$_internal_domains, true );
* Check if a URL is an internal existing file and return its real path and size.
* @since 1.6.2 Moved here from optm.cls due to usage of media.cls
* @param string $url URL.
* @param string|false $addition_postfix Optional postfix to append to path before checking.
* @return array{0:string,1:int}|false [realpath, size] or false.
public static function is_internal_file( $url, $addition_postfix = false ) {
if ( 'data:' === substr( $url, 0, 5 ) ) {
Debug2::debug2( '[Util] data: content not file' );
$url_parsed = wp_parse_url( $url );
if ( isset( $url_parsed['host'] ) && ! self::internal( $url_parsed['host'] ) ) {
if ( ! CDN::internal( $url_parsed['host'] ) ) {
Debug2::debug2( '[Util] external' );
if ( empty( $url_parsed['path'] ) ) {
// Replace child blog path for assets (multisite).
if ( is_multisite() && defined( 'PATH_CURRENT_SITE' ) ) {
$pattern = '#^' . PATH_CURRENT_SITE . '([_0-9a-zA-Z-]+/)(wp-(content|admin|includes))#U';
$replacement = PATH_CURRENT_SITE . '$2';
$url_parsed['path'] = preg_replace( $pattern, $replacement, $url_parsed['path'] );
if ( '/' === substr( $url_parsed['path'], 0, 1 ) ) {
$docroot = isset( $_SERVER['DOCUMENT_ROOT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) : '';
if ( defined( 'LITESPEED_WP_REALPATH' ) ) {
$file_path_ori = $docroot . constant( 'LITESPEED_WP_REALPATH' ) . $url_parsed['path'];
$file_path_ori = $docroot . $url_parsed['path'];
$file_path_ori = Router::frontend_path() . '/' . $url_parsed['path'];
if ( $addition_postfix ) {
$file_path_ori .= '.' . $addition_postfix;
$file_path_ori = apply_filters( 'litespeed_realpath', $file_path_ori );
$file_path = realpath( $file_path_ori );
if ( ! is_file( $file_path ) ) {
Debug2::debug2( '[Util] file not exist: ' . $file_path_ori );
return [ $file_path, (int) filesize( $file_path ) ];
* Safely parse URL and component.
* @param string $url URL to parse.
* @param int $component One of the PHP_URL_* constants.
public static function parse_url_safe( $url, $component = -1 ) {
if ( '//' === substr( $url, 0, 2 ) ) {
return wp_parse_url( $url, $component );
* Replace URLs in a srcset attribute using a callback.
* @param string $content HTML content containing srcset.
* @param callable $callback Callback that receives old URL and returns new URL or false.
* @return string Modified content.
public static function srcset_replace( $content, $callback ) {
preg_match_all( '# srcset=([\'"])(.+)\g{1}#iU', $content, $matches );
if ( ! empty( $matches[2] ) ) {
foreach ( $matches[2] as $k => $urls_ori ) {
$urls_final = explode( ',', $urls_ori );
foreach ( $urls_final as $k2 => $url_info ) {
$url_info_arr = explode( ' ', trim( $url_info ) );
$new_url = call_user_func( $callback, $url_info_arr[0] );
$urls_final[ $k2 ] = str_replace( $url_info_arr[0], $new_url, $url_info );
Debug2::debug2( '[Util] - srcset replaced to ' . $new_url . ( ! empty( $url_info_arr[1] ) ? ' ' . $url_info_arr[1] : '' ) );
$urls_final = implode( ',', $urls_final );
$srcset_ori[] = $matches[0][ $k ];
$srcset_final[] = str_replace( $urls_ori, $urls_final, $matches[0][ $k ] );
$content = str_replace( $srcset_ori, $srcset_final, $content );
Debug2::debug2( '[Util] - srcset replaced' );
* Generate pagination HTML or return offset.
* @param int $total Total items.
* @param int $limit Items per page.
* @param bool $return_offset When true, return numeric offset instead of HTML.
public static function pagination( $total, $limit, $return_offset = false ) {
$pagenum = isset( $_GET['pagenum'] ) ? absint( $_GET['pagenum'] ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$offset = ( $pagenum - 1 ) * $limit;
$num_of_pages = (int) ceil( $total / $limit );
if ( $offset > $total ) {
$offset = $total - $limit;
$page_links = paginate_links(
'base' => add_query_arg( 'pagenum', '%#%' ),
'prev_text' => '«',
'next_text' => '»',
'total' => $num_of_pages,
return '<div class="tablenav"><div class="tablenav-pages" style="margin: 1em 0">' . $page_links . '</div></div>';
* Build a GROUP placeholder like "(%s,%s),(%s,%s)" for a list of rows.
* @param array<int,array<int,string>> $data Data rows (values already prepared).
* @param string $fields Fields CSV (only used to count columns).
* @return string Placeholder string.
public static function chunk_placeholder( $data, $fields ) {
$division = substr_count( $fields, ',' ) + 1;
return '(' . implode( ',', $el ) . ')';
array_chunk( array_fill( 0, count( $data ), '%s' ), $division )
* Prepare image sizes list for optimization UI.
* @param bool $detailed When true, return detailed objects; otherwise size names.
* @return array<int,string|array<string,int|string>>
public static function prepare_image_sizes_array( $detailed = false ) {
$image_sizes = wp_get_registered_image_subsizes();
foreach ( $image_sizes as $current_size_name => $current_size ) {
if ( empty( $current_size['width'] ) && empty( $current_size['height'] ) ) {
$sizes[] = $current_size_name;
$label = $current_size['width'] . 'x' . $current_size['height'];
if ( $current_size_name !== $label ) {
$label = ucfirst( $current_size_name ) . ' ( ' . $label . ' )';
'file_size' => $current_size_name,
'width' => (int) $current_size['width'],
'height' => (int) $current_size['height'],