{ label, group, value: none|ids|terms|post_types|text|roles|languages|range, side }. */ function runline_rules_types() { $types = array( 'front' => array( __( 'Home page', 'runline' ), 'page', 'none', 'server' ), 'posts_page' => array( __( 'Blog (latest posts) page', 'runline' ), 'page', 'none', 'server' ), 'singular' => array( __( 'Pages, posts or products', 'runline' ), 'page', 'ids', 'server' ), 'post_type' => array( __( 'All items of a post type', 'runline' ), 'page', 'post_types', 'server' ), 'term' => array( __( 'Categories and tags', 'runline' ), 'page', 'terms', 'server' ), 'search' => array( __( 'Search results', 'runline' ), 'page', 'none', 'server' ), '404' => array( __( 'Page not found (404)', 'runline' ), 'page', 'none', 'server' ), 'url' => array( __( 'Address (URL) contains', 'runline' ), 'page', 'text', 'server' ), 'logged_in' => array( __( 'Logged-in visitors', 'runline' ), 'visitor', 'none', 'server' ), 'guest' => array( __( 'Guests (not logged in)', 'runline' ), 'visitor', 'none', 'server' ), 'role' => array( __( 'User role', 'runline' ), 'visitor', 'roles', 'server' ), 'language' => array( __( 'Site language', 'runline' ), 'visitor', 'languages', 'server' ), 'utm' => array( __( 'Came with a UTM tag containing', 'runline' ), 'visitor', 'text', 'client' ), 'referrer' => array( __( 'Came from a site containing', 'runline' ), 'visitor', 'text', 'client' ), 'country' => array( __( 'Country of the visitor', 'runline' ), 'visitor', 'countries', 'client' ), ); if ( class_exists( 'WooCommerce' ) ) { $types += array( 'woo_shop' => array( __( 'Shop page', 'runline' ), 'woo', 'none', 'server' ), 'woo_products' => array( __( 'Product pages', 'runline' ), 'woo', 'none', 'server' ), 'woo_cart' => array( __( 'Cart', 'runline' ), 'woo', 'none', 'server' ), 'woo_checkout' => array( __( 'Checkout', 'runline' ), 'woo', 'none', 'server' ), 'woo_account' => array( __( 'My account', 'runline' ), 'woo', 'none', 'server' ), 'cart_total' => array( __( 'Cart total between', 'runline' ), 'woo', 'range', 'client' ), ); } $out = array(); foreach ( $types as $type => $info ) { $out[ $type ] = array( 'label' => $info[0], 'group' => $info[1], 'value' => $info[2], 'side' => $info[3], ); } return apply_filters( 'runline_rules_types', $out ); } /** * Groups of rule types: key => name. */ function runline_rules_groups() { return array( 'page' => __( 'Pages', 'runline' ), 'woo' => __( 'WooCommerce', 'runline' ), 'visitor' => __( 'Visitor', 'runline' ), ); } /** * Cleans one list of rules. * * @param mixed $rules Rules from the editor. * @return array */ function runline_rules_sanitize_list( $rules ) { $types = runline_rules_types(); $out = array(); if ( ! is_array( $rules ) ) { return $out; } foreach ( $rules as $rule ) { if ( ! is_array( $rule ) || ! isset( $rule['type'] ) ) { continue; } $type = (string) $rule['type']; // Types of plugins that are off now (e.g. WooCommerce) are kept as they are. $kind = isset( $types[ $type ] ) ? $types[ $type ]['value'] : runline_rules_guess_kind( $type ); $value = isset( $rule['value'] ) ? $rule['value'] : null; switch ( $kind ) { case 'ids': $value = array_values( array_unique( array_filter( array_map( 'absint', (array) $value ) ) ) ); break; case 'terms': $value = array_values( array_unique( array_filter( array_map( function ( $term ) { return preg_match( '/^[a-z0-9_-]+:\d+$/', (string) $term ) ? (string) $term : ''; }, (array) $value ) ) ) ); break; case 'post_types': case 'roles': case 'languages': $value = array_values( array_unique( array_filter( array_map( 'sanitize_key', (array) $value ) ) ) ); break; case 'text': $value = mb_substr( trim( sanitize_text_field( (string) $value ) ), 0, 200 ); break; case 'countries': preg_match_all( '/\b[A-Za-z]{2}\b/', (string) $value, $codes ); $value = implode( ', ', array_unique( array_map( 'strtoupper', $codes[0] ) ) ); break; case 'range': $value = array( 'min' => isset( $value['min'] ) && '' !== $value['min'] ? max( 0, (float) $value['min'] ) : '', 'max' => isset( $value['max'] ) && '' !== $value['max'] ? max( 0, (float) $value['max'] ) : '', ); break; default: $value = null; } if ( preg_match( '/^[a-z0-9_]{1,40}$/', $type ) ) { $out[] = array( 'type' => $type, 'value' => $value, ); } if ( count( $out ) >= 50 ) { break; } } return $out; } function runline_rules_guess_kind( $type ) { return 'cart_total' === $type ? 'range' : ( 0 === strpos( $type, 'woo_' ) ? 'none' : 'unknown' ); } /** * Country of the visitor from the server: Cloudflare (CF-IPCountry) or a server GeoIP variable. "" = unknown. */ function runline_rules_country() { foreach ( array( 'HTTP_CF_IPCOUNTRY', 'HTTP_X_COUNTRY_CODE', 'GEOIP_COUNTRY_CODE', 'HTTP_X_GEO_COUNTRY', 'HTTP_CLOUDFRONT_VIEWER_COUNTRY' ) as $name ) { if ( ! empty( $_SERVER[ $name ] ) ) { $code = strtoupper( sanitize_text_field( wp_unslash( $_SERVER[ $name ] ) ) ); if ( preg_match( '/^[A-Z]{2}$/', $code ) && 'XX' !== $code ) { return $code; } } } return (string) apply_filters( 'runline_rules_country', '' ); } add_action( 'rest_api_init', function () { // Asked by the browser (not cached), so cached pages still get the right country. register_rest_route( 'runline/v1', '/country', array( 'methods' => WP_REST_Server::READABLE, 'callback' => function () { $response = rest_ensure_response( array( 'country' => runline_rules_country() ) ); $response->header( 'Cache-Control', 'no-store, private' ); return $response; }, 'permission_callback' => '__return_true', ) ); } ); /** * Cleans { show: [], hide: [] }. */ function runline_rules_sanitize( $rules ) { $rules = is_array( $rules ) ? $rules : array(); return array( 'show' => runline_rules_sanitize_list( isset( $rules['show'] ) ? $rules['show'] : array() ), 'hide' => runline_rules_sanitize_list( isset( $rules['hide'] ) ? $rules['hide'] : array() ), ); } /** * Cleans the schedule: start / end "YYYY-MM-DDTHH:MM" in the site time zone, days 1–7 (Monday = 1), from / to "HH:MM". */ function runline_rules_sanitize_schedule( $schedule ) { $schedule = is_array( $schedule ) ? $schedule : array(); $date = function ( $key ) use ( $schedule ) { $value = isset( $schedule[ $key ] ) ? (string) $schedule[ $key ] : ''; return preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/', $value ) ? $value : ''; }; $time = function ( $key ) use ( $schedule ) { $value = isset( $schedule[ $key ] ) ? (string) $schedule[ $key ] : ''; return preg_match( '/^([01]\d|2[0-3]):[0-5]\d$/', $value ) ? $value : ''; }; $days = array_values( array_unique( array_filter( array_map( 'intval', isset( $schedule['days'] ) ? (array) $schedule['days'] : array() ), function ( $day ) { return $day >= 1 && $day <= 7; } ) ) ); sort( $days ); return array( 'on' => ! empty( $schedule['on'] ), 'start' => $date( 'start' ), 'end' => $date( 'end' ), 'days' => $days, 'from' => $time( 'from' ), 'to' => $time( 'to' ), ); } /** * Whether a server rule matches the current page; null = decided in the browser. * * @param array $rule Rule. * @return bool|null */ function runline_rules_match( $rule ) { $types = runline_rules_types(); $type = $rule['type']; if ( isset( $types[ $type ] ) && 'client' === $types[ $type ]['side'] ) { return null; } $value = $rule['value']; switch ( $type ) { case 'front': return is_front_page(); case 'posts_page': return is_home(); case 'singular': return is_singular() && in_array( get_queried_object_id(), (array) $value, true ); case 'post_type': foreach ( (array) $value as $post_type ) { if ( is_singular( $post_type ) || is_post_type_archive( $post_type ) ) { return true; } } return false; case 'term': foreach ( (array) $value as $term ) { list( $taxonomy, $term_id ) = explode( ':', $term ); $term_id = (int) $term_id; if ( ( is_category() || is_tag() || is_tax() ) && get_queried_object_id() === $term_id ) { return true; } if ( is_singular() && taxonomy_exists( $taxonomy ) && has_term( $term_id, $taxonomy, get_queried_object_id() ) ) { return true; } } return false; case 'search': return is_search(); case '404': return is_404(); case 'url': $uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; return '' !== $value && false !== stripos( rawurldecode( $uri ), $value ); case 'logged_in': return is_user_logged_in(); case 'guest': return ! is_user_logged_in(); case 'role': $user = wp_get_current_user(); return (bool) array_intersect( (array) $value, (array) $user->roles ); case 'language': return in_array( runline_rules_current_language(), (array) $value, true ); case 'woo_shop': return function_exists( 'is_shop' ) && is_shop(); case 'woo_products': return function_exists( 'is_product' ) && is_product(); case 'woo_cart': return function_exists( 'is_cart' ) && is_cart(); case 'woo_checkout': return function_exists( 'is_checkout' ) && is_checkout(); case 'woo_account': return function_exists( 'is_account_page' ) && is_account_page(); } // Unknown types (a plugin that is off): never match. return (bool) apply_filters( 'runline_rules_match', false, $rule ); } /** * Language of the current page: WPML / Polylang code, or the WordPress locale. */ function runline_rules_current_language() { if ( defined( 'ICL_LANGUAGE_CODE' ) ) { return sanitize_key( ICL_LANGUAGE_CODE ); } if ( function_exists( 'pll_current_language' ) ) { return sanitize_key( (string) pll_current_language( 'slug' ) ); } return sanitize_key( determine_locale() ); } /** * Languages to choose from: code => name. */ function runline_rules_languages() { $languages = array(); if ( has_filter( 'wpml_active_languages' ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- the WPML API. foreach ( (array) apply_filters( 'wpml_active_languages', null, array( 'skip_missing' => 0 ) ) as $code => $language ) { $languages[ sanitize_key( $code ) ] = isset( $language['native_name'] ) ? $language['native_name'] : $code; } } elseif ( function_exists( 'pll_languages_list' ) ) { $names = pll_languages_list( array( 'fields' => 'name' ) ); foreach ( pll_languages_list( array( 'fields' => 'slug' ) ) as $i => $slug ) { $languages[ sanitize_key( $slug ) ] = isset( $names[ $i ] ) ? $names[ $i ] : $slug; } } else { $locale = get_locale(); $languages[ sanitize_key( $locale ) ] = $locale; foreach ( get_available_languages() as $code ) { $languages[ sanitize_key( $code ) ] = $code; } } return $languages; } /** * Decides a bar for the current page. * * @param array $rules { show, hide } from runline_rules_sanitize(). * @param array $schedule From runline_rules_sanitize_schedule(). * @return array|false False = not on this page. Otherwise what the browser still checks: { show?, hide?, sched? } * (empty array = show). */ function runline_rules_decide( $rules, $schedule ) { $client = array(); // Hide rules win. $hide_client = array(); foreach ( $rules['hide'] as $rule ) { $match = runline_rules_match( $rule ); if ( true === $match ) { return false; } if ( null === $match ) { $hide_client[] = $rule; } } if ( $hide_client ) { $client['hide'] = $hide_client; } if ( $rules['show'] ) { $shown = false; $show_client = array(); foreach ( $rules['show'] as $rule ) { $match = runline_rules_match( $rule ); if ( true === $match ) { $shown = true; break; } if ( null === $match ) { $show_client[] = $rule; } } if ( ! $shown ) { if ( ! $show_client ) { return false; } $client['show'] = $show_client; } } if ( ! empty( $schedule['on'] ) ) { $sched = runline_rules_schedule_for_browser( $schedule ); if ( false === $sched ) { return false; } $client['sched'] = $sched; } return $client; } /** * Schedule for the browser: start / end as timestamps (ms), days, minutes of the day and the site time zone. * False when the end has passed (no need to send the bar at all). */ function runline_rules_schedule_for_browser( $schedule ) { $zone = wp_timezone(); $ms = function ( $local ) use ( $zone ) { if ( '' === $local ) { return 0; } $date = date_create_immutable( $local, $zone ); return $date ? $date->getTimestamp() * 1000 : 0; }; $min = function ( $time ) { if ( '' === $time ) { return -1; } list( $h, $m ) = array_map( 'intval', explode( ':', $time ) ); return $h * 60 + $m; }; $end = $ms( $schedule['end'] ); if ( $end && $end < time() * 1000 ) { return false; } $name = wp_timezone_string(); return array( 'start' => $ms( $schedule['start'] ), 'end' => $end, 'days' => $schedule['days'], 'from' => $min( $schedule['from'] ), 'to' => $min( $schedule['to'] ), // IANA name for Intl, or the current offset in minutes for "UTC+2" style settings. 'tz' => preg_match( '#^[A-Za-z]+/#', $name ) ? $name : '', 'off' => (int) ( $zone->getOffset( new DateTime( 'now', $zone ) ) / 60 ), ); } /** * Short text about the rules for lists ("Whole site", "3 conditions"). */ function runline_rules_summary( $rules, $schedule ) { $count = count( $rules['show'] ) + count( $rules['hide'] ); $parts = array(); if ( ! $count ) { $parts[] = __( 'Whole site', 'runline' ); } elseif ( 1 === count( $rules['show'] ) && ! $rules['hide'] && 'front' === $rules['show'][0]['type'] ) { $parts[] = __( 'Home page only', 'runline' ); } else { /* translators: %d: number of display conditions. */ $parts[] = sprintf( __( 'Conditions: %d', 'runline' ), $count ); } if ( ! empty( $schedule['on'] ) ) { $parts[] = __( 'on a schedule', 'runline' ); } return implode( ', ', $parts ); }