getMessage(), 0, 200) . '. Reproduce the 404 once, then generate support data again; include this code if it recurs.'; } } /** * The census as a scannable header line plus one JSON record. * * Over-budget input sheds the suspect ROSTER -- the one reducible part -- * in two steps, keeping the earliest names before giving them up entirely, * and only then falls back to the finding alone. Never cut at a byte offset: * a record cut mid-JSON is unreadable by machine and misleading to a human, * which is the same failure the drained client buffer already taught the * composer (see SupportEvidenceExcerpt::appendClientTransportTelemetry). * * @param array $record ABJ_404_Solution_CanonicalRedirectHookCensus::read(). */ private static function render(array $record): string { if ($record === array()) { return 'Canonical hook census -- not yet observed: this site has not served a front-end' . ' 404 through this build, so whether WordPress core would still canonicalize was' . " never read. Absent, not intact.\n"; } $header = 'Canonical hook census -- ' . self::summary($record) . " (JSON):\n"; $roster = isset($record['callbacks']) && is_array($record['callbacks']) ? $record['callbacks'] : array(); $shortened = $record; $shortened['callbacks'] = array_slice($roster, 0, self::REDUCED_CALLBACK_COUNT); $shortened['callbacks_reduced'] = 'earliest_' . self::REDUCED_CALLBACK_COUNT; $reduced = $record; $reduced['callbacks'] = array(); $reduced['callbacks_reduced'] = 'over_budget'; $minimal = array( 'core_canonical' => self::textOf($record, 'core_canonical'), 'suppression' => isset($record['suppression']) && is_array($record['suppression']) ? $record['suppression'] : array(), 'core_canonical_priority' => $record['core_canonical_priority'] ?? null, 'plugin_listener_priority' => $record['plugin_listener_priority'] ?? null, 'callback_count' => self::countOf($record, 'callback_count'), 'recorded_at' => self::countOf($record, 'recorded_at'), 'reduced' => 'over_budget', ); foreach (array($record, $shortened, $reduced, $minimal) as $candidate) { $line = json_encode(array(self::CANONICAL_HOOK_CENSUS_KEY => $candidate)); if (is_string($line) && strlen($header) + strlen($line) <= self::MAX_CANONICAL_SUPPRESSION_BYTES) { return $header . $line; } } return $header . 'The census record could not be encoded for this payload. JSON error ' . (string)json_last_error() . ': ' . json_last_error_msg() . '. Check callback names and origins for invalid UTF-8.'; } /** * The one-line version, so the first thing a reader sees is the finding and * how much of the hook it was drawn from. * * "attached" with no suppression reasons and "attached" with the plugin * running first are entirely different findings -- the second one names this * plugin as the reason core never got its turn -- so the reasons are part of * the summary rather than buried in the record. * * @param array $record */ private static function summary(array $record): string { $reasons = isset($record['suppression']) && is_array($record['suppression']) ? array_filter($record['suppression'], 'is_scalar') : array(); return 'core redirect_canonical ' . self::textOf($record, 'core_canonical') . '; ' . ($reasons === array() ? 'no suppression found' : 'suppression: ' . implode(', ', $reasons)) . '; ' . self::countOf($record, 'callback_count') . ' callback(s) on ' . self::textOf($record, 'hook') . '; observed at ' . self::countOf($record, 'recorded_at'); } /** * One record field as a string, or '' when it is absent or not scalar. * * @param array $record */ private static function textOf(array $record, string $field): string { $value = $record[$field] ?? null; return is_scalar($value) ? (string)$value : ''; } /** * One record field as an integer, or 0 when it is absent or not scalar. * * @param array $record */ private static function countOf(array $record, string $field): int { $value = $record[$field] ?? null; return is_scalar($value) ? (int)$value : 0; } }