self::LIMIT || ! array_is_list( $existing ) || count( $existing ) > $context['reference_count'] ) { throw new \InvalidArgumentException( 'Incomplete or oversized server context.' ); } $retained = array(); foreach ( $existing as $link ) { if ( ! is_array( $link ) || ! self::digest( $link['link_key'] ?? null ) || ! self::digest( $link['request_binding'] ?? null ) || ! self::uuid( $link['probe_uid'] ?? null ) || isset( $retained[ $link['link_key'] ] ) ) { throw new \InvalidArgumentException( 'Invalid retained temporal identity.' ); } foreach ( array( 'event', 'anchor' ) as $kind ) { if ( ! self::id( $link[ $kind . '_id' ] ?? null ) || (string) $link[ $kind . '_id' ] !== (string) $context[ $kind ]['id'] || ( $link[ $kind . '_uid' ] ?? null ) !== $context[ $kind ]['uid'] ) { throw new \InvalidArgumentException( 'Retained local identity changed.' ); } } if ( ( $link['case_uid'] ?? null ) !== $context['case_uid'] || ( $link['connector_uid'] ?? null ) !== $context['source_uid'] || ( $link['source_binding'] ?? null ) !== $context['source_binding'] || ( $link['source_key'] ?? null ) !== $context['source_key'] || (string) ( $link['relation_version'] ?? '' ) !== '1' || (string) ( $link['window_seconds'] ?? '' ) !== (string) $context['window_seconds'] || (string) ( $link['late_seconds'] ?? '' ) !== (string) $context['late_seconds'] || ! self::id( $link['probe_id'] ?? null ) ) { throw new \InvalidArgumentException( 'Retained source identity changed.' ); } $retained[ $link['link_key'] ] = $link; } $seen_uid = array(); $seen_id = array(); $links = array(); $unchanged = array(); $rejected = 0; foreach ( $requests as $request ) { if ( ! is_array( $request ) || ! self::id( $request['network_id'] ?? null ) || ! self::id( $request['site_id'] ?? null ) || (string) $context['network_id'] !== (string) $request['network_id'] || (string) $context['site_id'] !== (string) $request['site_id'] || ! self::id( $request['id'] ?? null ) || ! self::uuid( $request['request_uid'] ?? null ) ) { throw new \InvalidArgumentException( 'Invalid scoped imported identity.' ); } foreach ( array( 'captured_at' => 19, 'ingested_at' => 19, 'sensor' => 24, 'source' => 24, 'family' => 48, 'safe_path' => 240, 'method' => 12, 'source_quality' => 24, 'source_generation' => 32, 'own_response' => 24, 'alert_state' => 24, 'effect' => 24, 'evidence' => 4096, 'parser_version' => 64, 'rule_id' => 96, ) as $field => $limit ) { if ( ! is_string( $request[ $field ] ?? null ) || strlen( $request[ $field ] ) > $limit ) { throw new \InvalidArgumentException( 'Unbounded imported projection.' ); } } foreach ( array( 'partial', 'rule_revision' ) as $field ) { if ( ( ! is_int( $request[ $field ] ?? null ) && ! is_string( $request[ $field ] ?? null ) ) || strlen( (string) $request[ $field ] ) > 16 ) { throw new \InvalidArgumentException( 'Invalid imported scalar.' ); } } $binding = self::binding( $request ); $uid = $request['request_uid']; $id = (string) $request['id']; if ( isset( $seen_uid[ $uid ] ) || isset( $seen_id[ $id ] ) ) { if ( ( $seen_uid[ $uid ] ?? null ) !== $binding || ( $seen_id[ $id ] ?? null ) !== $uid ) { throw new \InvalidArgumentException( 'Conflicting imported identity.' ); } continue; } $seen_uid[ $uid ] = $binding; $seen_id[ $id ] = $uid; $at = self::stamp( $request['captured_at'] ); $ingested = self::stamp( $request['ingested_at'] ); $event_at = self::event_microseconds( $context['event']['occurred_at'] ); if ( null === $at || null === $ingested || $at * 1000000 > $event_at || $at * 1000000 < $event_at - $context['window_seconds'] * 1000000 || $at > $ingested || $ingested > $now || $ingested - $at > $context['late_seconds'] || $at + $context['request_days'] * 86400 <= $now || ( 'synthetic_test' === $context['event']['source'] ? 'synthetic_test' : 'server_log' ) !== $request['source'] || ! self::projection( $request, $context['source_uid'] ) ) { ++$rejected; continue; } // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- Preserve the versioned case/event/request/source identity tuple and throwing JSON encoder used to derive immutable deduplication keys. $key = hash( 'sha256', json_encode( array( 'server_pattern_window_v1', $context['network_id'], $context['site_id'], $context['case_uid'], $context['event']['uid'], $uid, $context['source_uid'] ), JSON_THROW_ON_ERROR ) ); $old = $retained[ $key ] ?? null; if ( null !== $old && $old['request_binding'] !== $binding ) { throw new \InvalidArgumentException( 'Retained imported projection changed.' ); } $probe = null; foreach ( $context['probes'] as $candidate ) { if ( $candidate['family'] === $request['family'] && $candidate['safe_path'] === $request['safe_path'] && ( null === $old || $old['probe_uid'] === $candidate['uid'] ) && ( null === $probe || strcmp( $candidate['uid'], $probe['uid'] ) < 0 ) ) { $probe = $candidate; } } if ( null === $probe ) { ++$rejected; continue; } if ( null !== $old ) { if ( (string) $old['probe_id'] !== (string) $probe['id'] || (string) ( $old['proof_id'] ?? '' ) !== (string) $probe['proof_id'] || ( $old['proof_key'] ?? null ) !== $probe['proof_key'] ) { throw new \InvalidArgumentException( 'Retained probe identity changed.' ); } $unchanged[] = $key; continue; } // A Free read can validate retained relations, never propose new correlation. ++$rejected; } if ( $context['reference_count'] + count( $links ) > self::LIMIT || ( $links && $context['history_count'] >= 199 ) ) { throw new \InvalidArgumentException( 'Temporal references exceed the case bound.' ); } usort( $links, static fn( array $a, array $b ): int => strcmp( $a['link_key'], $b['link_key'] ) ); sort( $unchanged, SORT_STRING ); return array( 'version' => self::VERSION, 'case_uid' => $context['case_uid'], 'expected_revision' => $context['revision'], 'source_binding' => $context['source_binding'], 'links' => $links, 'unchanged' => $unchanged, 'rejected' => $rejected, ); } /** * Require a finite, current local reference contract; this is not an authorization check. * * @param array $context Case, source, anchor and probe references to validate. * @param int $now Current evaluation epoch in seconds. * @return void * @throws \InvalidArgumentException If any required identity, bound or local time relation is invalid. */ private static function context( array $context, int $now ): void { foreach ( array( 'network_id', 'site_id', 'revision', 'case_expires_at', 'request_days', 'window_seconds', 'late_seconds', 'reference_count', 'history_count' ) as $key ) { if ( ! is_int( $context[ $key ] ?? null ) || $context[ $key ] < 1 ) { throw new \InvalidArgumentException( 'Invalid temporal context bound.' ); } } if ( $now < 1 || $context['revision'] >= PHP_INT_MAX || $context['case_expires_at'] <= $now || $context['request_days'] > 3650 || $context['window_seconds'] < 60 || $context['window_seconds'] > 3600 || $context['late_seconds'] > 86400 || $context['reference_count'] > self::LIMIT || $context['history_count'] > 200 || ! self::uuid( $context['case_uid'] ?? null ) || ! self::uuid( $context['source_uid'] ?? null ) || ! self::digest( $context['source_binding'] ?? null ) || ! self::digest( $context['source_key'] ?? null ) || ! is_array( $context['probes'] ?? null ) || ! array_is_list( $context['probes'] ) || ! $context['probes'] || count( $context['probes'] ) > self::LIMIT ) { throw new \InvalidArgumentException( 'Invalid current temporal context.' ); } foreach ( array( 'event', 'anchor' ) as $kind ) { $row = $context[ $kind ] ?? null; if ( ! is_array( $row ) || ! self::id( $row['id'] ?? null ) || ! self::uuid( $row['uid'] ?? null ) || ! is_int( $row['expires_at'] ?? null ) || $row['expires_at'] <= $now || null === self::stamp( $row['occurred_at'] ?? null, 'event' === $kind ) ) { throw new \InvalidArgumentException( 'Invalid retained local reference.' ); } } $event_at = self::event_microseconds( $context['event']['occurred_at'] ); if ( ( $context['event']['request_uid'] ?? null ) !== $context['anchor']['uid'] || ! in_array( $context['event']['source'] ?? null, array( 'wordpress', 'synthetic_test' ), true ) || intdiv( $event_at, 1000000 ) > $now || abs( $event_at - self::stamp( $context['anchor']['occurred_at'] ) * 1000000 ) > $context['window_seconds'] * 1000000 ) { throw new \InvalidArgumentException( 'Invalid local request anchor.' ); } $uids = array(); $ids = array(); foreach ( $context['probes'] as $probe ) { if ( ! is_array( $probe ) || ! self::id( $probe['id'] ?? null ) || ! self::uuid( $probe['uid'] ?? null ) || ! self::id( $probe['proof_id'] ?? null ) || ! self::digest( $probe['proof_key'] ?? null ) || isset( $uids[ $probe['uid'] ] ) || isset( $ids[ (string) $probe['id'] ] ) || ! is_string( $probe['family'] ?? null ) || ! isset( RequestsClassifier::rules()[ $probe['family'] ] ) || ! is_string( $probe['safe_path'] ?? null ) || strlen( $probe['safe_path'] ) > 240 || ( $probe['source'] ?? null ) !== $context['event']['source'] || ! is_int( $probe['expires_at'] ?? null ) || $probe['expires_at'] <= $now ) { throw new \InvalidArgumentException( 'Invalid retained local probe.' ); } $at = self::stamp( $probe['occurred_at'] ?? null ); if ( null === $at || $at * 1000000 > $event_at || $at * 1000000 < $event_at - $context['window_seconds'] * 1000000 ) { throw new \InvalidArgumentException( 'Local probe is outside the case window.' ); } $uids[ $probe['uid'] ] = true; $ids[ (string) $probe['id'] ] = true; } } /** * Reproduce the exact current Free import projection over a finite safe-path catalog. * * @param array $row Imported request fields and minimized JSON evidence. * @param string $connector Expected current connector UUID. * @return bool Whether the row matches an allowed current classifier projection. */ private static function projection( array $row, string $connector ): bool { if ( 'server_log' !== ( $row['sensor'] ?? null ) || ! in_array( $row['source'] ?? null, array( 'server_log', 'synthetic_test' ), true ) || '1' !== (string) ( $row['partial'] ?? '' ) || 'reported_server' !== ( $row['source_quality'] ?? null ) || str_repeat( '0', 32 ) !== ( $row['source_generation'] ?? null ) || 'none' !== ( $row['own_response'] ?? null ) || 'skipped' !== ( $row['alert_state'] ?? null ) || 'unknown' !== ( $row['effect'] ?? null ) || ! in_array( $row['method'] ?? null, self::METHODS, true ) || ! is_string( $row['evidence'] ?? null ) || strlen( $row['evidence'] ) > 4096 || ! is_string( $row['family'] ?? null ) || ! is_string( $row['safe_path'] ?? null ) ) { return false; } $rule = RequestsClassifier::rules()[ $row['family'] ] ?? null; if ( null === $rule || RequestsClassifier::VERSION !== $row['parser_version'] || $rule['id'] !== $row['rule_id'] || ! in_array( $row['rule_revision'] ?? null, array( $rule['revision'], (string) $rule['revision'] ), true ) || ! in_array( $row['partial'] ?? null, array( 1, '1' ), true ) ) { return false; } $data = json_decode( $row['evidence'], true, 12 ); $import = is_array( $data ) ? ( $data['import'] ?? null ) : null; if ( ! is_array( $import ) || ( $import['connector_uid'] ?? null ) !== $connector || ! is_int( $import['response_bytes'] ?? null ) || $import['response_bytes'] < 0 || $import['response_bytes'] > 9007199254740991 ) { return false; } foreach ( array_merge( RequestsClassifier::safe_paths(), array( '/wp-content/plugins/reference/readme.txt', '/wp-content/themes/reference/readme.txt' ) ) as $path ) { $analysis = RequestsClassifier::analyze( $path, $row['method'] ); $first = $analysis['matches'][0] ?? null; if ( null === $first || $first['family'] !== $row['family'] || $first['safe_template'] !== $row['safe_path'] ) { continue; } $analysis['partial'] = true; $analysis['quality_flags'][] = 'server_safe_class_projection'; $analysis['quality_flags'][] = 'original_target_and_query_unavailable'; $analysis['import'] = array( 'connector_uid' => $connector, 'format' => 'nginx_json_lines_minimized_v1', 'projection' => 'safe-path-classes-v1', 'query_coverage' => 'not_collected', 'response_bytes' => $import['response_bytes'], 'future_within_skew' => false, 'identity' => 'unverified', 'response_attribution' => 'reporting_server', ); // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- Compare reconstructed finite imported evidence to its exact stored JSON bytes; a display-oriented encoding repair cannot replace this projection identity check. if ( json_encode( $analysis ) === $row['evidence'] ) { return true; } } return false; } /** * Bind only the selected address-free input projection; never hash an address or arbitrary row fields. * * @param array $row Imported row from which the fixed field list is selected. * @return string SHA-256 digest of the selected field values. * @throws \JsonException If the selected projection cannot be encoded. */ private static function binding( array $row ): string { $selected = array(); foreach ( array( 'id', 'request_uid', 'network_id', 'site_id', 'captured_at', 'ingested_at', 'sensor', 'source', 'family', 'safe_path', 'method', 'partial', 'source_quality', 'source_generation', 'own_response', 'alert_state', 'effect', 'parser_version', 'rule_id', 'rule_revision', 'evidence' ) as $field ) { $selected[ $field ] = $row[ $field ] ?? null; } // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- Hash the exact ordered retained-request fields with throwing encoding for replay/change detection; malformed rows must not be repaired into another preimage. return hash( 'sha256', json_encode( $selected, JSON_THROW_ON_ERROR ) ); } /** * Preserve event precision at the inclusive occurrence-window boundary. * * @param string $value Previously validated UTC event timestamp with six fractional digits. * @return int Event epoch in microseconds. */ private static function event_microseconds( string $value ): int { return self::stamp( $value, true ) * 1000000 + (int) substr( $value, 20, 6 ); } /** * Parse an exact UTC timestamp without normalizing invalid date fields. * * @param mixed $value Candidate timestamp to check. * @param bool $microseconds Whether six fractional-second digits are required. * @return int|null Positive epoch seconds, or null when the timestamp does not match. * @throws \ValueError If the native date parser rejects a NUL byte in the input. */ private static function stamp( mixed $value, bool $microseconds = false ): ?int { $format = $microseconds ? 'Y-m-d H:i:s.u' : 'Y-m-d H:i:s'; if ( ! is_string( $value ) || strlen( $value ) !== ( $microseconds ? 26 : 19 ) ) { return null; } $time = \DateTimeImmutable::createFromFormat( '!' . $format, $value, new \DateTimeZone( 'UTC' ) ); return $time && $time->format( $format ) === $value && $time->getTimestamp() > 0 ? $time->getTimestamp() : null; } /** * Check the finite positive decimal identifier representation. * * @param mixed $value Candidate integer or decimal string. * @return bool Whether the value contains one to nineteen positive decimal digits. */ private static function id( mixed $value ): bool { return ( is_string( $value ) || is_int( $value ) ) && 1 === preg_match( '/\A[1-9][0-9]{0,18}\z/D', (string) $value ); } /** * Check the lowercase UUID representation used by stored references. * * @param mixed $value Candidate UUID string. * @return bool Whether the UUID has the required hexadecimal grouping. */ private static function uuid( mixed $value ): bool { return is_string( $value ) && 1 === preg_match( '/\A[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\z/D', $value ); } /** * Check the lowercase SHA-256 digest representation. * * @param mixed $value Candidate digest string. * @return bool Whether the value contains exactly sixty-four hexadecimal digits. */ private static function digest( mixed $value ): bool { return is_string( $value ) && 1 === preg_match( '/\A[a-f0-9]{64}\z/D', $value ); } }