| 1 |
<?php // phpcs:disable WordPress.Files.FileName -- PSR-4 class file naming; renaming would break the autoloader. |
| 2 |
|
| 3 |
|
| 4 |
namespace GauchoPlugins\VersionInfo; |
| 5 |
|
| 6 |
defined( 'ABSPATH' ) || exit; // Exit if accessed directly. |
| 7 |
|
| 8 |
/** |
| 9 |
* Free-tier system data helpers. |
| 10 |
* |
| 11 |
* Single source of truth for the PHP end-of-life table — the PRO Health |
| 12 |
* Advisor reads it through php_eol_dates(), so free and PRO EOL data can |
| 13 |
* never drift apart. |
| 14 |
* |
| 15 |
* Everything here must stay portable and dependency-free: no /proc reads |
| 16 |
* (system RAM stays PRO territory in Pro\MemoryProvider), no Pro\* |
| 17 |
* references (includes/pro/ is stripped from the wp.org free build), and |
| 18 |
* no PHP 7+ syntax — the plugin still installs on PHP 5.6 / WP 4.7. |
| 19 |
*/ |
| 20 |
class SystemInfo { |
| 21 |
|
| 22 |
/** |
| 23 |
* PHP minor versions and their end-of-life (security support end) dates. |
| 24 |
* |
| 25 |
* `const` (rather than `private const`) — class constant visibility |
| 26 |
* modifiers require PHP 7.1+. Dates verified against endoflife.date on |
| 27 |
* 2026-08-11. PHP 8.6 is unreleased and deliberately absent. |
| 28 |
* |
| 29 |
* @var array<string, string> |
| 30 |
*/ |
| 31 |
const PHP_EOL_DATES = array( |
| 32 |
'5.6' => '2018-12-31', |
| 33 |
'7.0' => '2019-01-10', |
| 34 |
'7.1' => '2019-12-01', |
| 35 |
'7.2' => '2020-11-30', |
| 36 |
'7.3' => '2021-12-06', |
| 37 |
'7.4' => '2022-11-28', |
| 38 |
'8.0' => '2023-11-26', |
| 39 |
'8.1' => '2025-12-31', |
| 40 |
'8.2' => '2026-12-31', |
| 41 |
'8.3' => '2027-12-31', |
| 42 |
'8.4' => '2028-12-31', |
| 43 |
'8.5' => '2029-12-31', |
| 44 |
); |
| 45 |
|
| 46 |
/** |
| 47 |
* Returns the PHP minor-version EOL date table. |
| 48 |
* |
| 49 |
* @return array<string, string> |
| 50 |
*/ |
| 51 |
public static function php_eol_dates() { |
| 52 |
return self::PHP_EOL_DATES; |
| 53 |
} |
| 54 |
|
| 55 |
/** |
| 56 |
* EOL status of the running PHP version. |
| 57 |
* |
| 58 |
* @return array{date: string|null, days_left: int|null, is_eol: bool} |
| 59 |
*/ |
| 60 |
public static function php_eol_info() { |
| 61 |
$parts = explode( '.', phpversion() ); |
| 62 |
$minor = ( isset( $parts[0] ) ? $parts[0] : '' ) . '.' . ( isset( $parts[1] ) ? $parts[1] : '0' ); |
| 63 |
if ( ! array_key_exists( $minor, self::PHP_EOL_DATES ) ) { |
| 64 |
return array( |
| 65 |
'date' => null, |
| 66 |
'days_left' => null, |
| 67 |
'is_eol' => false, |
| 68 |
); |
| 69 |
} |
| 70 |
$eol_ts = strtotime( self::PHP_EOL_DATES[ $minor ] ); |
| 71 |
$is_eol = time() > $eol_ts; |
| 72 |
return array( |
| 73 |
'date' => self::PHP_EOL_DATES[ $minor ], |
| 74 |
'days_left' => $is_eol ? null : (int) round( ( $eol_ts - time() ) / DAY_IN_SECONDS ), |
| 75 |
'is_eol' => $is_eol, |
| 76 |
); |
| 77 |
} |
| 78 |
|
| 79 |
/** |
| 80 |
* PHP memory limit and current usage. PHP-process memory only — system |
| 81 |
* RAM (/proc/meminfo, WMI) is the PRO MemoryProvider's job. |
| 82 |
* |
| 83 |
* @return array{usage: int|null, limit: int, percent: float|null} |
| 84 |
*/ |
| 85 |
public static function memory_usage() { |
| 86 |
$usage = function_exists( 'memory_get_usage' ) ? (int) memory_get_usage( true ) : null; |
| 87 |
$raw = function_exists( 'ini_get' ) ? ini_get( 'memory_limit' ) : false; |
| 88 |
// ini_get() returns string|false — false is the only non-string case to normalize. |
| 89 |
$limit = self::parse_memory_limit( false === $raw ? '' : (string) $raw ); |
| 90 |
return array( |
| 91 |
'usage' => $usage, |
| 92 |
'limit' => $limit, |
| 93 |
'percent' => self::memory_percent( $usage, $limit ), |
| 94 |
); |
| 95 |
} |
| 96 |
|
| 97 |
/** |
| 98 |
* Pure helper: shorthand ini value to bytes. 0 means unknown/unlimited |
| 99 |
* ("-1" = unlimited; treated as "no limit", Pro\MemoryProvider precedent). |
| 100 |
* |
| 101 |
* @param string $raw Raw ini value, e.g. "256M", "1G", "-1". |
| 102 |
* @return int |
| 103 |
*/ |
| 104 |
public static function parse_memory_limit( $raw ) { |
| 105 |
$raw = trim( (string) $raw ); |
| 106 |
if ( '' === $raw || '-1' === $raw ) { |
| 107 |
return 0; |
| 108 |
} |
| 109 |
$bytes = wp_convert_hr_to_bytes( $raw ); |
| 110 |
return $bytes > 0 ? (int) $bytes : 0; |
| 111 |
} |
| 112 |
|
| 113 |
/** |
| 114 |
* Pure helper: usage percentage of the limit; null when either is unknown. |
| 115 |
* |
| 116 |
* @param int|null $usage Bytes currently used, or null when unavailable. |
| 117 |
* @param int $limit Limit in bytes; 0 = unknown/unlimited. |
| 118 |
* @return float|null |
| 119 |
*/ |
| 120 |
public static function memory_percent( $usage, $limit ) { |
| 121 |
if ( null === $usage || $limit <= 0 ) { |
| 122 |
return null; |
| 123 |
} |
| 124 |
return round( ( $usage / $limit ) * 100, 1 ); |
| 125 |
} |
| 126 |
|
| 127 |
/** |
| 128 |
* Shared human-readable memory string for the three display surfaces. |
| 129 |
* |
| 130 |
* @return string "24.6 MB of 256 MB (9.6%)", "24.6 MB (no limit)", or '' |
| 131 |
* when usage is unavailable (callers omit the row/segment). |
| 132 |
*/ |
| 133 |
public static function memory_text() { |
| 134 |
$memory = self::memory_usage(); |
| 135 |
if ( null === $memory['usage'] ) { |
| 136 |
return ''; |
| 137 |
} |
| 138 |
if ( null === $memory['percent'] ) { |
| 139 |
/* translators: %s: current PHP memory usage */ |
| 140 |
return sprintf( __( '%s (no limit)', 'version-info' ), size_format( $memory['usage'] ) ); |
| 141 |
} |
| 142 |
return sprintf( |
| 143 |
/* translators: %1$s: current PHP memory usage, %2$s: PHP memory limit, %3$s: percentage of the limit used */ |
| 144 |
__( '%1$s of %2$s (%3$s%%)', 'version-info' ), |
| 145 |
size_format( $memory['usage'] ), |
| 146 |
size_format( $memory['limit'] ), |
| 147 |
number_format_i18n( $memory['percent'], 1 ) |
| 148 |
); |
| 149 |
} |
| 150 |
|
| 151 |
/** |
| 152 |
* Shared EOL alert string for the footer and admin bar. Empty unless the |
| 153 |
* running PHP is past EOL or within 365 days of it — the countdown is an |
| 154 |
* alert, not permanent string growth for healthy stacks. |
| 155 |
* |
| 156 |
* @return string |
| 157 |
*/ |
| 158 |
public static function php_eol_alert_text() { |
| 159 |
$eol = self::php_eol_info(); |
| 160 |
if ( $eol['is_eol'] ) { |
| 161 |
return __( 'PHP past EOL', 'version-info' ); |
| 162 |
} |
| 163 |
if ( null !== $eol['days_left'] && $eol['days_left'] <= 365 ) { |
| 164 |
/* translators: %s: number of days until the running PHP version reaches end of life */ |
| 165 |
return sprintf( __( 'PHP EOL in %s days', 'version-info' ), number_format_i18n( $eol['days_left'] ) ); |
| 166 |
} |
| 167 |
return ''; |
| 168 |
} |
| 169 |
|
| 170 |
/** |
| 171 |
* Server IP, best effort. Mirrors Pro\LocationProvider::server_ip() — |
| 172 |
* duplicated by design: free code must never reference Pro\* classes |
| 173 |
* because includes/pro/ is stripped from the wp.org build. |
| 174 |
* |
| 175 |
* @return string Empty string when unavailable (callers omit the row). |
| 176 |
*/ |
| 177 |
public static function server_ip() { |
| 178 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- server-set values (not client input), sanitized by the character whitelist below. |
| 179 |
$ip = isset( $_SERVER['SERVER_ADDR'] ) ? $_SERVER['SERVER_ADDR'] : ( isset( $_SERVER['LOCAL_ADDR'] ) ? $_SERVER['LOCAL_ADDR'] : '' ); |
| 180 |
return (string) preg_replace( '/[^0-9a-fA-F:\.]/', '', (string) $ip ); |
| 181 |
} |
| 182 |
|
| 183 |
/** |
| 184 |
* Compact Markdown table of the core stack, built server-side so the |
| 185 |
* clipboard JS stays dumb. Deliberately excludes the plugin/theme list — |
| 186 |
* that is the PRO System Export feature. |
| 187 |
* |
| 188 |
* @return string |
| 189 |
*/ |
| 190 |
public static function markdown_table() { |
| 191 |
global $wpdb; |
| 192 |
$memory = self::memory_usage(); |
| 193 |
$server = sanitize_text_field( isset( $_SERVER['SERVER_SOFTWARE'] ) ? $_SERVER['SERVER_SOFTWARE'] : __( 'Unknown', 'version-info' ) ); |
| 194 |
|
| 195 |
$rows = array( |
| 196 |
array( __( 'WordPress', 'version-info' ), apply_filters( 'version_info_wp_version', get_bloginfo( 'version' ) ) ), |
| 197 |
array( __( 'PHP', 'version-info' ), apply_filters( 'version_info_php_version', phpversion() ) ), |
| 198 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- reading the MySQL server version; no WP API exists and the value is not site data. |
| 199 |
array( __( 'MySQL', 'version-info' ), apply_filters( 'version_info_mysql_version', (string) $wpdb->get_var( 'SELECT VERSION()' ) ) ), |
| 200 |
array( __( 'Web Server', 'version-info' ), apply_filters( 'version_info_server_software', $server ) ), |
| 201 |
array( __( 'PHP Memory Limit', 'version-info' ), $memory['limit'] > 0 ? size_format( $memory['limit'] ) : __( 'Unlimited', 'version-info' ) ), |
| 202 |
array( __( 'WP Memory Limit', 'version-info' ), defined( 'WP_MEMORY_LIMIT' ) ? WP_MEMORY_LIMIT : __( 'Unknown', 'version-info' ) ), |
| 203 |
array( __( 'Locale', 'version-info' ), get_locale() ), |
| 204 |
array( __( 'Multisite', 'version-info' ), is_multisite() ? __( 'Yes', 'version-info' ) : __( 'No', 'version-info' ) ), |
| 205 |
); |
| 206 |
|
| 207 |
$lines = array( |
| 208 |
'| ' . __( 'Item', 'version-info' ) . ' | ' . __( 'Value', 'version-info' ) . ' |', |
| 209 |
'| --- | --- |', |
| 210 |
); |
| 211 |
foreach ( $rows as $row ) { |
| 212 |
$lines[] = '| ' . str_replace( '|', '\|', (string) $row[0] ) . ' | ' . str_replace( '|', '\|', (string) $row[1] ) . ' |'; |
| 213 |
} |
| 214 |
return implode( "\n", $lines ); |
| 215 |
} |
| 216 |
} |
| 217 |
|