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 / feedback / ErrorEmailBodyFormatter.php

ErrorEmailBodyFormatter.php in 404 Solution trunk, at includes/feedback/ErrorEmailBodyFormatter.php

148 lines 7.2 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 /**
9 * Presentation layer for the developer error/heartbeat email sent by
10 * ABJ_404_Solution_Logging::emailLogFileToDeveloper().
11 *
12 * Pure transform from a FeedbackTransport payload to the strings that go to
13 * wp_mail(): subject line and HTML body. No file I/O, no DB writes, no
14 * wp_mail() dispatch, no service container lookups. The result is therefore
15 * unit-testable without mocking the WordPress mail surface, and the body
16 * shape lives in one place rather than being intertwined with zip creation
17 * and dispatch.
18 *
19 * The body format is plain text separated by literal "<BR/>\n" because the
20 * Content-Type header on the wp_mail() send is text/html; consumers of this
21 * email (the plugin developer's inbox) have been rendering it that way since
22 * well before this refactor and the format is preserved verbatim.
23 */
24 class ABJ_404_Solution_ErrorEmailBodyFormatter {
25
26 /**
27 * Build the wp_mail() subject line.
28 *
29 * @param array<string, mixed> $payload FeedbackTransport payload.
30 * @return string
31 */
32 public function buildSubject(array $payload): string {
33 $isHeartbeat = $this->isHeartbeat($payload);
34 $pluginVersion = $this->scalarString($payload, 'plugin_version',
35 defined('ABJ404_VERSION') ? ABJ404_VERSION : '');
36 $brand = defined('ABJ404_PP') ? ABJ404_PP : '404 Solution';
37 return $brand . ($isHeartbeat ? ' heartbeat' : ' error') . ' log file. Plugin version: ' . $pluginVersion;
38 }
39
40 /**
41 * Build the wp_mail() HTML body.
42 *
43 * @param array<string, mixed> $payload FeedbackTransport payload.
44 * @param string $subject Pre-built subject line (kept as the first body
45 * line for forwarding-friendliness).
46 * @param string $debugFilename Name of the on-disk debug file the zip
47 * was built from. Included so the developer can correlate which
48 * rotated file the attached log came from.
49 * @return string
50 */
51 public function buildBody(array $payload, string $subject, string $debugFilename): string {
52 $errorLineMessage = $this->scalarString($payload, 'error_signature', '');
53 $totalErrorCount = $this->scalarInt($payload, 'error_count_in_log', 0);
54
55 $logTableSizeMB = round($this->scalarInt($payload, 'log_table_size_bytes', 0) / (1024 * 1024), 2);
56 $debugFileSizeMB = round($this->scalarInt($payload, 'debug_file_size_bytes', 0) / (1024 * 1024), 2);
57 $extensions = isset($payload['extensions']) && is_array($payload['extensions']) ? $payload['extensions'] : array();
58 $activePlugins = isset($payload['active_plugins']) && is_array($payload['active_plugins']) ? $payload['active_plugins'] : array();
59 $isMultisite = !empty($payload['is_multisite']);
60
61 $lines = array();
62 $lines[] = $subject . ". Sent " . date('Y/m/d h:i:s T', abj_clock()->now());
63 $lines[] = " ";
64 $lines[] = "Error: " . $errorLineMessage;
65 $lines[] = " ";
66 $lines[] = "PHP version: " . $this->scalarString($payload, 'php_version', PHP_VERSION);
67 $lines[] = "WordPress version: " . $this->scalarString($payload, 'wp_version', '');
68 $lines[] = "Plugin version: " . $this->scalarString($payload, 'plugin_version',
69 defined('ABJ404_VERSION') ? ABJ404_VERSION : '');
70 $lines[] = "MySQL version: " . $this->scalarString($payload, 'db_version', '');
71 $lines[] = "Site URL: " . $this->scalarString($payload, 'site_url', '');
72 $lines[] = "Multisite: " . ($isMultisite ? 'yes' : 'no');
73 if ($isMultisite && function_exists('is_plugin_active_for_network') && defined('ABJ404_FILE')
74 && function_exists('plugin_basename')) {
75 $lines[] = "Network activated: " .
76 (is_plugin_active_for_network(plugin_basename(ABJ404_FILE)) ? 'yes' : 'no');
77 }
78 $lines[] = "WP_MEMORY_LIMIT: " . (defined('WP_MEMORY_LIMIT') ? WP_MEMORY_LIMIT : '');
79
80 $extensionStrings = array_map(static function ($v): string {
81 return is_scalar($v) ? (string)$v : '';
82 }, $extensions);
83 $lines[] = "Extensions: " . implode(", ", $extensionStrings);
84
85 $lines[] = " ";
86 $lines[] = "--- WordPress Content Counts ---";
87 $lines[] = "Published posts: " . $this->scalarString($payload, 'published_posts_count', '0');
88 $lines[] = "Published pages: " . $this->scalarString($payload, 'published_pages_count', '0');
89 $lines[] = "Categories: " . $this->scalarString($payload, 'categories_count', '0');
90 $lines[] = "Tags: " . $this->scalarString($payload, 'tags_count', '0');
91
92 $lines[] = " ";
93 $lines[] = "--- 404 Solution Counts ---";
94 $lines[] = "Total redirects (active): " . $this->scalarString($payload, 'redirects_active_total', '0');
95 $lines[] = " - Manual redirects: " . $this->scalarString($payload, 'redirects_manual_count', '0');
96 $lines[] = " - Automatic redirects: " . $this->scalarString($payload, 'redirects_automatic_count', '0');
97 $lines[] = " - Regex redirects: " . $this->scalarString($payload, 'redirects_regex_count', '0');
98 $lines[] = " - Trashed redirects: " . $this->scalarString($payload, 'redirects_trashed_count', '0');
99 $lines[] = "Captured 404s (active): " . $this->scalarString($payload, 'captured_404s_active_total', '0');
100 $lines[] = " - Captured (new): " . $this->scalarString($payload, 'captured_404s_new_count', '0');
101 $lines[] = " - Ignored: " . $this->scalarString($payload, 'captured_404s_ignored_count', '0');
102 $lines[] = " - Later: " . $this->scalarString($payload, 'captured_404s_later_count', '0');
103 $lines[] = " - Trashed: " . $this->scalarString($payload, 'captured_404s_trashed_count', '0');
104 $lines[] = "Log entries in database: " . $this->scalarString($payload, 'log_entries_count', '0');
105 $lines[] = "Log table size: " . $logTableSizeMB . " MB";
106
107 $lines[] = " ";
108 $lines[] = "Total error count in log file: " . $totalErrorCount;
109 $lines[] = "Debug file name: " . $debugFilename;
110 $lines[] = "Debug file size: " . $debugFileSizeMB . " MB";
111 $lines[] = "Active plugins: <pre>" .
112 json_encode($activePlugins, JSON_PRETTY_PRINT) . "</pre>";
113
114 return implode("<BR/>\n", $lines);
115 }
116
117 /** @param array<string, mixed> $payload */
118 private function isHeartbeat(array $payload): bool {
119 return isset($payload['report_type']) && $payload['report_type'] === 'heartbeat';
120 }
121
122 /**
123 * @param array<string, mixed> $payload
124 * @param string $key
125 * @param string $fallback
126 * @return string
127 */
128 private function scalarString(array $payload, string $key, string $fallback): string {
129 if (!array_key_exists($key, $payload)) {
130 return $fallback;
131 }
132 return is_scalar($payload[$key]) ? (string)$payload[$key] : $fallback;
133 }
134
135 /**
136 * @param array<string, mixed> $payload
137 * @param string $key
138 * @param int $fallback
139 * @return int
140 */
141 private function scalarInt(array $payload, string $key, int $fallback): int {
142 if (!array_key_exists($key, $payload)) {
143 return $fallback;
144 }
145 return is_scalar($payload[$key]) ? (int)$payload[$key] : $fallback;
146 }
147 }
148