PluginProbe
phpinfo() WP – Site Health, PHP Compatibility & Server Audit / 7.2.6
phpinfo() WP – Site Health, PHP Compatibility & Server Audit v7.2.6
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.6, at includes/class-ssl.php

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