' . esc_html__( 'Recorded changes', 'logged-ai' ) . '';
foreach ( array_slice( $changes, 0, 3 ) as $change ) {
echo '- ' . esc_html( $change ) . '
';
}
echo '
';
if ( count( $changes ) > 3 ) {
/* translators: %d: number of other captured differences available in the event details. */
echo '' . esc_html( sprintf( _n( '%d more change in full evidence.', '%d more changes in full evidence.', count( $changes ) - 3, 'logged-ai' ), count( $changes ) - 3 ) ) . '';
}
echo '';
}
/**
* Summarize allowlisted native changes using only retained scalar fields.
*
* @param array $event Authorized retained event.
* @return array Plain text summaries requiring output escaping.
*/
public static function summary( array $event ): array {
if ( 'configuration.option_changed' === ( $event['code'] ?? null ) ) {
return self::setting_summary( $event );
}
$types = array( 'commerce.product_changed' => 'product', 'commerce.stock_changed' => 'product', 'commerce.order_changed' => 'order' );
$code = $event['code'] ?? '';
$data = $event['detail'] ?? null;
if ( ! is_string( $code ) || ! isset( $types[ $code ] ) || $types[ $code ] !== ( $event['object_type'] ?? null ) || 'stored_change' !== ( $event['outcome'] ?? null )
|| ! is_array( $data ) || array_key_exists( 'extension', $data ) || 'woocommerce_crud_v1' !== ( $data['collector'] ?? '' ) || 'site' !== ( $data['event_scope'] ?? '' )
|| ! in_array( $data['source'] ?? '', array( 'wordpress', 'synthetic_test' ), true ) || ! is_array( $data['diffs'] ?? null ) || count( $data['diffs'] ) > 32 || ! is_array( $data['facts'] ?? null ) ) {
return array();
}
$fields = array();
foreach ( $data['diffs'] as $diff ) {
if ( ! is_array( $diff ) || ! is_string( $diff['field'] ?? null ) || isset( $fields[ $diff['field'] ] ) ) {
return array();
}
$fields[ $diff['field'] ] = $diff;
}
$labels = 'commerce.stock_changed' === $code ? array( 'quantity' => __( 'Stock', 'logged-ai' ) ) : ( 'commerce.order_changed' === $code
? array( 'status' => __( 'Order status', 'logged-ai' ), 'total' => __( 'Order total', 'logged-ai' ), 'currency' => __( 'Currency', 'logged-ai' ) )
: array( 'regular_price' => __( 'Regular price', 'logged-ai' ), 'sale_price' => __( 'Sale price', 'logged-ai' ), 'effective_price' => __( 'Active price', 'logged-ai' ), 'regular_price_set' => __( 'Regular price', 'logged-ai' ), 'sale_price_set' => __( 'Sale price', 'logged-ai' ), 'stock_status' => __( 'Stock status', 'logged-ai' ), 'manage_stock' => __( 'Stock tracking', 'logged-ai' ), 'status' => __( 'Product status', 'logged-ai' ), 'name' => __( 'Product name', 'logged-ai' ), 'description' => __( 'Product description', 'logged-ai' ) ) );
$out = array();
foreach ( $labels as $field => $label ) {
if ( ! isset( $fields[ $field ] ) || ( str_ends_with( $field, '_set' ) && isset( $fields[ substr( $field, 0, -4 ) ] ) ) ) {
continue;
}
$diff = $fields[ $field ];
if ( 'redacted' === ( $diff['availability'] ?? '' ) || true === ( $diff['redacted'] ?? false ) || in_array( $field, array( 'name', 'description' ), true ) ) {
/* translators: %s: name of the field whose value is not retained. */
$out[] = sprintf( __( '%s changed; value redacted.', 'logged-ai' ), $label );
continue;
}
$before = self::value( $diff, 'before', $field, $data['facts'], $fields );
$after = self::value( $diff, 'after', $field, $data['facts'], $fields );
/* translators: 1: field name, 2: retained earlier value, 3: retained later value. */
$out[] = sprintf( __( '%1$s: %2$s → %3$s', 'logged-ai' ), $label, $before, $after );
}
return $out;
}
/**
* Name a native setting from retained facts without retrieving its current value.
*
* Earlier records lacking these facts keep the generic event presentation.
* Confirmation describes the retained request, never its current state or delivery.
*
* @param array $event Authorized retained event.
* @return array Translated summaries, with no option values or arbitrary keys.
*/
private static function setting_summary( array $event ): array {
$data = $event['detail'] ?? null;
if ( 'site' !== ( $event['object_type'] ?? null ) || 'stored_change' !== ( $event['outcome'] ?? null )
|| ! is_array( $data ) || array_key_exists( 'extension', $data ) || 'core_allowlisted_v1' !== ( $data['collector'] ?? '' )
|| 'site' !== ( $data['event_scope'] ?? '' ) || ! in_array( $data['source'] ?? '', array( 'wordpress', 'synthetic_test' ), true )
|| ! is_array( $data['facts'] ?? null ) || count( $data['facts'] ) > 32 ) {
return array();
}
$labels = array(
'blogname' => __( 'Site title', 'logged-ai' ),
'blogdescription' => __( 'Tagline', 'logged-ai' ),
'users_can_register' => __( 'User registration', 'logged-ai' ),
'default_role' => __( 'Default user role', 'logged-ai' ),
'siteurl' => __( 'WordPress address', 'logged-ai' ),
'home' => __( 'Site address', 'logged-ai' ),
'permalink_structure' => __( 'Permalink structure', 'logged-ai' ),
'admin_email' => __( 'Administration email', 'logged-ai' ),
'new_admin_email' => __( 'Administration email change request', 'logged-ai' ),
'timezone_string' => __( 'Timezone', 'logged-ai' ),
'gmt_offset' => __( 'UTC offset', 'logged-ai' ),
'blog_public' => __( 'Search engine visibility', 'logged-ai' ),
'active_plugins' => __( 'Active plugins', 'logged-ai' ),
'stylesheet' => __( 'Theme stylesheet', 'logged-ai' ),
'template' => __( 'Theme template', 'logged-ai' ),
);
$option = $data['facts']['option'] ?? null;
$change = $data['facts']['change'] ?? null;
if ( ! is_string( $option ) || ! isset( $labels[ $option ] ) || ! in_array( $change, array( 'added', 'changed', 'deleted' ), true ) ) {
return array();
}
// These fixed labels describe only the retained mutation; never format values.
if ( 'added' === $change ) {
/* translators: %s: allowlisted site setting name. */
$out = array( sprintf( __( '%s added.', 'logged-ai' ), $labels[ $option ] ) );
} elseif ( 'deleted' === $change ) {
/* translators: %s: allowlisted site setting name. */
$out = array( sprintf( __( '%s deleted.', 'logged-ai' ), $labels[ $option ] ) );
} else {
/* translators: %s: allowlisted site setting name. */
$out = array( sprintf( __( '%s changed.', 'logged-ai' ), $labels[ $option ] ) );
}
if ( 'new_admin_email' === $option && 'deleted' !== $change && 'pending' === ( $data['facts']['confirmation'] ?? null ) ) {
$out[] = __( 'Recorded as pending confirmation; this does not establish that the active administration email changed.', 'logged-ai' );
}
// Explain omissions only when the retained descriptor explicitly proves them.
$diffs = $data['diffs'] ?? null;
$diff = is_array( $diffs ) && 1 === count( $diffs ) ? ( $diffs[0] ?? null ) : null;
if ( ! in_array( $option, array( 'users_can_register', 'default_role' ), true )
&& is_array( $diff ) && 'value' === ( $diff['field'] ?? null ) && 'scalar' === ( $diff['type'] ?? null )
&& 'redacted' === ( $diff['availability'] ?? null ) && true === ( $diff['redacted'] ?? null )
&& array_key_exists( 'before', $diff ) && null === $diff['before'] && array_key_exists( 'after', $diff ) && null === $diff['after'] ) {
$out[] = __( 'Previous and new values were not retained.', 'logged-ai' );
}
return $out;
}
/**
* Format a recorded value without floating-point conversion or current lookups.
*
* @param array $diff Retained difference.
* @param string $side Before or after.
* @param string $field Allowlisted field.
* @param array $facts Retained event facts.
* @param array $fields Retained differences indexed by name.
* @return string Readable historical value or explicit unavailable marker.
*/
private static function value( array $diff, string $side, string $field, array $facts, array $fields ): string {
$value = $diff[ $side ] ?? null;
if ( null === $value || ! self::side_available( $diff, $side ) ) {
return __( 'Unavailable', 'logged-ai' );
}
if ( str_ends_with( $field, '_set' ) || 'manage_stock' === $field ) {
return ! is_bool( $value ) ? __( 'Unavailable', 'logged-ai' ) : ( 'manage_stock' === $field ? ( $value ? __( 'On', 'logged-ai' ) : __( 'Off', 'logged-ai' ) ) : ( $value ? __( 'Set', 'logged-ai' ) : __( 'Not set', 'logged-ai' ) ) );
}
if ( in_array( $field, array( 'quantity', 'regular_price', 'sale_price', 'effective_price', 'total' ), true ) ) {
if ( ! is_string( $value ) || strlen( $value ) > 24 || ! preg_match( '/\A-?(?:0|[1-9][0-9]{0,14})(?:\.[0-9]{1,8})?\z/D', $value ) ) {
return __( 'Unavailable', 'logged-ai' );
}
$value = str_contains( $value, '.' ) ? rtrim( rtrim( $value, '0' ), '.' ) : $value;
if ( 'quantity' === $field ) {
return $value;
}
$currency = isset( $fields['currency'] ) ? ( ! self::side_available( $fields['currency'], $side ) ? null : ( $fields['currency'][ $side ] ?? null ) ) : ( $facts['currency'] ?? null );
$precision = $facts['precision'] ?? null;
if ( is_int( $precision ) && $precision >= 0 && $precision <= 8 ) {
$parts = explode( '.', $value, 2 );
if ( $precision > 0 ) {
$value = $parts[0] . '.' . str_pad( $parts[1] ?? '', $precision, '0' );
}
}
return is_string( $currency ) && preg_match( '/\A[A-Z]{3}\z/D', $currency ) ? $value . ' ' . $currency : $value . ' (' . __( 'currency unavailable', 'logged-ai' ) . ')';
}
if ( 'currency' === $field ) {
return is_string( $value ) && preg_match( '/\A[A-Z]{3}\z/D', $value ) ? $value : __( 'Unavailable', 'logged-ai' );
}
if ( ! is_string( $value ) || ! preg_match( '/\A[a-zA-Z0-9_-]{1,80}\z/D', $value ) ) {
return __( 'Unavailable', 'logged-ai' );
}
$labels = array( 'instock' => __( 'In stock', 'logged-ai' ), 'outofstock' => __( 'Out of stock', 'logged-ai' ), 'onbackorder' => __( 'On backorder', 'logged-ai' ), 'pending' => __( 'Pending', 'logged-ai' ), 'processing' => __( 'Processing', 'logged-ai' ), 'on-hold' => __( 'On hold', 'logged-ai' ), 'completed' => __( 'Completed', 'logged-ai' ), 'cancelled' => __( 'Cancelled', 'logged-ai' ), 'refunded' => __( 'Refunded', 'logged-ai' ), 'failed' => __( 'Failed', 'logged-ai' ), 'draft' => __( 'Draft', 'logged-ai' ), 'publish' => __( 'Published', 'logged-ai' ), 'private' => __( 'Private', 'logged-ai' ), 'trash' => __( 'Trash', 'logged-ai' ) );
return $labels[ $value ] ?? $value;
}
/**
* Honor the canonical stored availability independently for each side.
*
* @param array $diff Retained difference, already normalized by Store.
* @param string $side Before or after.
* @return bool Whether this side was retained and is not redacted.
*/
private static function side_available( array $diff, string $side ): bool {
$availability = $diff['availability'] ?? null;
return true !== ( $diff['redacted'] ?? false )
&& in_array( $availability, array( 'captured', 'before_unavailable', 'after_unavailable' ), true )
&& $side . '_unavailable' !== $availability;
}
}