PluginProbe
404 Solution / 4.1.19
404 Solution v4.1.19
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 / schema / feedback_payload_schema.php

feedback_payload_schema.php in 404 Solution 4.1.19, at includes/schema/feedback_payload_schema.php

218 lines 11.4 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 * Wire-schema for ABJ_404_Solution_FeedbackTransport payloads.
9 *
10 * This file is the contract between:
11 * - the PHP producer (FeedbackTransport::buildPayload), tested by
12 * FeedbackPayloadSchemaContractTest;
13 * - the JS producer of the uninstall AJAX form (uninstall-modal.js),
14 * which ships the user-facing extras the PHP builder folds in;
15 * - the reports server endpoint, which accepts these fields and stores
16 * them in typed columns or the environment JSON passthrough.
17 *
18 * Each report type (`error`, `heartbeat`, `uninstall`) shares a base set
19 * of fields and adds a small per-type extras section. The schema is
20 * declared once and merged per-type so a new field added in one place
21 * (say a new server column) requires updates only here, not in three
22 * test files.
23 *
24 * Reference fixes that this schema would have caught:
25 *
26 * - 4080ffb5 B4: resource_limits values shipped as ini shorthand
27 * strings ("256M", "30") instead of integers (bytes/seconds). The
28 * `php_memory`, `wp_memory`, `php_post_max_size`, etc. specifiers
29 * below pin those to `int`.
30 * - 4080ffb5 B4 (field-name): the resource_limits map used names that
31 * did not match the server columns (`max_execution_time` vs
32 * `php_max_execution_seconds`). `unexpected_field` detection +
33 * declared key names catch this.
34 * - 4080ffb5 B5: `active_theme` shipped as `{name, version}` object but
35 * server schema declares `string`. `object_cache` shipped as bool,
36 * server expects "external"/"default" string. `extensions` shipped
37 * as `array<string>`, server expects `object` keyed by ext name.
38 * The specifiers below force the string/object shapes.
39 * - 35380dcc: nested `content_counts` / `redirect_counts` /
40 * `captured_counts` arrays were flattened to top-level fields.
41 * Listing every flat field as required + `unexpected_field` rejects
42 * the nested re-introduction.
43 *
44 * Returns array<string, array<string, array<string, mixed>>> keyed by
45 * report type. Each value is a flat FieldSpec map ready to feed to
46 * ABJ_404_Solution_PayloadSchema::validate().
47 */
48
49 return (function (): array {
50
51 // Base fields shared by every report type. The producer always
52 // returns these regardless of `$type`; the per-type extras are
53 // unioned in below.
54 $base = [
55 // Plugin and request metadata
56 'plugin_version' => ['type' => 'string', 'description' => 'ABJ404_VERSION at send time. Empty string is allowed only in early-boot test contexts.'],
57 'report_type' => ['type' => 'string', 'enum' => ['error', 'heartbeat', 'uninstall', 'support_request']],
58 'is_uninstall' => ['type' => 'bool', 'description' => 'Back-compat alias for report_type=uninstall. True iff report_type=uninstall.'],
59 'site_url' => ['type' => 'string', 'description' => 'home_url(). Server GROUP BY key.'],
60 'locale' => ['type' => 'string'],
61
62 // Database identity
63 'db_type' => ['type' => 'string', 'enum' => ['mysql', 'mariadb']],
64 'db_version' => ['type' => 'string'],
65 'table_prefix' => ['type' => 'string'],
66
67 // WordPress identity
68 'wp_version' => ['type' => 'string'],
69 'is_multisite' => ['type' => 'bool'],
70 'wp_debug' => ['type' => 'bool'],
71
72 // PHP runtime identity
73 'php_version' => ['type' => 'string'],
74 'server_software' => [
75 'type' => 'string',
76 'description' => 'Server software banner, with the Apache mod_status "Server at <host> Port <n>" footer stripped before transmit and capped to 100 chars. See FeedbackTransport::sanitizeServerSoftware().',
77 ],
78
79 // Resource limits. Every value is bytes (for size fields) or
80 // seconds (for time fields). Strings like "256M" are a schema
81 // violation: that was bug 4080ffb5 B4.
82 'resource_limits' => [
83 'type' => 'object',
84 'key_type' => 'string',
85 'value_type' => 'int',
86 'description' => 'Bytes for size fields, seconds for time fields. Never ini shorthand strings.',
87 ],
88 'wp_memory_limit_bytes' => ['type' => 'int|null', 'description' => 'Convenience top-level alias for resource_limits.wp_memory.'],
89
90 // Extensions and active plugins
91 'extensions' => [
92 'type' => 'object',
93 'key_type' => 'string',
94 'value_type' => 'bool',
95 'description' => '{curl:true, mbstring:true, ...} map. Bug 4080ffb5 B5 shipped this as array<string>; the server rejected it.',
96 ],
97 'active_plugins' => ['type' => 'array', 'item_type' => 'string'],
98 'active_theme' => ['type' => 'string', 'description' => '"Name Version" string. Bug 4080ffb5 B5 shipped {name,version} object.'],
99
100 // Cache identity
101 'object_cache' => [
102 'type' => 'string',
103 'enum' => ['external', 'default'],
104 'description' => 'Bug 4080ffb5 B5 shipped this as bool. Server schema is string enum.',
105 ],
106
107 // Content-count diagnostics. Nullable because the underlying
108 // wp_count_posts / wp_count_terms can fail; null distinguishes
109 // "lookup failed" from "really zero".
110 'published_posts_count' => ['type' => 'int|null'],
111 'published_pages_count' => ['type' => 'int|null'],
112 'categories_count' => ['type' => 'int|null'],
113 'tags_count' => ['type' => 'int|null'],
114
115 // Redirect status counts. Flat layout was required by server
116 // schema (commit 35380dcc); the nested layout below the line is
117 // explicitly forbidden by `unexpected_field`.
118 'redirects_active_total' => ['type' => 'int|null'],
119 'redirects_manual_count' => ['type' => 'int|null'],
120 'redirects_automatic_count' => ['type' => 'int|null'],
121 'redirects_regex_count' => ['type' => 'int|null'],
122 'redirects_trashed_count' => ['type' => 'int|null'],
123
124 // Captured-404 status counts.
125 'captured_404s_active_total' => ['type' => 'int|null'],
126 'captured_404s_new_count' => ['type' => 'int|null'],
127 'captured_404s_ignored_count' => ['type' => 'int|null'],
128 'captured_404s_later_count' => ['type' => 'int|null'],
129 'captured_404s_trashed_count' => ['type' => 'int|null'],
130
131 // Log + debug file health.
132 'log_entries_count' => ['type' => 'int|null'],
133 'log_table_size_bytes' => ['type' => 'int|null'],
134 'error_count_in_log' => ['type' => 'int|null'],
135 'debug_file_size_bytes' => ['type' => 'int|null'],
136
137 // Optional dev-environment marker. Only present when the
138 // producer detected a dev host; absence on real prod sites is
139 // expected and not a schema violation.
140 'environment_type' => [
141 'type' => 'string',
142 'enum' => ['development'],
143 'required' => false,
144 ],
145
146 // Bruno/Troy diagnostic passthrough. Sites where the redirects
147 // tab times out or rebuild stalls cannot be triaged from typed
148 // columns alone: the binding constraints are MySQL globals
149 // (innodb_buffer_pool_size, tmp_table_size), disk headroom,
150 // and PHP SAPI specifics that the server doesn't pre-declare.
151 // Stored on the server in the JSON passthrough column (the
152 // existing `extras_json` field on the reports row), keyed by
153 // a stable namespace so future probes can land here without
154 // schema changes on either side. Producer-side detail:
155 // FeedbackTransport::environmentExtras().
156 'environment_extras' => [
157 'type' => 'object',
158 'key_type' => 'string',
159 // value_type intentionally omitted: this is the JSON
160 // passthrough, mixed scalar/object/array values allowed.
161 'description' => 'Best-effort site diagnostics: MySQL globals + status counters + session probe, disk free/total, PHP SAPI / opcache (on/off + detail settings), plugin table sizes (with data_free fragmentation), view-build freshness state, active connection count, per-index cardinality, hosting + panel class, object-cache backend, DB charset/collate + per-column collation, WP+PHP timezone, plugin install/upgrade lifecycle, top recurring error signatures, opcache revalidate/validate/cli detail, open_basedir restriction, multisite role + network-activation, .htaccess writability, /tmp filesystem free bytes. Anything new diagnosed for a recurring-user failure goes here first, then optionally graduates to a typed column.',
162 ],
163 ];
164
165 $errorExtras = [
166 'error_signature' => ['type' => 'string'],
167 'previously_sent_line' => ['type' => 'int'],
168 ];
169
170 $heartbeatExtras = $errorExtras;
171
172 $uninstallExtras = [
173 'uninstall_reason' => ['type' => 'string'],
174 'selected_issues' => ['type' => 'string', 'description' => 'Comma-joined checkbox values from the modal (sanitized server-side).'],
175 'followup_details' => ['type' => 'string'],
176 'better_plugin_name' => ['type' => 'string', 'required' => false],
177 'other_reason_text' => ['type' => 'string', 'required' => false],
178 'contact_email' => ['type' => 'string'],
179 'include_diagnostics' => ['type' => 'bool'],
180 'debug_log' => ['type' => 'string'],
181 ];
182
183 // type='support_request' carries 4 user-facing extras on top of the
184 // standard diagnostic base. Sent by Ajax_SupportRequest, which is the
185 // only producer of this type today; the JS form is bound to a fixed
186 // set of trigger surfaces (the "Send support request" button on the
187 // redirects page, the captured-404s page, the plugins-row action, the
188 // settings debug screen, and the corrupt-install fallback screen).
189 // The triggered_from enum is pinned to the producer's allow-list so
190 // a drift in either direction (PHP adds a surface the schema doesn't
191 // list, or JS posts a value PHP did not accept) fails the wire-schema
192 // validator before the server endpoint sees it. Keep this list in
193 // sync with ABJ_404_Solution_Ajax_SupportRequest::ALLOWED_TRIGGER_SOURCES.
194 $supportRequestExtras = [
195 'user_message' => ['type' => 'string', 'description' => 'Free-text message from the requester (sanitize_textarea_field, capped at MAX_USER_MESSAGE_LENGTH source-side). Empty string allowed.'],
196 'reply_email' => ['type' => 'string', 'description' => 'Optional reply address (sanitize_email). Empty string = anonymous request.'],
197 'triggered_from' => [
198 'type' => 'string',
199 'enum' => [
200 'redirects_page',
201 'captured_404s_page',
202 'plugins_row_action',
203 'settings_debug',
204 'system_corrupt_install',
205 ],
206 'description' => 'Which admin surface launched the request. Pinned enum; mirror of Ajax_SupportRequest::ALLOWED_TRIGGER_SOURCES.',
207 ],
208 'debug_log_excerpt' => ['type' => 'string', 'description' => 'Best-effort log tail via Ajax_SupportRequest::resolveDebugLogExcerpt(). Empty string when the log file is missing or the Logging service is unavailable.'],
209 ];
210
211 return [
212 'error' => array_merge($base, $errorExtras),
213 'heartbeat' => array_merge($base, $heartbeatExtras),
214 'uninstall' => array_merge($base, $uninstallExtras),
215 'support_request' => array_merge($base, $supportRequestExtras),
216 ];
217 })();
218