| Current File : /home/digitaw/www/wp-content/plugins/simple-history/inc/channels/class-alert-evaluator.php |
<?php
namespace Simple_History\Channels;
/**
* Alert evaluator using JsonLogic.
*
* Thin wrapper around the JsonLogic library for evaluating alert rules
* against event data. All rule logic (AND, OR, nested groups, negation,
* comparisons) is handled by JsonLogic.
*
* @since 5.0.0
* @see https://jsonlogic.com/
*/
class Alert_Evaluator {
/**
* Whether JsonLogic library is loaded.
*
* @var bool
*/
private static bool $library_loaded = false;
/**
* Load the JsonLogic library.
*
* @return bool True if library is available.
*/
private static function load_library(): bool {
if ( self::$library_loaded ) {
return true;
}
if ( class_exists( '\JWadhams\JsonLogic' ) ) {
self::$library_loaded = true;
return true;
}
$library_path = __DIR__ . '/../libraries/JsonLogic.php';
if ( file_exists( $library_path ) ) {
// phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable -- Path is safely constructed from __DIR__.
require_once $library_path;
self::$library_loaded = true;
return true;
}
return false;
}
/**
* Check if JsonLogic is available.
*
* @return bool True if JsonLogic can be used.
*/
public static function is_available(): bool {
return self::load_library();
}
/**
* Evaluate a JsonLogic rule against event data.
*
* @param array|object|null $rule The JsonLogic rule. Empty/null means all events pass.
* @param array $event_data The event data to evaluate against.
* @return bool True if rule matches (alert should trigger), false otherwise.
*/
public static function evaluate( $rule, array $event_data ): bool {
// No rule means all events pass.
if ( empty( $rule ) ) {
return true;
}
// Ensure library is loaded.
if ( ! self::load_library() ) {
// Library not available - fail open (allow event).
return true;
}
try {
// Convert object to array if needed.
if ( is_object( $rule ) ) {
$rule = json_decode( wp_json_encode( $rule ), true );
}
// Flatten event data for easier access in rules.
$data = self::prepare_event_data( $event_data );
// Apply JsonLogic rule.
$result = \JWadhams\JsonLogic::apply( $rule, $data );
// Ensure boolean return.
return (bool) $result;
} catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
// Fail open on error.
return true;
}
}
/**
* Prepare event data for JsonLogic evaluation.
*
* Flattens nested context data for easier access in rules.
* E.g., context._user_id becomes accessible as both context._user_id and user_id.
*
* @param array $event_data Raw event data.
* @return array Prepared data for JsonLogic.
*/
private static function prepare_event_data( array $event_data ): array {
$data = $event_data;
// Flatten context fields for convenience.
if ( isset( $event_data['context'] ) && is_array( $event_data['context'] ) ) {
foreach ( $event_data['context'] as $key => $value ) {
// Remove underscore prefix for cleaner access.
$clean_key = ltrim( $key, '_' );
if ( ! isset( $data[ $clean_key ] ) ) {
$data[ $clean_key ] = $value;
}
}
}
return $data;
}
/**
* Evaluate an alert configuration against event data.
*
* @param array $alert_config Alert configuration with 'rule' key containing JsonLogic.
* @param array $event_data Event data to evaluate.
* @return bool True if alert should trigger.
*/
public static function evaluate_alert( array $alert_config, array $event_data ): bool {
$rule = $alert_config['rule'] ?? $alert_config['jsonlogic_rule'] ?? null;
return self::evaluate( $rule, $event_data );
}
/**
* Validate a JsonLogic rule structure.
*
* Basic validation to catch obvious errors before storage.
*
* @param mixed $rule The rule to validate.
* @return array Array with 'valid' boolean and 'errors' array.
*/
public static function validate_rule( $rule ): array {
$errors = [];
if ( empty( $rule ) ) {
// Empty rule is valid (matches all).
return [
'valid' => true,
'errors' => [],
];
}
if ( ! is_array( $rule ) && ! is_object( $rule ) ) {
$errors[] = __( 'Rule must be a JsonLogic object.', 'simple-history' );
return [
'valid' => false,
'errors' => $errors,
];
}
// Convert to array for validation.
if ( is_object( $rule ) ) {
$rule = json_decode( wp_json_encode( $rule ), true );
}
// Try to apply with empty data to catch syntax errors.
if ( self::load_library() ) {
try {
\JWadhams\JsonLogic::apply( $rule, [] );
} catch ( \Exception $e ) {
$errors[] = sprintf(
/* translators: %s: Error message */
__( 'Invalid JsonLogic rule: %s', 'simple-history' ),
$e->getMessage()
);
}
}
return [
'valid' => empty( $errors ),
'errors' => $errors,
];
}
/**
* Get a human-readable description of a JsonLogic rule.
*
* @param array|null $rule The JsonLogic rule.
* @return string Human-readable description.
*/
public static function get_rule_description( $rule ): string {
if ( empty( $rule ) ) {
return __( 'All events (no filter)', 'simple-history' );
}
// For complex rules, just return a generic description.
// A full JsonLogic-to-text converter would be complex.
$operator = is_array( $rule ) ? array_key_first( $rule ) : null;
if ( $operator === 'and' ) {
$count = is_array( $rule['and'] ) ? count( $rule['and'] ) : 0;
return sprintf(
/* translators: %d: Number of conditions */
_n( '%d condition (all must match)', '%d conditions (all must match)', $count, 'simple-history' ),
$count
);
}
if ( $operator === 'or' ) {
$count = is_array( $rule['or'] ) ? count( $rule['or'] ) : 0;
return sprintf(
/* translators: %d: Number of conditions */
_n( '%d condition (any must match)', '%d conditions (any must match)', $count, 'simple-history' ),
$count
);
}
return __( 'Custom filter', 'simple-history' );
}
}