' . esc_html__( 'Recorded changes', 'logged-ai' ) . ''; 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; } }