(float) $expected; case 'lt': return (float) $actual < (float) $expected; case 'set': return '' !== $actual; case 'empty': return '' === $actual; default: return $actual === $expected; } } /** Device class from the user agent (server-side, so hidden content is never sent). */ public static function device() { $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; if ( preg_match( '/iPad|Tablet|Nexus 7|Nexus 10|SM-T|Kindle|Silk|PlayBook|Android(?!.*Mobile)/i', $ua ) ) { return 'tablet'; } return preg_match( '/Mobile|iPhone|iPod|Android|BlackBerry|Opera Mini|IEMobile/i', $ua ) ? 'mobile' : 'desktop'; } /** Visit counter cookie as an integer (the default applies when it is not set). */ private static function visits( $default ) { return isset( $_COOKIE['nx_visits'] ) ? absint( wp_unslash( $_COOKIE['nx_visits'] ) ) : (int) $default; } public static function visitor_id() { return isset( $_COOKIE['nx_vid'] ) ? preg_replace( '/[^a-z0-9]/', '', sanitize_text_field( wp_unslash( $_COOKIE['nx_vid'] ) ) ) : ''; } /** * Visitor cookies for A/B and new/returning rules. Only set when a published layout uses those rules, * so sites that don't use them stay cookie-free and fully cacheable. */ public static function track() { if ( is_admin() || wp_doing_ajax() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || headers_sent() || ! get_option( 'akram_visitor_rules' ) ) { return; } $opts = array( 'path' => COOKIEPATH ?: '/', 'secure' => is_ssl(), 'httponly' => true, 'samesite' => 'Lax', ); if ( ! self::visitor_id() ) { $id = strtolower( wp_generate_password( 16, false ) ); $_COOKIE['nx_vid'] = $id; setcookie( 'nx_vid', $id, array( 'expires' => time() + YEAR_IN_SECONDS ) + $opts ); } if ( empty( $_COOKIE['nx_sess'] ) ) { $visits = self::visits( 0 ) + 1; $_COOKIE['nx_visits'] = (string) $visits; setcookie( 'nx_visits', (string) $visits, array( 'expires' => time() + YEAR_IN_SECONDS ) + $opts ); setcookie( 'nx_sess', '1', $opts ); $_COOKIE['nx_sess'] = '1'; } } /** Remember whether any saved layout uses visitor-tracking rules. */ public static function note_rules( array $cond ) { $uses = false; array_walk_recursive( $cond, static function ( $val, $key ) use ( &$uses ) { if ( 't' === $key && in_array( $val, array( 'ab', 'visits', 'returning' ), true ) ) { $uses = true; } } ); if ( $uses && ! get_option( 'akram_visitor_rules' ) ) { update_option( 'akram_visitor_rules', 1 ); } } public static function rule( array $r ) { $t = (string) ( $r['t'] ?? '' ); $op = (string) ( $r['op'] ?? 'is' ); $v = $r['v'] ?? ''; $k = (string) ( $r['k'] ?? '' ); $neg = 'not' === $op; switch ( $t ) { case 'login': return 'out' === $v ? ! is_user_logged_in() : is_user_logged_in(); case 'role': $roles = array_map( 'trim', is_array( $v ) ? $v : explode( ',', (string) $v ) ); $has = is_user_logged_in() && (bool) array_intersect( $roles, (array) wp_get_current_user()->roles ); return $neg ? ! $has : $has; case 'user': $ids = array_map( 'intval', is_array( $v ) ? $v : explode( ',', (string) $v ) ); $has = in_array( get_current_user_id(), $ids, true ); return $neg ? ! $has : $has; case 'date': $now = current_time( 'timestamp' ); $from = ! empty( $r['from'] ) ? strtotime( (string) $r['from'] ) : null; $to = ! empty( $r['to'] ) ? strtotime( (string) $r['to'] ) : null; return ( ! $from || $now >= $from ) && ( ! $to || $now <= $to ); case 'time': $now = current_time( 'H:i' ); $from = (string) ( $r['from'] ?? '00:00' ); $to = (string) ( $r['to'] ?? '23:59' ); return $from <= $to ? ( $now >= $from && $now <= $to ) : ( $now >= $from || $now <= $to ); case 'weekday': $days = array_map( 'strval', is_array( $v ) ? $v : explode( ',', (string) $v ) ); $has = in_array( (string) current_time( 'w' ), $days, true ); return $neg ? ! $has : $has; case 'param': return self::cmp( $op, isset( $_GET[ $k ] ) ? sanitize_text_field( wp_unslash( $_GET[ $k ] ) ) : '', $v ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only request value; it changes nothing and is sanitised. case 'url': return self::cmp( $op, isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '', $v ); case 'referrer': return self::cmp( $op, isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '', $v ); case 'cookie': return self::cmp( $op, isset( $_COOKIE[ $k ] ) ? sanitize_text_field( wp_unslash( $_COOKIE[ $k ] ) ) : '', $v ); case 'page': $ids = array_map( 'intval', is_array( $v ) ? $v : explode( ',', (string) $v ) ); $has = is_singular() && in_array( get_queried_object_id(), $ids, true ); return $neg ? ! $has : $has; case 'post_type': $has = is_singular( $v ) || is_post_type_archive( $v ); return $neg ? ! $has : $has; case 'front': return is_front_page() xor $neg; case 'in_term': $term = get_term( (int) $v ); $has = $term && ! is_wp_error( $term ) && has_term( (int) $v, $term->taxonomy, get_the_ID() ); return $neg ? ! $has : $has; case 'meta': return self::cmp( $op, get_post_meta( get_the_ID(), $k, true ), $v ); case 'user_meta': return self::cmp( $op, is_user_logged_in() ? get_user_meta( get_current_user_id(), $k, true ) : '', $v ); case 'acf': return function_exists( 'get_field' ) ? self::cmp( $op, get_field( $k, get_the_ID() ), $v ) : false; case 'browser': $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; $b = preg_match( '/Edg\//', $ua ) ? 'edge' : ( preg_match( '/OPR\//', $ua ) ? 'opera' : ( preg_match( '/Chrome\//', $ua ) ? 'chrome' : ( preg_match( '/Firefox\//', $ua ) ? 'firefox' : ( preg_match( '/Safari\//', $ua ) ? 'safari' : 'other' ) ) ) ); return self::cmp( $op, $b, $v ); case 'os': $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; $os = preg_match( '/iPhone|iPad|iPod/', $ua ) ? 'ios' : ( preg_match( '/Android/', $ua ) ? 'android' : ( preg_match( '/Windows/', $ua ) ? 'windows' : ( preg_match( '/Mac OS X/', $ua ) ? 'mac' : ( preg_match( '/Linux/', $ua ) ? 'linux' : 'other' ) ) ) ); return self::cmp( $op, $os, $v ); case 'language': return self::cmp( $op, determine_locale(), $v ); case 'woo_cart': if ( ! function_exists( 'WC' ) || ! WC()->cart ) { return false; } if ( 'total' === $k ) { return self::cmp( $op, WC()->cart->get_cart_contents_total(), $v ); } return self::cmp( $op, WC()->cart->is_empty() ? 'empty' : 'items', $v ); case 'device': return self::cmp( 'not' === $op ? 'not' : 'is', self::device(), $v ); case 'ab': $bucket = ( abs( crc32( self::visitor_id() . '|' . ( $k ?: 'test' ) ) ) % 100 ) < max( 1, min( 99, (int) ( $r['split'] ?? 50 ) ) ) ? 'a' : 'b'; return $bucket === ( 'b' === $v ? 'b' : 'a' ); case 'visits': return self::cmp( $op ?: 'gt', self::visits( 1 ), (int) $v ); case 'returning': $returning = self::visits( 1 ) > 1; return 'new' === $v ? ! $returning : $returning; case 'ip': $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; $list = array_filter( array_map( 'trim', explode( ',', (string) $v ) ) ); $has = false; foreach ( $list as $pfx ) { if ( $ip === $pfx || ( '*' === substr( $pfx, -1 ) && 0 === strpos( $ip, rtrim( $pfx, '*' ) ) ) ) { $has = true; } } return $neg ? ! $has : $has; case 'archive': $map = array( 'any' => is_archive() || is_home(), 'category' => is_category(), 'tag' => is_tag(), 'tax' => is_tax(), 'author' => is_author(), 'date' => is_date(), 'search' => is_search(), 'blog' => is_home(), 'shop' => function_exists( 'is_shop' ) && ( is_shop() || is_product_taxonomy() ), '404' => is_404(), ); $has = ! empty( $map[ (string) $v ] ); return $neg ? ! $has : $has; case 'author': $ids = array_map( 'intval', explode( ',', (string) $v ) ); $has = in_array( (int) get_post_field( 'post_author', get_the_ID() ), $ids, true ); return $neg ? ! $has : $has; case 'tag': $allowed = array( 'is_home', 'is_front_page', 'is_single', 'is_page', 'is_singular', 'is_archive', 'is_search', 'is_404', 'is_category', 'is_tag', 'is_attachment', 'is_paged', 'is_user_logged_in', 'wp_is_mobile', 'is_shop', 'is_product', 'is_product_category', 'is_cart', 'is_checkout', 'is_account_page', 'is_woocommerce', ); // Only a WordPress or WooCommerce conditional tag from the list above can run, with no arguments. $fn = (string) $v; $has = in_array( $fn, $allowed, true ) && function_exists( $fn ) && (bool) call_user_func( $fn ); return $neg ? ! $has : $has; case 'woo_bought': return function_exists( 'wc_customer_bought_product' ) && is_user_logged_in() && wc_customer_bought_product( '', get_current_user_id(), (int) $v ); } return (bool) apply_filters( 'akram_condition', true, $r ); } }