PluginProbe
phpinfo() WP – Site Health, PHP Compatibility & Server Audit / 7.2.5
phpinfo() WP – Site Health, PHP Compatibility & Server Audit v7.2.5
7.2.7 7.2.6 7.2.5 7.2.4 7.2.3 7.2.0 7.2.1 7.2.2 7.1.0 7.0.3 7.0.4 7.0.5 trunk 6.0 7.0.0 7.0.1 7.0.2
phpinfo-wp / includes / class-ssl.php

class-ssl.php in phpinfo() WP – Site Health, PHP Compatibility & Server Audit 7.2.5, at includes/class-ssl.php

213 lines 7.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 defined('ABSPATH') or die('Unauthorized Access');
3
4 class Phpinfo_WP_SSL {
5
6 const OPT_DOMAINS = 'phpinfowp_ssl_domains';
7
8 private static function _pro(): bool { return Phpinfo_WP_License::is_valid(); }
9
10 // Returns cert info array or ['error' => '...']
11 public static function check(string $host, int $port = 443): array {
12 if (!self::_pro()) return ['error' => 'Pro license required.'];
13 $host = strtolower(trim($host));
14 if (!$host) return ['error' => 'No host provided.'];
15
16 $result = self::_check_via_stream($host, $port);
17 if (isset($result['error']) && function_exists('curl_init')) {
18 $result = self::_check_via_curl($host, $port);
19 }
20 return $result;
21 }
22
23 private static function _check_via_stream(string $host, int $port): array {
24 if (!function_exists('stream_socket_client')) {
25 return ['error' => 'stream_socket_client not available.'];
26 }
27
28 $ctx = stream_context_create([
29 'ssl' => [
30 'capture_peer_cert' => true,
31 'verify_peer' => false,
32 'verify_peer_name' => false,
33 'SNI_enabled' => true,
34 'peer_name' => $host,
35 ],
36 ]);
37
38 $socket = @stream_socket_client(
39 "ssl://{$host}:{$port}", $errno, $errstr, 15,
40 STREAM_CLIENT_CONNECT, $ctx
41 );
42
43 if (!$socket) {
44 return ['error' => $errstr ?: "Could not connect to {$host}:{$port}"];
45 }
46
47 $params = stream_context_get_params($socket);
48 $cert = $params['options']['ssl']['peer_certificate'] ?? null;
49 fclose($socket);
50
51 if (!$cert) return ['error' => 'Connected but no certificate returned.'];
52 return self::_parse_cert($cert, $host);
53 }
54
55 private static function _check_via_curl(string $host, int $port): array {
56 $ch = curl_init();
57 curl_setopt_array($ch, [
58 CURLOPT_URL => "https://{$host}:{$port}/",
59 CURLOPT_RETURNTRANSFER => true,
60 CURLOPT_NOBODY => true,
61 CURLOPT_CERTINFO => true,
62 CURLOPT_SSL_VERIFYPEER => false,
63 CURLOPT_SSL_VERIFYHOST => false,
64 CURLOPT_CONNECTTIMEOUT => 15,
65 CURLOPT_TIMEOUT => 15,
66 ]);
67 curl_exec($ch);
68
69 $info = curl_getinfo($ch);
70 $certinfo = $info['certinfo'] ?? [];
71 curl_close($ch);
72
73 if (empty($certinfo[0])) {
74 return ['error' => 'Could not retrieve certificate via cURL.'];
75 }
76
77 $c = $certinfo[0];
78 $expiry = isset($c['Expire date']) ? strtotime($c['Expire date']) : 0;
79 $issued = isset($c['Start date']) ? strtotime($c['Start date']) : 0;
80 $days = $expiry ? (int) round(($expiry - time()) / DAY_IN_SECONDS) : 0;
81
82 return [
83 'host' => $host,
84 'cn' => $c['Subject'] ?? $host,
85 'issuer' => $c['Issuer'] ?? '',
86 'issued' => $issued ? gmdate('Y-m-d', $issued) : '',
87 'expiry' => $expiry ? gmdate('Y-m-d', $expiry) : '',
88 'expiry_ts' => $expiry,
89 'days' => $days,
90 'status' => self::_status($days),
91 'sans' => [],
92 'error' => null,
93 ];
94 }
95
96 private static function _parse_cert($cert, string $host): array {
97 $info = openssl_x509_parse($cert);
98 if (!$info) return ['error' => 'Could not parse certificate data.'];
99 $expiry = (int) ($info['validTo_time_t'] ?? 0);
100 $issued = (int) ($info['validFrom_time_t'] ?? 0);
101 $days = $expiry ? (int) round(($expiry - time()) / DAY_IN_SECONDS) : 0;
102
103 $sans = [];
104 if (!empty($info['extensions']['subjectAltName'])) {
105 preg_match_all('/DNS:([^,\s]+)/', $info['extensions']['subjectAltName'], $m);
106 $sans = $m[1] ?? [];
107 }
108
109 $cn = $info['subject']['CN'] ?? $host;
110 $issuer = $info['issuer']['O'] ?? ($info['issuer']['CN'] ?? '');
111
112 return [
113 'host' => $host,
114 'cn' => $cn,
115 'issuer' => $issuer,
116 'issued' => $issued ? gmdate('Y-m-d', $issued) : '',
117 'expiry' => $expiry ? gmdate('Y-m-d', $expiry) : '',
118 'expiry_ts' => $expiry,
119 'days' => $days,
120 'status' => self::_status($days),
121 'sans' => $sans,
122 'error' => null,
123 ];
124 }
125
126 private static function _status(int $days): string {
127 if ($days < 0) return 'expired';
128 if ($days < 7) return 'critical';
129 if ($days < 30) return 'warning';
130 return 'ok';
131 }
132
133 // Checks the site's own cert + any stored extra domains
134 public static function check_all(): array {
135 if (!self::_pro()) return [];
136 $site_host = parse_url(get_site_url(), PHP_URL_HOST) ?: '';
137 $hosts = [$site_host];
138
139 $extra = self::get_extra_domains();
140 foreach ($extra as $h) {
141 if ($h && $h !== $site_host) $hosts[] = $h;
142 }
143
144 $results = [];
145 foreach (array_unique($hosts) as $host) {
146 $cache_key = 'phpinfowp_ssl_' . md5($host);
147 $cached = get_transient($cache_key);
148 if ($cached !== false) {
149 $cached['cached'] = true;
150 $results[] = $cached;
151 } else {
152 $r = self::check($host);
153 set_transient($cache_key, $r, 6 * HOUR_IN_SECONDS);
154 $results[] = $r;
155 }
156 }
157 return $results;
158 }
159
160 public static function bust_cache(): void {
161 $hosts = [parse_url(get_site_url(), PHP_URL_HOST) ?: ''];
162 foreach (self::get_extra_domains() as $h) $hosts[] = $h;
163 foreach ($hosts as $h) delete_transient('phpinfowp_ssl_' . md5($h));
164 }
165
166 public static function get_extra_domains(): array {
167 if (!self::_pro()) return [];
168 $raw = get_option(self::OPT_DOMAINS, '');
169 return array_values(array_filter(array_map('trim', preg_split('/[\r\n]+/', $raw))));
170 }
171
172 public static function save_extra_domains(string $raw): void {
173 if (!self::_pro()) return;
174 $domains = array_values(array_filter(array_map('trim', preg_split('/[\r\n]+/', $raw))));
175 // Sanitize each as a hostname
176 $clean = array_filter($domains, function ($d) {
177 return preg_match('/^[a-z0-9._-]+$/i', $d);
178 });
179 update_option(self::OPT_DOMAINS, implode("\n", $clean), false);
180 self::bust_cache();
181 }
182
183 public static function status_color(string $status): string {
184 switch ($status) {
185 case 'expired':
186 return '#d63638';
187 case 'critical':
188 return '#d63638';
189 case 'warning':
190 return '#dba617';
191 case 'ok':
192 return '#00a32a';
193 default:
194 return '#666';
195 }
196 }
197
198 public static function status_label(string $status): string {
199 switch ($status) {
200 case 'expired':
201 return 'EXPIRED';
202 case 'critical':
203 return 'CRITICAL';
204 case 'warning':
205 return 'EXPIRING SOON';
206 case 'ok':
207 return 'VALID';
208 default:
209 return 'UNKNOWN';
210 }
211 }
212 }
213