PluginProbe
404 Solution / trunk
404 Solution vtrunk
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / core / PhpRuntimeCapabilityAdapter.php

PhpRuntimeCapabilityAdapter.php in 404 Solution trunk, at includes/core/PhpRuntimeCapabilityAdapter.php

186 lines 6.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * Capability-safe boundary for PHP functions a hosting provider can remove.
9 *
10 * PHP 8 removes names in disable_functions from the function table, so a
11 * direct call raises Error before a return-value check can help. PHP 7.4 emits
12 * a warning and returns null for the same configuration. Callers use this
13 * adapter so both runtimes degrade to one explicit return contract and the
14 * capability decision exists in one place.
15 */
16 final class ABJ_404_Solution_PhpRuntimeCapabilityAdapter {
17
18 /**
19 * Functions the plugin actually invokes and THIS adapter owns.
20 *
21 * Not the whole boundary: the pcntl and OPcache extensions have their own
22 * adapters and their own lists, because a host removes those as a unit
23 * rather than a function at a time. scripts/lint/lint-disable-functions-boundary.php
24 * composes the union across all three, so a raw call to any of them is
25 * still flagged.
26 */
27 private const OWNED_FUNCTIONS = array(
28 'disk_free_space',
29 'disk_total_space',
30 'error_log',
31 'gethostname',
32 'getmypid',
33 'getrusage',
34 'ini_set',
35 'posix_geteuid',
36 'sys_getloadavg',
37 );
38
39 /** @var string|null Process-stable fallback when the host withholds its PID. */
40 private static $syntheticProcessToken = null;
41
42 /** @return array<int, string> Functions whose direct use this boundary owns. */
43 public static function ownedFunctions(): array {
44 return self::OWNED_FUNCTIONS;
45 }
46
47 /** Whether a PHP function is callable on this host. */
48 public static function isFunctionAvailable(string $name): bool {
49 return function_exists($name);
50 }
51
52 /** The real positive process ID, or null when the host withholds it. */
53 public static function processId(): ?int {
54 if (!self::isFunctionAvailable('getmypid')) {
55 return null;
56 }
57 $pid = getmypid();
58 return is_int($pid) && $pid > 0 ? $pid : null;
59 }
60
61 /**
62 * Stable identity for lock values and filenames within this interpreter.
63 *
64 * The healthy path preserves the previous decimal PID shape. The fallback
65 * is generated once per interpreter and is deliberately labelled synthetic
66 * so it can never be mistaken for a PID. Every uniqueness-sensitive caller
67 * also retains its request id, timestamp, or per-claim uniqid suffix, so the
68 * fallback replaces process correlation without becoming the sole entropy.
69 */
70 public static function processToken(): string {
71 $pid = self::processId();
72 if ($pid !== null) {
73 return (string)$pid;
74 }
75 if (self::$syntheticProcessToken === null) {
76 self::$syntheticProcessToken = 'synthetic-'
77 . substr(hash('sha256', uniqid('', true)), 0, 20);
78 }
79 return self::$syntheticProcessToken;
80 }
81
82 /**
83 * Positive integer for compact internal IDs that historically embedded a
84 * PID. A real PID is preserved; the synthetic token is deterministically
85 * folded only when the host withholds it.
86 */
87 public static function processNumericToken(): int {
88 $pid = self::processId();
89 if ($pid !== null) {
90 return $pid;
91 }
92 return (int)hexdec(substr(hash('sha256', self::processToken()), 0, 7));
93 }
94
95 /** Server hostname, or null when unavailable or invalid. */
96 public static function hostname(): ?string {
97 if (!self::isFunctionAvailable('gethostname')) {
98 return null;
99 }
100 $hostname = gethostname();
101 return is_string($hostname) && $hostname !== '' ? $hostname : null;
102 }
103
104 /** @return array<string, int>|null Process resource counters, or null when unavailable. */
105 public static function resourceUsage(): ?array {
106 if (!self::isFunctionAvailable('getrusage')) {
107 return null;
108 }
109 $usage = getrusage();
110 return is_array($usage) ? $usage : null;
111 }
112
113 /** Effective numeric OS user ID, or null when unavailable. */
114 public static function effectiveUserId(): ?int {
115 if (!self::isFunctionAvailable('posix_geteuid')) {
116 return null;
117 }
118 return posix_geteuid();
119 }
120
121 /** @return float|false */
122 public static function diskFreeSpace(string $path) {
123 return self::isFunctionAvailable('disk_free_space') ? @disk_free_space($path) : false;
124 }
125
126 /** @return float|false */
127 public static function diskTotalSpace(string $path) {
128 return self::isFunctionAvailable('disk_total_space') ? @disk_total_space($path) : false;
129 }
130
131 /** @return array<int, float>|false */
132 public static function systemLoadAverage() {
133 return self::isFunctionAvailable('sys_getloadavg') ? @sys_getloadavg() : false;
134 }
135
136 /**
137 * Set one INI directive for the rest of this request.
138 *
139 * Keyed, not positional. Both parts are strings, and a transposed call is
140 * not an error anywhere: ini_set() would be asked to set a directive named
141 * "0" and would simply return false, so `display_errors` would stay ON
142 * while the caller believed it had turned it off -- which at two of the
143 * call sites here means PHP notices printed into an AJAX response body,
144 * the exact corruption this plugin's canary ladder exists to diagnose.
145 * Shipped code has a PHP 7.4 floor, so a keyed bag is what makes the swap
146 * unwriteable.
147 *
148 * @param array{directive: string, value: string} $setting
149 * @return string|false Previous value, or false when unavailable/refused.
150 */
151 public static function setIni(array $setting) {
152 return self::isFunctionAvailable('ini_set')
153 ? @ini_set($setting['directive'], $setting['value']) : false;
154 }
155
156 /** Write the final PHP-log fallback, or return false when no sink exists. */
157 public static function writeErrorLog(string $message): bool {
158 return self::isFunctionAvailable('error_log') && @error_log($message);
159 }
160
161 /**
162 * Which of the given plugin-owned functions are not callable here, sorted.
163 *
164 * Takes the names rather than reading its own list, so a caller can ask
165 * about the union across every capability boundary without this class
166 * having to depend on the other two -- the support fingerprint does
167 * exactly that, and it must, because a host that removed the pcntl
168 * extension is precisely the host whose diagnostics need to say so.
169 *
170 * Configuration directives are deliberately not consulted or exposed.
171 *
172 * @param array<int, string> $owned
173 * @return array<int, string>
174 */
175 public static function disabledAmong(array $owned): array {
176 $disabled = array();
177 foreach ($owned as $name) {
178 if (!self::isFunctionAvailable($name)) {
179 $disabled[] = $name;
180 }
181 }
182 sort($disabled, SORT_STRING);
183 return $disabled;
184 }
185 }
186