suppress_errors( true ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- a table name cannot be a placeholder; it is checked to be a bare identifier above. $rows = $wpdb->get_results( "SHOW COLUMNS FROM `{$table}`", ARRAY_A ); $wpdb->suppress_errors( $suppressed ); if ( ! is_array( $rows ) || array() === $rows ) { return null; } return array_values( array_map( static fn( array $row ): string => (string) ( $row['Field'] ?? '' ), $rows ) ); } /** * Whether a value is one the live logger would never store, judged by the * name it is stored under: a credential-shaped key (the live logger's own * EventLogger::is_secret_setting() backstop, so the two can never drift), * or an email address -- the live logger names accounts by login and * never copies an address into a summary (docs/event-coverage.md, "What * is never stored"). * * @param string $key A context key or metadata name, e.g. `new_value`, `user_email`, `NewEmail`. */ public static function is_withheld_key( string $key ): bool { $snake = strtolower( (string) preg_replace( '/(?<=[a-z0-9])(?=[A-Z])/', '_', $key ) ); $snake = ltrim( $snake, '_' ); return EventLogger::is_secret_setting( $snake ) || 1 === preg_match( '/(^|_)e?-?mail(_|$)|email/', $snake ); } /** * A source's `{placeholder}` template with its values filled in, each one * flattened, bounded and -- when its key is withheld -- replaced by * WITHHELD. A placeholder the source did not record is left as written, * which is what the source's own screen shows. * * @param string $template Text with `{key}` placeholders. * @param array $values Key => recorded value. * @param string[] $withheld Further keys withheld for this row (a secret option's values). */ public static function interpolate( string $template, array $values, array $withheld = array() ): string { $text = preg_replace_callback( '/\{([A-Za-z0-9_\->]+)\}/', static function ( array $m ) use ( $values, $withheld ): string { $key = $m[1]; if ( ! array_key_exists( $key, $values ) ) { return $m[0]; } if ( in_array( $key, $withheld, true ) || self::is_withheld_key( $key ) ) { return self::WITHHELD; } return self::value_text( $values[ $key ] ); }, $template ); return self::summary( is_string( $text ) ? $text : $template ); } /** * A stored value as another plugin wrote it: plain, or * `maybe_serialize()`d (WP Activity Log's metadata, Stream's meta before * 3.2). Serialised values are read by SerializedValue, never * unserialize(): no class of any plugin is ever instantiated from another * plugin's table, and a stdClass reads as an array. A serialised value it * will not read (another class, a reference, a malformed or truncated * string) comes back null and is dropped, not shown raw. * * @param string $value Stored value. * @return mixed */ public static function decode( string $value ) { if ( ! is_serialized( $value ) ) { return $value; } return SerializedValue::parse( trim( $value ) ); } /** * A recorded value as one bounded line of text. * * @param mixed $value Anything a source stored. */ public static function value_text( $value ): string { if ( is_bool( $value ) ) { $value = $value ? 'true' : 'false'; } elseif ( ! is_scalar( $value ) ) { $encoded = wp_json_encode( $value ); $value = false === $encoded ? '' : $encoded; } $value = trim( (string) preg_replace( '/\s+/', ' ', wp_strip_all_tags( (string) $value ) ) ); return EventLogger::bounded( $value, self::VALUE_LENGTH ); } /** * A summary as the log stores it: one line, no markup, bounded. * * @param string $text Rendered text. */ public static function summary( string $text ): string { $text = trim( (string) preg_replace( '/\s+/', ' ', wp_strip_all_tags( $text ) ) ); return EventLogger::bounded( $text, self::SUMMARY_LENGTH ); } /** * An IP address as a source recorded it, as a valid address or null. * * Simple History anonymises by default, storing `192.168.1.x`; that * reads here as `192.168.1.0`, exactly what this plugin's own truncation * would have stored. The importer then applies the site's IP setting on * top (Importer::stored_ip()), so an address a source kept in full is * truncated unless the site opted into full addresses. * * @param mixed $value Recorded address. */ public static function ip( $value ): ?string { if ( ! is_string( $value ) ) { return null; } $value = trim( $value ); if ( str_contains( $value, ',' ) ) { $value = trim( explode( ',', $value )[0] ); } if ( 1 === preg_match( '/^(\d{1,3}\.\d{1,3}\.\d{1,3})\.x$/i', $value, $m ) ) { $value = $m[1] . '.0'; } return false !== filter_var( $value, FILTER_VALIDATE_IP ) ? $value : null; } /** * A positive integer id, or null. * * @param mixed $value Recorded id. */ public static function id( $value ): ?int { if ( is_int( $value ) ) { return $value > 0 ? $value : null; } if ( is_string( $value ) && 1 === preg_match( '/^[0-9]{1,19}$/D', $value ) && (int) $value > 0 ) { return (int) $value; } return null; } /** * An email address the source recorded for its actor, or null. * * @param mixed $value Recorded address. */ public static function email( $value ): ?string { if ( ! is_string( $value ) ) { return null; } $value = trim( $value ); return '' !== $value && false !== filter_var( $value, FILTER_VALIDATE_EMAIL ) ? EventLogger::bounded( $value, 100 ) : null; } }