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 / DebugLogArchiveBuilder.php

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

126 lines 5.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 if (!defined('ABSPATH')) {
5 exit;
6 }
7
8 // allow-no-test-found: covered by tests/LoggingTest.php zip-failure mail fallback entry-point test.
9
10 /**
11 * Builds the developer-feedback debug-log zip attachment.
12 *
13 * The builder only creates the archive from paths supplied by the caller. It
14 * does not send mail and does not decide whether a missing/failed archive
15 * should abort the report.
16 */
17 class ABJ_404_Solution_DebugLogArchiveBuilder {
18
19 /**
20 * Name of the entry that states what the archive was ASKED to carry.
21 *
22 * A file that does not exist is skipped rather than failing the archive,
23 * which leaves the receiver with the same ambiguity the support payload's
24 * empty excerpt used to have: nothing separates "this site wrote no
25 * journal" from "the builder was handed paths on the wrong node". The
26 * manifest is how a zip that is missing a journal says so out loud. Same
27 * property as ABJ_404_Solution_DiagnosticCollectionManifest, on the
28 * out-of-band channel.
29 */
30 const MANIFEST_ENTRY = 'abj404-archive-manifest.txt';
31
32 /**
33 * Create a zip containing the current and rotated debug logs, plus any
34 * additional diagnostic files the caller wants carried WHOLE.
35 *
36 * The additional-paths channel exists because the support payload's
37 * diagnostic journals ride a byte budget and a ranking: whatever that
38 * budget decides to drop is gone, and the failing session is one we only
39 * get once. The archive has no such bound, so it is the out-of-band copy.
40 *
41 * @param string $zipPath
42 * @param string $debugFilePath
43 * @param string $oldDebugFilePath
44 * @param array<int, string> $additionalPaths Absolute paths; missing ones are skipped but stated.
45 * @return string The requested zip path, or an empty string when ZipArchive is unavailable.
46 */
47 public function build(string $zipPath, string $debugFilePath, string $oldDebugFilePath,
48 array $additionalPaths = array()): string {
49 if (file_exists($zipPath)) {
50 ABJ_404_Solution_FileSystemService::safeUnlink($zipPath);
51 }
52 if (!class_exists('ZipArchive')) {
53 return '';
54 }
55 $zipDirectory = dirname($zipPath);
56 if (!is_dir($zipDirectory)) {
57 abj404_logPhpFallback('logger-internal', 'debug log zip directory does not exist: ' . $zipDirectory);
58 return $zipPath;
59 }
60 $zip = new ZipArchive;
61 $openResult = $zip->open($zipPath, ZipArchive::CREATE);
62 if ($openResult === true) {
63 if (file_exists($debugFilePath)) {
64 $zip->addFile($debugFilePath, basename($debugFilePath));
65 }
66 if (file_exists($oldDebugFilePath)) {
67 $zip->addFile($oldDebugFilePath, basename($oldDebugFilePath));
68 }
69 foreach ($additionalPaths as $additionalPath) {
70 if (is_string($additionalPath) && $additionalPath !== '' && file_exists($additionalPath)) {
71 $zip->addFile($additionalPath, basename($additionalPath));
72 }
73 }
74 $zip->addFromString(self::MANIFEST_ENTRY,
75 $this->manifest(array_merge(array($debugFilePath, $oldDebugFilePath), $additionalPaths)));
76 if (!$zip->close()) {
77 abj404_logPhpFallback('logger-internal', 'debug log zip close failed for ' . $zipPath);
78 }
79 } else {
80 abj404_logPhpFallback(
81 'logger-internal',
82 'debug log zip open failed for ' . $zipPath . ' (status ' . (string)$openResult . ')'
83 );
84 }
85 return $zipPath;
86 }
87
88 /**
89 * One line per requested path: whether it was there, how big it was, and
90 * when it last changed. Requested paths, not added ones, because the
91 * question this answers is "what did the collector look for", and the
92 * absent ones are the interesting half of the answer.
93 *
94 * @param array<int, string> $requestedPaths
95 */
96 private function manifest(array $requestedPaths): string {
97 $hostname = ABJ_404_Solution_PhpRuntimeCapabilityAdapter::hostname();
98 $pid = ABJ_404_Solution_PhpRuntimeCapabilityAdapter::processId();
99 $processIdentity = $pid !== null
100 ? 'pid ' . $pid
101 : 'synthetic process ' . ABJ_404_Solution_PhpRuntimeCapabilityAdapter::processToken();
102 $lines = array(
103 'Files this archive was asked to carry, and whether they were found.',
104 'Collected by ' . ($hostname !== null ? $hostname : 'unknown-host')
105 . ' ' . $processIdentity . ' (' . PHP_SAPI . ').',
106 '',
107 );
108 foreach ($requestedPaths as $path) {
109 if (!is_string($path) || $path === '') {
110 continue;
111 }
112 if (!file_exists($path)) {
113 $lines[] = 'absent ' . $path;
114 continue;
115 }
116 $size = @filesize($path);
117 $modified = @filemtime($path);
118 $lines[] = 'present ' . $path
119 . ' (' . (is_int($size) ? $size . ' bytes' : 'size unreadable')
120 . ', modified ' . (is_int($modified) ? gmdate('Y-m-d H:i:s', $modified) . ' UTC' : 'unknown')
121 . ')';
122 }
123 return implode("\n", $lines) . "\n";
124 }
125 }
126