JezK
Edit File: utility.cls.php
<?php /** * Utility helpers for LiteSpeed Cache. * * @since 1.1.5 * @package LiteSpeed */ namespace LiteSpeed; 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 1.0.9 * @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. * * @since 3.0 * * @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 = []; foreach ( $arr as $v ) { $new_arr[] = preg_quote( $v, '#' ); } $regex = implode( '|', $new_arr ); $regex = str_replace( ' ', '\\ ', $regex ); if ( $drop_delimiter ) { return $regex; } return '#' . $regex . '#'; } /** * Replace wildcard characters in a string/array with their regex equivalents. * * @since 3.2.2 * * @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 ); } return $value; } /** * Get current page type string. * * @since 2.9 * * @return string Page type. */ public static function page_type() { global $wp_query; $page_type = 'default'; if ( $wp_query->is_page ) { $page_type = is_front_page() ? 'front' : 'page'; } elseif ( $wp_query->is_home ) { $page_type = 'home'; } elseif ( $wp_query->is_single ) { $page_type = get_post_type(); } elseif ( $wp_query->is_category ) { $page_type = 'category'; } elseif ( $wp_query->is_tag ) { $page_type = 'tag'; } elseif ( $wp_query->is_tax ) { $page_type = 'tax'; } elseif ( $wp_query->is_archive ) { if ( $wp_query->is_day ) { $page_type = 'day'; } elseif ( $wp_query->is_month ) { $page_type = 'month'; } elseif ( $wp_query->is_year ) { $page_type = 'year'; } elseif ( $wp_query->is_author ) { $page_type = 'author'; } else { $page_type = 'archive'; } } elseif ( $wp_query->is_search ) { $page_type = 'search'; } elseif ( $wp_query->is_404 ) { $page_type = '404'; } return $page_type; } /** * Build the current request URL from WordPress request globals. * * @since 7.9.1 * * @return string Current request URL. */ public static function request_url() { global $wp; $permalink_structure = get_option( 'permalink_structure' ); if ( ! empty( $permalink_structure ) ) { return trailingslashit( home_url( $wp->request ) ); } $qs_add = $wp->query_string ? '?' . (string) $wp->query_string : ''; return home_url( $wp->request ) . $qs_add; } /** * Get ping speed to a domain via HTTP HEAD timing. * * @since 2.9 * * @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); $status = 0; if (!$file) { $status = 99999; } else { // Site is up 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"); return $status; } /** * Convert seconds/timestamp to a readable relative time. * * @since 1.6.5 * * @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 ); } } else { $seconds = (int) $seconds_or_timestamp; } $res = ''; if ( $seconds > 86400 ) { $num = (int) floor( $seconds / 86400 ); $res .= $num . 'd'; $seconds %= 86400; } if ( $seconds > 3600 ) { if ( $res ) { $res .= ', '; } $num = (int) floor( $seconds / 3600 ); $res .= $num . 'h'; $seconds %= 3600; } if ( $seconds > 60 ) { if ( $res ) { $res .= ', '; } $num = (int) floor( $seconds / 60 ); $res .= $num . 'm'; $seconds %= 60; } if ( $seconds > 0 ) { if ( $res ) { $res .= ' '; } $res .= $seconds . 's'; } if ( ! $res ) { 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. * * @since 1.6 * * @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 $arr; } return base64_encode( wp_json_encode( $arr ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode } /** * Convert size in bytes to human readable form. * * @since 1.6 * * @param int $filesize Bytes. * @param bool $is_1000 When true, use 1000-based units. * @return string */ 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'; } else { $filesize = $filesize . 'B'; } return $filesize; } /** * Parse HTML attribute string into an array. * * @since 1.2.2 * @since 1.4 Moved from optimize to utility * @access private * * @param string $str Raw attribute string. * @return array<string,string> Attributes. */ public static function parse_attr( $str ) { $attrs = []; $parsed = wp_kses_hair( $str, self::_kses_protocols() ); foreach ( $parsed as $name => $data ) { $value = $data['value']; // Fix for WP 7.0+ wp_kses_hair() re-encodes character references (`&` -> `&`). if ( '' !== $value && false === strpos( $str, $value ) && preg_match( '#(?:^|\s)' . preg_quote( $name, '#' ) . '\s*=\s*(["\'])(.*?)\1#is', $str, $m ) && html_entity_decode( $m[2], ENT_QUOTES | ENT_HTML5 ) === html_entity_decode( $value, ENT_QUOTES | ENT_HTML5 ) ) { $value = $m[2]; } $attrs[ $name ] = trim( $value ); } return $attrs; } /** * Remove an attribute from an HTML attribute string using wp_kses_hair. * * @since 7.8 * * @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 ] ) ) { return $attr_str; } $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 $result; } /** * 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). * * @since 7.8 * @return string[] */ private static function _kses_protocols() { static $protocols; if ( null === $protocols ) { $protocols = array_merge( wp_allowed_protocols(), [ 'data' ] ); } return $protocols; } /** * Search for a hit within an array of strings/rules. * * Supports ^prefix, suffix$, ^exact$, and substring. * * @since 1.3 * @access private * * @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 ( ! $haystack ) { return false; } if ( ! is_array( $haystack ) ) { Debug2::debug( '[Util] ❌ bad param in str_hit_array()!' ); return false; } $hit = false; $this_ttl = 0; foreach ( $haystack as $item ) { if ( ! $item ) { continue; } if ( $has_ttl ) { $this_ttl = 0; $item = explode( ' ', $item ); if ( ! empty( $item[1] ) ) { $this_ttl = $item[1]; } $item = $item[0]; } if ( '^' === substr( $item, 0, 1 ) && '$' === substr( $item, -1 ) ) { if ( substr( $item, 1, -1 ) === $needle ) { $hit = $item; break; } } elseif ( '$' === substr( $item, -1 ) ) { if ( substr( $item, 0, -1 ) === substr( $needle, -strlen( $item ) + 1 ) ) { $hit = $item; break; } } elseif ( '^' === substr( $item, 0, 1 ) ) { if ( substr( $item, 1 ) === substr( $needle, 0, strlen( $item ) - 1 ) ) { $hit = $item; break; } } elseif ( false !== strpos( $needle, $item ) ) { $hit = $item; break; } } if ( $hit ) { return $has_ttl ? [ $hit, $this_ttl ] : $hit; } return false; } /** * Load PHP-compat library. * * @since 1.2.2 * @return void */ public static function compatibility() { require_once LSCWP_DIR . 'lib/php-compatibility.func.php'; } /** * Convert URI path to absolute URL. * * @since 1.3 * * @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 ) ) { self::domain_const(); $url = LSCWP_DOMAIN . $uri; } else { $url = home_url( '/' ) . $uri; } return $url; } /** * Get basename from URL. * * @since 4.7 * * @param string $url URL. * @return string Basename. */ public static function basename( $url ) { $url = trim( $url ); $uri = wp_parse_url( $url, PHP_URL_PATH ); $basename = pathinfo( (string) $uri, PATHINFO_BASENAME ); return $basename; } /** * Drop .webp and .avif suffix from a filename. * * @since 4.7 * * @param string $filename Filename. * @return string Cleaned filename. */ public static function drop_webp( $filename ) { $path = (string) wp_parse_url( $filename, PHP_URL_PATH ); if ( ! $path ) { $path = $filename; } $base_ext = strtolower( pathinfo( $path, PATHINFO_EXTENSION ) ); if ( ! in_array( $base_ext, [ 'webp', 'avif' ], true ) ) { return $filename; } // Only strip the optimized suffix when a real source image extension exists underneath, e.g. `.png.webp`. Native `image.webp` is returned unchanged. $prev_ext = strtolower( pathinfo( pathinfo( $path, PATHINFO_FILENAME ), PATHINFO_EXTENSION ) ); if ( ! in_array( $prev_ext, [ 'png', 'jpg', 'jpeg', 'gif', 'bmp' ], true ) ) { return $filename; } // Remove the `.webp`/`.avif` suffix only, preserving any query string or fragment (lazyload can pass raw cache-busted `<img src>` URLs). return preg_replace( '~\.' . $base_ext . '(?=$|[?#])~i', '', $filename, 1 ); } /** * Convert URL to URI (optionally keep query). * * @since 1.2.2 * @since 1.6.2.1 Added 2nd param keep_qs * * @param string $url URL. * @param bool $keep_qs Keep query string. * @return string URI. */ public static function url2uri( $url, $keep_qs = false ) { $url = trim( $url ); $uri = wp_parse_url( $url, PHP_URL_PATH ); $qs = wp_parse_url( $url, PHP_URL_QUERY ); if ( ! $keep_qs || ! $qs ) { return (string) $uri; } return (string) $uri . '?' . $qs; } /** * Get attachment relative path to upload folder. * * @since 3.0 * * @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 ); return $short_path; } /** * 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 ) { self::domain_const(); if ( 0 === strpos( $url, LSCWP_DOMAIN ) ) { $url = substr( $url, strlen( LSCWP_DOMAIN ) ); } return trim( $url ); } /** * Extract just the scheme+host portion from a URL. * * @since 1.7.1 * * @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'] ) ) { return ''; } if ( ! empty( $parsed['scheme'] ) ) { return $parsed['scheme'] . '://' . $parsed['host']; } return '//' . $parsed['host']; } /** * Drop protocol from URL (e.g., https://example.com -> //example.com). * * @since 3.3 * * @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 ); } return $url; } /** * Validate IPv4 public address. * * @since 5.5 * * @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). * * @since 1.3 * @return void */ public static function domain_const() { if ( defined( 'LSCWP_DOMAIN' ) ) { return; } self::compatibility(); $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. * * @since 1.3 * * @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 ( ! $arr ) { if ( 'string' === $type ) { return ''; } return []; } if ( ! is_array( $arr ) ) { $arr = explode( "\n", $arr ); } $arr = array_map( 'trim', $arr ); $changed = false; if ( in_array( 'uri', $types, true ) ) { $arr = array_map( __CLASS__ . '::url2uri', $arr ); $changed = true; } if ( in_array( 'basename', $types, true ) ) { $arr = array_map( __CLASS__ . '::basename', $arr ); $changed = true; } if ( in_array( 'drop_webp', $types, true ) ) { $arr = array_map( __CLASS__ . '::drop_webp', $arr ); $changed = true; } if ( in_array( 'relative', $types, true ) ) { $arr = array_map( __CLASS__ . '::make_relative', $arr ); $changed = true; } if ( in_array( 'domain', $types, true ) ) { $arr = array_map( __CLASS__ . '::parse_domain', $arr ); $changed = true; } if ( in_array( 'noprotocol', $types, true ) ) { $arr = array_map( __CLASS__ . '::noprotocol', $arr ); $changed = true; } if ( in_array( 'trailingslash', $types, true ) ) { $arr = array_map( 'trailingslashit', $arr ); $changed = true; } if ( $changed ) { $arr = array_map( 'trim', $arr ); } $arr = array_unique( $arr ); $arr = array_filter( $arr ); if ( in_array( 'string', $types, true ) ) { return implode( "\n", $arr ); } return $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 ) { $prefix = '?'; if ( '_ori' === $page ) { $page = true; $append_arr['_litespeed_ori'] = 1; } if ( ! $is_ajax ) { if ( $page ) { if ( true === $page ) { $page = 'admin.php'; } elseif ( false !== strpos( $page, '?' ) ) { $prefix = '&'; } $combined = $page . $prefix . Router::ACTION . '=' . $action; } else { // 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 ) . '&'; } } global $pagenow; $combined = $pagenow . $prefix . Router::ACTION . '=' . $action; } } else { $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 ); if ( $type ) { // Remove potential param `type` from url. $parsed = wp_parse_url( htmlspecialchars_decode( $url ) ); $query = []; 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 ) ); self::compatibility(); $url = http_build_url( $parsed ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url $url = htmlspecialchars( $url, ENT_QUOTES, 'UTF-8' ); } if ( $unescape ) { $url = wp_specialchars_decode( $url ); } return $url; } /** * Check if a host is internal (same as site host or filtered list). * * @since 1.2.3 * * @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' ); } else { $home_host = get_option( 'home' ); } define( 'LITESPEED_FRONTEND_HOST', (string) wp_parse_url( $home_host, PHP_URL_HOST ) ); } if ( LITESPEED_FRONTEND_HOST === $host ) { return true; } 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 ); } return false; } /** * Map an internal URL to its local file target and size. * * @since 1.2.2 * @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. * @param bool $allow_missing Whether a hook-provided local target may be absent. * @return array{0:string,1:int}|false [local path, size] or false. */ public static function is_internal_file( $url, $addition_postfix = false, $allow_missing = false ) { if ( 'data:' === substr( $url, 0, 5 ) ) { Debug2::debug2( '[Util] data: content not file' ); return false; } $url_parsed = wp_parse_url( $url ); if ( isset( $url_parsed['host'] ) && ! self::internal( $url_parsed['host'] ) ) { // Check if is cdn path. if ( ! CDN::internal( $url_parsed['host'] ) ) { Debug2::debug2( '[Util] external' ); return false; } } if ( empty( $url_parsed['path'] ) ) { return false; } // 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'] ); } // Parse file 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']; } else { $file_path_ori = $docroot . $url_parsed['path']; } } else { $file_path_ori = Router::frontend_path() . '/' . $url_parsed['path']; } // Optional postfix. 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 ( ! $file_path && $allow_missing ) { $file_path = $file_path_ori; } if ( ( file_exists( $file_path ) || ! $allow_missing ) && ! is_file( $file_path ) ) { Debug2::debug2( '[Util] file not exist: ' . $file_path_ori ); return false; } return [ $file_path, is_file( $file_path ) ? (int) filesize( $file_path ) : 0 ]; } /** * Safely parse URL and component. * * @since 3.4.3 * * @param string $url URL to parse. * @param int $component One of the PHP_URL_* constants. * @return mixed */ public static function parse_url_safe( $url, $component = -1 ) { if ( '//' === substr( $url, 0, 2 ) ) { $url = 'https:' . $url; } return wp_parse_url( $url, $component ); } /** * Replace URLs in a srcset attribute using a callback. * * @since 2.2.3 * * @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 ); $srcset_ori = []; $srcset_final = []; if ( ! empty( $matches[2] ) ) { foreach ( $matches[2] as $k => $urls_ori ) { $urls_final = explode( ',', $urls_ori ); $changed = false; foreach ( $urls_final as $k2 => $url_info ) { $url_info_arr = explode( ' ', trim( $url_info ) ); $new_url = call_user_func( $callback, $url_info_arr[0] ); if ( ! $new_url ) { continue; } $changed = true; $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] : '' ) ); } if ( ! $changed ) { continue; } $urls_final = implode( ',', $urls_final ); $srcset_ori[] = $matches[0][ $k ]; $srcset_final[] = str_replace( $urls_ori, $urls_final, $matches[0][ $k ] ); } } if ( $srcset_ori ) { $content = str_replace( $srcset_ori, $srcset_final, $content ); Debug2::debug2( '[Util] - srcset replaced' ); } return $content; } /** * Generate pagination HTML or return offset. * * @since 3.0 * * @param int $total Total items. * @param int $limit Items per page. * @param bool $return_offset When true, return numeric offset instead of HTML. * @return int|string */ 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; } if ( $offset < 0 ) { $offset = 0; } if ( $return_offset ) { return $offset; } $page_links = paginate_links( [ 'base' => add_query_arg( 'pagenum', '%#%' ), 'format' => '', 'prev_text' => '«', 'next_text' => '»', 'total' => $num_of_pages, 'current' => $pagenum, ] ); 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. * * @since 2.0 * * @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; $q = implode( ',', array_map( function ( $el ) { return '(' . implode( ',', $el ) . ')'; }, array_chunk( array_fill( 0, count( $data ), '%s' ), $division ) ) ); return $q; } /** * Prepare image sizes list for optimization UI. * * @since 7.5 * * @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(); $sizes = []; foreach ( $image_sizes as $current_size_name => $current_size ) { if ( empty( $current_size['width'] ) && empty( $current_size['height'] ) ) { continue; } if ( ! $detailed ) { $sizes[] = $current_size_name; } else { $label = $current_size['width'] . 'x' . $current_size['height']; if ( $current_size_name !== $label ) { $label = ucfirst( $current_size_name ) . ' ( ' . $label . ' )'; } $sizes[] = [ 'label' => $label, 'file_size' => $current_size_name, 'width' => (int) $current_size['width'], 'height' => (int) $current_size['height'], ]; } } return $sizes; } }
BanaBridge News
https://validator.w3.org/feed/docs/rss2.html
Court Orders Ex-Weah Protocol Chief Jailed on US$8 Million Bail, Sends 15 Jurors to Prison Over Misconduct
Women Renew Call for War and Economic Crimes Court at Liberia’s Historic Peace Prayer Site
Finance Minister Ngafuan Urges Transparency, Accountability as Liberia Advances ARREST Agenda at UN Retreat
PPCC Donates ICT Equipment to 50 Government Institutions to Expand Electronic Procurement System
Liberia Commissions €100,000 Elemental Analyzer to Strengthen Environmental Research and Food Security
EPA Ends 16 Years of Renting with Purchase of Permanent Headquarters
West African Tax Bodies Deepen Partnership to Boost Revenue Mobilization, Launch Carbon Tax Study
Liberia Hosts Sierra Leone Procurement Delegation for E-GP Reform Study Tour
World Bank Praises Liberia’s Central Bank for Economic Stability and Private Sector Progress
Liberians See Slight Improvement in Corruption Fight, But Graft Still Viewed as Widespread, CENTAL Report Finds
Canada Afrique Care Foundation Set for Official Launch, Expanding Free Mental Health Access Across Canada and Africa
Weah Claims Liberia Has Become a “Narco State” as Opposition Rallies Behind Calls for Independent Probe into US$19 Million Cocaine Bust
EPA Boss Calls for Stronger Student Engagement, Unveils Push for Academic Partnership at UL Climate Symposium
Education Minister Urges Students to Embrace Tax Responsibility at National Student Tax Day
CDC Secretary General Urges U.S. Government to Review Support to Liberia National Police
Liberia Partners with CACFO to Expand Mental Health Services Ahead of Global Launch
Wreh-Toe Appointed Board Chair of CACFO as Foundation Strengthens Mental Health Push
GIABA Opens New Information Center Headquarters in Abidjan
Liberia Wins Fresh Backing as World Bank Group Executive Praises Progress, Pushes Faster Project Delivery
LRA Honors Liberia’s Top Taxpayers as Revenue Collections Hit Historic High
Root Cause in Question as Border Tensions Persist After Conakry Peace Deal
From Enforcement to Innovation: EPA Marks 2025 as a Defining Year for Liberia’s Environment
Weah Declares ‘Road to 2029 Begins Today’ as CDC Marks 22nd Anniversary in Zwedru
500 Documented Governance Failures: Activist Publishes Sweeping End-of-Year Indictment of Boakai Administration
CDC Calls for Probe into Border Crisis, Cites Alleged Mining Activities
Liberia Moves to Strengthen Small Businesses with $672,000 Packaging Solution Initiative
Former Public Works Minister Ruth Coker Collins Defends Zwedru Corridor Strategy, Calls for “Facts Over Rhetoric” in Infrastructure Debate
Power Shake-Up in CDC Diaspora: NEC Dissolves CDC-USA Interim Leadership, Appoints Rev. Muin to Lead U.S. Chapter
Tweah Reemerges, Sparks Fierce National Debate Over Governance and Liberia’s Future
Koijee Slams Tribal Politics and Political Manipulation in Lofa, Accuses Boakai Government
Mali and Burkina Faso Impose Reciprocal Travel Ban on U.S. Citizens
Guinea Election: A Turn Toward Continuity Amid Questions of Democratic Credibility
Liberia’s Revenue Grows Stronger as LRA Exceeds National Target for Second Year
Liberia’s Gender Legal Landscape: Laws in Place, Systems Lag Far Behind
Liberia’s Economy Gains Momentum as CBL Governor Highlights Progress at ECOWAS Meeting
Liberia Positions Itself for Eco Currency Entry as Economy Expands by 5.1%
From Overcrowded Classrooms to Renewal: NPA Mobilizes $1 Million to Transform UL
Ex-Public Works Minister Ruth Coker Collins Slams Ministry’s “Lack of Capacity,” Defends Her Infrastructure Record
CDC Expels Three Senior Members, Citing Alleged Alignment With Government and Undermining Party Ideology
UL to Celebrate 75 Years of Excellence
Saudi Fund Advances Liberia’s Push for Major Infrastructure Development
West African Financial Leaders Convene in Monrovia for ECOWAS Statutory Meetings
Unlocking Liberia’s Potential: Government Launches Major Revenue and Transparency Reforms
Incremental Gains, Enduring Impunity: Liberia’s CPI Improvement Fails the Test of Reform
Boakai’s Call for Unity Faces Scrutiny as Opposition, Citizens Question Actions Behind the Words
REMAPSEN Liberia Set to Strengthen Health and Environmental Reporting
Energy Sector Women Step Into the Spotlight at Liberia’s International Women’s Day Celebration
FeJAL Concludes Three-Day Retreat, Honors Outstanding Women Journalists
Dr. Rasha Kelej Named Among Africa’s 100 Most Influential Leaders
LAVA Honored for Strengthening Liberia–U.S. Ties at 2025 Military Ball