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 / diagnostics / HostFilesystemPressureProbe.php

HostFilesystemPressureProbe.php in 404 Solution trunk, at includes/diagnostics/HostFilesystemPressureProbe.php

179 lines 6.8 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 * Measures filesystem capacity and create/write/cleanup ability for host diagnostics.
9 */
10 final class ABJ_404_Solution_HostFilesystemPressureProbe {
11
12 /** @return array<string, mixed> */
13 public static function capture(): array {
14 $paths = self::defaultProbePaths();
15 if (function_exists('apply_filters')) {
16 try {
17 $filtered = apply_filters('abj404_host_pressure_probe_paths', $paths);
18 $paths = is_array($filtered) ? $filtered : array('configuration' => null);
19 } catch (Throwable $e) {
20 self::reportFailure('filesystem probe-path filter failed: ' . get_class($e) . ' code=' .
21 $e->getCode() . ' message=' . $e->getMessage());
22 return array(
23 'configuration' => array(
24 'status' => 'unavailable',
25 'reason' => 'path_filter_failed',
26 'path' => '',
27 'attempted_paths' => array(
28 'apply_filters(abj404_host_pressure_probe_paths)' =>
29 'exception:' . get_class($e),
30 ),
31 ),
32 );
33 }
34 }
35
36 static $requestCache = array();
37 $cacheKey = hash('sha256', serialize($paths));
38 if (isset($requestCache[$cacheKey])) {
39 return $requestCache[$cacheKey];
40 }
41
42 $probes = array();
43 foreach ($paths as $label => $path) {
44 $probeLabel = is_string($label) && $label !== '' ? $label : 'path_' . count($probes);
45 $probes[$probeLabel] = self::probe($path);
46 }
47 $requestCache[$cacheKey] = $probes;
48 return $probes;
49 }
50
51 /** @return array<string, string> */
52 private static function defaultProbePaths(): array {
53 $paths = array();
54 $contentDirectory = defined('WP_CONTENT_DIR')
55 ? rtrim((string)WP_CONTENT_DIR, '/\\')
56 : (defined('ABSPATH') ? rtrim((string)ABSPATH, '/\\') . '/wp-content' : '');
57 if ($contentDirectory !== '') {
58 $paths['wordpress_uploads'] = $contentDirectory . '/uploads';
59 if (is_dir($contentDirectory . '/cache')) {
60 $paths['wordpress_cache'] = $contentDirectory . '/cache';
61 }
62 }
63 if (function_exists('abj404_getUploadsDir')) {
64 try {
65 $pluginUploads = abj404_getUploadsDir();
66 if (is_string($pluginUploads) && $pluginUploads !== '') {
67 $paths['plugin_diagnostics'] = $pluginUploads;
68 }
69 } catch (Throwable $e) {
70 self::reportFailure('plugin diagnostics path lookup failed: ' . get_class($e) . ' code=' .
71 $e->getCode() . ' message=' . $e->getMessage());
72 }
73 }
74 return $paths;
75 }
76
77 /**
78 * @param mixed $path
79 * @return array<string, mixed>
80 */
81 private static function probe($path): array {
82 if (!is_string($path) || $path === '') {
83 return self::unavailable('invalid_path', array(
84 'configured_path' => !is_string($path) ? 'not_string' : 'empty',
85 ), '');
86 }
87 $path = rtrim($path, '/\\');
88 if (!is_dir($path)) {
89 return self::unavailable('not_directory', array(
90 'is_dir(' . $path . ')' => 'not_directory',
91 ), $path);
92 }
93 $freeBytes = ABJ_404_Solution_PhpRuntimeCapabilityAdapter::diskFreeSpace($path);
94 $totalBytes = ABJ_404_Solution_PhpRuntimeCapabilityAdapter::diskTotalSpace($path);
95
96 return array(
97 'status' => 'available',
98 'path' => $path,
99 'free_bytes' => is_numeric($freeBytes) ? (int)$freeBytes : null,
100 'total_bytes' => is_numeric($totalBytes) ? (int)$totalBytes : null,
101 'create_write_probe' => self::createWriteProbe($path),
102 );
103 }
104
105 /** @return array<string, mixed> */
106 private static function createWriteProbe(string $path): array {
107 $attemptedPaths = array();
108 if (!is_writable($path)) {
109 return self::unavailable('directory_not_writable', array(
110 'is_writable(' . $path . ')' => 'false',
111 ));
112 }
113 $attemptedPaths['is_writable(target_directory)'] = 'true';
114 $sentinel = @tempnam($path, 'abj404-pressure-');
115 if (!is_string($sentinel)) {
116 $attemptedPaths['tempnam(target_directory)'] = 'failed';
117 return self::unavailable('create_failed', $attemptedPaths);
118 }
119 $attemptedPaths['tempnam(target_directory)'] = 'created';
120 $targetDirectory = realpath($path);
121 $createdDirectory = realpath(dirname($sentinel));
122 if ($targetDirectory === false || $createdDirectory !== $targetDirectory) {
123 @unlink($sentinel);
124 $attemptedPaths['realpath(created_directory)'] = 'outside_target';
125 return self::unavailable('create_fell_back_outside_target', $attemptedPaths);
126 }
127 $attemptedPaths['realpath(created_directory)'] = 'target_match';
128 $handle = @fopen($sentinel, 'wb');
129 if ($handle === false) {
130 @unlink($sentinel);
131 $attemptedPaths['fopen(sentinel)'] = 'failed';
132 return self::unavailable('open_failed', $attemptedPaths);
133 }
134 $attemptedPaths['fopen(sentinel)'] = 'opened';
135 $bytesWritten = @fwrite($handle, 'x');
136 $closed = @fclose($handle);
137 $removed = @unlink($sentinel);
138 $attemptedPaths['fwrite(sentinel)'] = 'bytes_written:' . (string)$bytesWritten;
139 $attemptedPaths['fclose(sentinel)'] = $closed ? 'true' : 'false';
140 $attemptedPaths['unlink(sentinel)'] = $removed ? 'true' : 'false';
141 if ($bytesWritten !== 1) {
142 return self::unavailable('write_failed', $attemptedPaths);
143 }
144 if (!$closed) {
145 return self::unavailable('close_failed', $attemptedPaths);
146 }
147 if (!$removed) {
148 return self::unavailable('cleanup_failed', $attemptedPaths);
149 }
150 return array('status' => 'available', 'bytes_written' => 1);
151 }
152
153 /**
154 * @param array<string, string> $attemptedPaths
155 * @return array<string, mixed>
156 */
157 private static function unavailable(
158 string $reason,
159 array $attemptedPaths,
160 ?string $path = null
161 ): array {
162 $reading = array(
163 'status' => 'unavailable',
164 'reason' => $reason,
165 'attempted_paths' => $attemptedPaths,
166 );
167 if ($path !== null) {
168 $reading['path'] = $path;
169 }
170 return $reading;
171 }
172
173 private static function reportFailure(string $message): void {
174 if (function_exists('abj404_logPhpFallback')) {
175 abj404_logPhpFallback('host-pressure', $message);
176 }
177 }
178 }
179