true, 'display_mode' => 'all', 'display_targets' => array(), 'display_pages' => array(), 'display_paths' => '', 'show_desktop' => true, 'show_mobile' => true, 'audience' => 'everyone', 'hide_roles' => array(), 'load_strategy' => 'immediate', 'load_delay' => 5, ); } /** * Page types a rule can name. WooCommerce entries are only offered when * WooCommerce is active, but stay valid in storage either way. * * @return array key => label */ public static function targets() { $targets = array( 'front_page' => __( 'Home page', 'aivastark-widget' ), 'posts' => __( 'Blog posts', 'aivastark-widget' ), 'pages' => __( 'Pages', 'aivastark-widget' ), 'archives' => __( 'Archives & categories', 'aivastark-widget' ), 'search' => __( 'Search results', 'aivastark-widget' ), 'not_found' => __( '404 page', 'aivastark-widget' ), ); $woo = array( 'woo_shop' => __( 'Shop', 'aivastark-widget' ), 'woo_product' => __( 'Product pages', 'aivastark-widget' ), 'woo_cart' => __( 'Cart', 'aivastark-widget' ), 'woo_checkout' => __( 'Checkout', 'aivastark-widget' ), 'woo_account' => __( 'My account', 'aivastark-widget' ), ); return array_merge( $targets, $woo ); } /** True when WooCommerce is active, so its page types are worth offering. */ public static function has_woocommerce() { return class_exists( 'WooCommerce' ); } /** * Clean a submitted Display rules form into storable options. * * @param array $input Raw (unslashed) form values. * @param array $all_roles Valid role slugs (wp_roles()->roles keys). * @return array Sanitized display options, every key present. */ public static function sanitize( array $input, array $all_roles ) { $d = self::defaults(); $out = array(); $out['widget_enabled'] = ! empty( $input['widget_enabled'] ); $mode = isset( $input['display_mode'] ) ? (string) $input['display_mode'] : 'all'; $out['display_mode'] = in_array( $mode, array( 'all', 'include', 'exclude' ), true ) ? $mode : 'all'; $targets = isset( $input['display_targets'] ) ? (array) $input['display_targets'] : array(); $out['display_targets'] = array_values( array_intersect( array_keys( self::targets() ), array_map( 'strval', $targets ) ) ); $pages = isset( $input['display_pages'] ) ? (array) $input['display_pages'] : array(); $pages = array_values( array_unique( array_filter( array_map( 'absint', $pages ) ) ) ); $out['display_pages'] = array_slice( $pages, 0, self::MAX_PAGES ); $out['display_paths'] = self::sanitize_paths( isset( $input['display_paths'] ) ? (string) $input['display_paths'] : '' ); $out['show_desktop'] = ! empty( $input['show_desktop'] ); $out['show_mobile'] = ! empty( $input['show_mobile'] ); // "Neither device" is not a setting anyone means: that is what the // on/off switch is for. Fall back to both rather than hide silently. if ( ! $out['show_desktop'] && ! $out['show_mobile'] ) { $out['show_desktop'] = true; $out['show_mobile'] = true; } $audience = isset( $input['audience'] ) ? (string) $input['audience'] : 'everyone'; $out['audience'] = in_array( $audience, array( 'everyone', 'guests', 'members' ), true ) ? $audience : 'everyone'; $roles = isset( $input['hide_roles'] ) ? (array) $input['hide_roles'] : array(); $out['hide_roles'] = array_values( array_intersect( $all_roles, array_map( 'strval', $roles ) ) ); $strategy = isset( $input['load_strategy'] ) ? (string) $input['load_strategy'] : 'immediate'; $out['load_strategy'] = in_array( $strategy, array( 'immediate', 'delay', 'interaction' ), true ) ? $strategy : 'immediate'; $delay = isset( $input['load_delay'] ) ? (int) $input['load_delay'] : $d['load_delay']; $out['load_delay'] = max( 1, min( 60, $delay ) ); return $out; } /** * One URL path pattern per line: "/pricing", "/blog/*". Anything that is * not a site-relative path is dropped rather than guessed at. * * @param string $raw Textarea contents. * @return string Clean patterns, newline-separated. */ public static function sanitize_paths( $raw ) { $lines = preg_split( '/\r\n|\r|\n/', (string) $raw ); $out = array(); foreach ( $lines as $line ) { $line = trim( $line ); if ( '' === $line ) { continue; } // A full URL pasted in: keep only its path. if ( preg_match( '#^https?://#i', $line ) ) { $path = wp_parse_url( $line, PHP_URL_PATH ); $line = is_string( $path ) && '' !== $path ? $path : '/'; } if ( '/' !== substr( $line, 0, 1 ) ) { $line = '/' . $line; } // Paths and the * wildcard only; no spaces, quotes or markup. $line = preg_replace( '#[^A-Za-z0-9/_\-.*%~]#', '', $line ); if ( '' === $line || in_array( $line, $out, true ) ) { continue; } $out[] = $line; if ( count( $out ) >= self::MAX_PATHS ) { break; } } return implode( "\n", $out ); } /** * Should the widget load for this request? * * @param array $o Options (aivastark_widget_get_options()). * @param array $ctx Request description, see context(). Keys: * logged_in (bool), roles (string[]), object_id (int), * path (string, relative to the site's home), * is (array, keyed like targets()), * shortcode (bool) — the [aivastark] shortcode asked. * @return bool */ public static function decide( array $o, array $ctx ) { if ( empty( $o['widget_enabled'] ) ) { return false; } $logged_in = ! empty( $ctx['logged_in'] ); $audience = isset( $o['audience'] ) ? $o['audience'] : 'everyone'; if ( 'guests' === $audience && $logged_in ) { return false; } if ( 'members' === $audience && ! $logged_in ) { return false; } if ( $logged_in && ! empty( $o['hide_roles'] ) && ! empty( $ctx['roles'] ) ) { if ( array_intersect( (array) $o['hide_roles'], (array) $ctx['roles'] ) ) { return false; } } // A shortcode placed on a page IS a page rule, so page rules don't // overrule it. The switch and the audience still do. if ( ! empty( $ctx['shortcode'] ) ) { return true; } $mode = isset( $o['display_mode'] ) ? $o['display_mode'] : 'all'; if ( 'include' !== $mode && 'exclude' !== $mode ) { return true; } $match = self::matches( $o, $ctx ); return 'include' === $mode ? $match : ! $match; } /** * Does the request match any page rule (page type, chosen page, or path)? */ public static function matches( array $o, array $ctx ) { $is = isset( $ctx['is'] ) && is_array( $ctx['is'] ) ? $ctx['is'] : array(); foreach ( (array) ( isset( $o['display_targets'] ) ? $o['display_targets'] : array() ) as $target ) { if ( ! empty( $is[ $target ] ) ) { return true; } } $object_id = isset( $ctx['object_id'] ) ? (int) $ctx['object_id'] : 0; $pages = array_map( 'intval', (array) ( isset( $o['display_pages'] ) ? $o['display_pages'] : array() ) ); if ( $object_id > 0 && in_array( $object_id, $pages, true ) ) { return true; } $path = isset( $ctx['path'] ) ? (string) $ctx['path'] : ''; $raw = isset( $o['display_paths'] ) ? (string) $o['display_paths'] : ''; foreach ( preg_split( '/\r\n|\r|\n/', $raw ) as $pattern ) { if ( '' !== trim( $pattern ) && self::path_matches( trim( $pattern ), $path ) ) { return true; } } return false; } /** * "/blog/*" matches "/blog/hello"; "/pricing" matches "/pricing" and * "/pricing/". Case-insensitive, trailing slash ignored. */ public static function path_matches( $pattern, $path ) { $norm = function ( $p ) { $p = strtolower( (string) $p ); return '/' === $p ? $p : rtrim( $p, '/' ); }; $pattern = $norm( $pattern ); $path = $norm( '' === $path ? '/' : $path ); $regex = '#^' . str_replace( '\*', '.*', preg_quote( $pattern, '#' ) ) . '$#'; return 1 === preg_match( $regex, $path ); } /** * Describe the current front-end request for decide(). Only call after the * main query has run (wp_enqueue_scripts or later). * * @param bool $shortcode Whether the [aivastark] shortcode is asking. * @return array */ public static function context( $shortcode = false ) { $user = wp_get_current_user(); $roles = ( $user && $user->ID ) ? array_values( (array) $user->roles ) : array(); $is = array( 'front_page' => is_front_page(), 'posts' => is_singular( 'post' ), 'pages' => is_page(), 'archives' => is_archive() || is_home(), 'search' => is_search(), 'not_found' => is_404(), ); if ( self::has_woocommerce() ) { $is['woo_shop'] = function_exists( 'is_shop' ) && is_shop(); $is['woo_product'] = function_exists( 'is_product' ) && is_product(); $is['woo_cart'] = function_exists( 'is_cart' ) && is_cart(); $is['woo_checkout'] = function_exists( 'is_checkout' ) && is_checkout(); $is['woo_account'] = function_exists( 'is_account_page' ) && is_account_page(); } return array( 'logged_in' => is_user_logged_in(), 'roles' => $roles, 'object_id' => is_singular() ? (int) get_queried_object_id() : 0, 'path' => self::request_path(), 'is' => $is, 'shortcode' => (bool) $shortcode, ); } /** * The request path relative to the site's home, so "/pricing" means the * same thing on a site installed at example.com/shop/. */ public static function request_path() { $uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '/'; $path = wp_parse_url( $uri, PHP_URL_PATH ); $path = is_string( $path ) && '' !== $path ? $path : '/'; $home = wp_parse_url( home_url( '/' ), PHP_URL_PATH ); $home = is_string( $home ) ? rtrim( $home, '/' ) : ''; if ( '' !== $home && 0 === strpos( $path, $home ) ) { $path = substr( $path, strlen( $home ) ); } return '' === $path ? '/' : $path; } /** * The rules in a few words each, for the Home screen: * ["Every page", "All devices", "Everyone", "Loads right away"]. * * @param array $o Options. * @return string[] */ public static function summary( array $o ) { $o = array_merge( self::defaults(), $o ); $out = array(); if ( 'include' === $o['display_mode'] ) { $out[] = __( 'Chosen pages only', 'aivastark-widget' ); } elseif ( 'exclude' === $o['display_mode'] ) { $out[] = __( 'Every page but a few', 'aivastark-widget' ); } else { $out[] = __( 'Every page', 'aivastark-widget' ); } if ( $o['show_desktop'] && ! $o['show_mobile'] ) { $out[] = __( 'Desktop only', 'aivastark-widget' ); } elseif ( $o['show_mobile'] && ! $o['show_desktop'] ) { $out[] = __( 'Phones only', 'aivastark-widget' ); } else { $out[] = __( 'All devices', 'aivastark-widget' ); } if ( 'guests' === $o['audience'] ) { $out[] = __( 'Logged-out visitors', 'aivastark-widget' ); } elseif ( 'members' === $o['audience'] ) { $out[] = __( 'Logged-in users', 'aivastark-widget' ); } else { $out[] = __( 'Everyone', 'aivastark-widget' ); } if ( 'delay' === $o['load_strategy'] ) { /* translators: %d: seconds */ $out[] = sprintf( _n( 'Loads after %d second', 'Loads after %d seconds', (int) $o['load_delay'], 'aivastark-widget' ), (int) $o['load_delay'] ); } elseif ( 'interaction' === $o['load_strategy'] ) { $out[] = __( 'Loads on first interaction', 'aivastark-widget' ); } return $out; } /** * Loader settings when the plain