| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
/** |
| 8 |
* Proof that the diagnostics directory is genuinely usable for THIS request. |
| 9 |
* |
| 10 |
* A resolved directory path is not evidence of anything. Beta.1 ended with |
| 11 |
* journals that "came back empty", and an empty journal has several |
| 12 |
* indistinguishable causes: a path that resolved but is not writable, a host |
| 13 |
* that writes to a per-worker filesystem, an open_basedir restriction, a quota, |
| 14 |
* a directory that exists for the writing worker and not for the reading one. |
| 15 |
* Only an actual round trip can separate those, and only if it is performed by |
| 16 |
* the request being diagnosed rather than by a health check at some other time. |
| 17 |
* |
| 18 |
* So this walks the whole file lifecycle -- create, append, flush, stat, glob, |
| 19 |
* read back, delete -- against a request-specific file in that directory, and |
| 20 |
* journals the outcome of every step. The step name in a failed record IS the |
| 21 |
* finding: "stat" says the bytes did not land, "glob" says the directory cannot |
| 22 |
* be enumerated (which is exactly what the support collector later has to do), |
| 23 |
* "delete" says the plugin is accumulating files it cannot clean up. |
| 24 |
* |
| 25 |
* The results go through ABJ_404_Solution_AjaxCheckpointLogger, the immediate |
| 26 |
* append-only channel, rather than through the staged trace journal this is |
| 27 |
* partly probing on behalf of: a defect in the component under investigation |
| 28 |
* must not be able to erase the evidence about it. That makes this class a |
| 29 |
* PRODUCER of checkpoint records, like every other instrumented boundary, and |
| 30 |
* the recorder knows nothing about it. |
| 31 |
*/ |
| 32 |
final class ABJ_404_Solution_DiagnosticDirectoryProbe { |
| 33 |
|
| 34 |
/** Prefix for the probe file, also the glob the enumeration step tests. */ |
| 35 |
const FILE_PREFIX = 'abj404_checkpoint_selftest_'; |
| 36 |
|
| 37 |
/** |
| 38 |
* Run the whole round trip and journal each step's outcome. Never throws: |
| 39 |
* a probe that cannot complete is a finding about the host, and it must not |
| 40 |
* become the reason the request it was proving fails. |
| 41 |
*/ |
| 42 |
public static function run(string $requestId): void { |
| 43 |
try { |
| 44 |
$directory = ABJ_404_Solution_AjaxCheckpointLogger::resolveDirectory(); |
| 45 |
if ($directory === '') { |
| 46 |
self::recordStep($requestId, false, 'resolve_directory'); |
| 47 |
return; |
| 48 |
} |
| 49 |
$path = $directory . self::FILE_PREFIX . $requestId . '_' |
| 50 |
. ABJ_404_Solution_PhpRuntimeCapabilityAdapter::processToken() . '.tmp'; |
| 51 |
$payload = 'abj404-selftest-' . $requestId; |
| 52 |
|
| 53 |
$handle = @fopen($path, 'wb'); |
| 54 |
if ($handle === false) { |
| 55 |
self::recordStep($requestId, false, 'create'); |
| 56 |
return; |
| 57 |
} |
| 58 |
$written = @fwrite($handle, $payload); |
| 59 |
$flushed = @fflush($handle); |
| 60 |
@fclose($handle); |
| 61 |
if ($written === false || !$flushed) { |
| 62 |
self::recordStep($requestId, false, 'append_flush'); |
| 63 |
return; |
| 64 |
} |
| 65 |
|
| 66 |
$size = @filesize($path); |
| 67 |
if (!is_int($size) || $size !== strlen($payload)) { |
| 68 |
self::recordStep($requestId, false, 'stat', array('size' => $size)); |
| 69 |
return; |
| 70 |
} |
| 71 |
|
| 72 |
$globMatches = @glob($directory . self::FILE_PREFIX . '*'); |
| 73 |
$globCount = is_array($globMatches) ? count($globMatches) : 0; |
| 74 |
if ($globCount < 1) { |
| 75 |
self::recordStep($requestId, false, 'glob', array('glob_count' => $globCount)); |
| 76 |
return; |
| 77 |
} |
| 78 |
|
| 79 |
$readBack = @file_get_contents($path); |
| 80 |
if ($readBack !== $payload) { |
| 81 |
self::recordStep($requestId, false, 'read'); |
| 82 |
return; |
| 83 |
} |
| 84 |
|
| 85 |
if (!@unlink($path)) { |
| 86 |
self::recordStep($requestId, false, 'delete'); |
| 87 |
return; |
| 88 |
} |
| 89 |
|
| 90 |
self::recordStep($requestId, true, 'complete', array('glob_count' => $globCount)); |
| 91 |
} catch (Throwable $e) { |
| 92 |
// Reported to the debug log as well as journaled: an exception here |
| 93 |
// is a host condition the plugin cannot act on, and the journal is |
| 94 |
// read only when an admin sends a support request. |
| 95 |
abj404_logPhpFallback('ajax-checkpoint', |
| 96 |
'AJAX checkpoint self-test failed: ' . $e->getMessage()); |
| 97 |
self::recordStep($requestId, false, 'exception', |
| 98 |
array('message' => substr($e->getMessage(), 0, 200))); |
| 99 |
} |
| 100 |
} |
| 101 |
|
| 102 |
/** |
| 103 |
* One step outcome, under the event name the evidence catalogue and both |
| 104 |
* acceptance gates already know this probe by. |
| 105 |
* |
| 106 |
* @param array<string, mixed> $extra |
| 107 |
*/ |
| 108 |
private static function recordStep(string $requestId, bool $ok, string $step, |
| 109 |
array $extra = array()): void { |
| 110 |
ABJ_404_Solution_AjaxCheckpointLogger::record($requestId, 'selftest', |
| 111 |
array_merge(array('ok' => $ok, 'step' => $step), $extra)); |
| 112 |
} |
| 113 |
} |
| 114 |
|