condition type. */ const FREE_ATTRIBUTES = array( 'user_state' => 'user_state', 'role' => 'user_role', 'page_type' => 'page_type', 'archive' => 'archive_page', 'device' => 'visitor_device', ); /** * Rule evaluator. * * @var Evaluator */ private $evaluator; /** * Saved rules. * * @var SavedRules */ private $saved_rules; /** * Constructor. * * @param Evaluator $evaluator Rule evaluator. * @param SavedRules $saved_rules Saved rules. */ public function __construct( Evaluator $evaluator, SavedRules $saved_rules ) { $this->evaluator = $evaluator; $this->saved_rules = $saved_rules; } /** * Registers hooks. * * @return void */ public function register(): void { add_action( 'init', array( $this, 'register_shortcodes' ) ); } /** * Registers the shortcode at each nesting level, and its "else" tag. A * stray "else" tag outside its shortcode prints nothing, and registering it * lets strip_shortcodes() remove it from excerpts. * * @return void */ public function register_shortcodes(): void { foreach ( self::TAGS as $tag ) { add_shortcode( $tag, array( $this, 'render' ) ); add_shortcode( $tag . self::ELSE_SUFFIX, '__return_empty_string' ); } } /** * The attributes that can be used as conditions, as attribute => condition type. * * @return array */ public function attributes(): array { /** * Filters the shortcode attributes that are conditions. Pro adds its own. * * Each attribute also has a `_not` version for "is not". The values are * the condition's values in the rule format, comma-separated. * * @param array $attributes Condition types keyed by attribute name. * Values that aren't a type ID are ignored. */ $attributes = apply_filters( 'yangra_conditional_shortcode_attributes', self::FREE_ATTRIBUTES ); $clean = array(); foreach ( (array) $attributes as $attribute => $type ) { $attribute = sanitize_key( (string) $attribute ); if ( '' !== $attribute && self::RULE_ATTRIBUTE !== $attribute && is_string( $type ) && '' !== $type ) { $clean[ $attribute ] = $type; } } return $clean; } /** * Renders the shortcode: the content when the conditions match, otherwise * the content after the "else" tag (or nothing). * * @param array|string $atts Attributes ('' when there are none). * @param string|null $content Content between the tags. * @param string $tag The tag used, e.g. "yangra_conditional_2". * @return string */ public function render( $atts, $content = null, $tag = 'yangra_conditional' ): string { $atts = is_array( $atts ) ? $atts : array(); $parts = $this->split( (string) $content, (string) $tag ); return do_shortcode( $this->matches( $atts ) ? $parts[0] : $parts[1] ); } /** * Splits the content at the tag's own "else" tag. * * @param string $content Content between the tags. * @param string $tag The tag used. * @return array{0: string, 1: string} The content to show when the conditions match, and when they don't. */ public function split( string $content, string $tag ): array { $parts = preg_split( '/\[' . preg_quote( $tag . self::ELSE_SUFFIX, '/' ) . '\s*\/?\]/', $content, 2 ); if ( ! is_array( $parts ) ) { $parts = array( $content ); } return array( $this->trim_breaks( $parts[0] ), $this->trim_breaks( $parts[1] ?? '' ) ); } /** * Removes the line break that follows a tag on its own line, and the one * before the next tag. The Shortcode block and the classic editor turn * those line breaks into `
` before shortcodes run. * * @param string $content Content between two tags. * @return string */ private function trim_breaks( string $content ): string { return (string) preg_replace( array( '/^\s*/i', '/\s*$/i' ), '', $content ); } /** * Whether the shortcode's conditions match the current request. * * @param array $atts Attributes, names lowercase (as WordPress parses them). * @return bool */ public function matches( array $atts ): bool { if ( isset( $atts[ self::RULE_ATTRIBUTE ] ) ) { $rule = $this->saved_rules->get( trim( (string) $atts[ self::RULE_ATTRIBUTE ] ) ); // A saved rule that no longer exists can't be evaluated: it never matches. return null !== $rule && $this->evaluator->is_visible( $rule ); } return $this->evaluator->is_visible( $this->rule( $atts ) ); } /** * The rule that the attributes describe: Show, when all of them match. * Attributes that aren't conditions are left out. * * @param array $atts Attributes. * @return array{version: int, enabled: bool, action: string, groups: array}>}>} */ public function rule( array $atts ): array { $attributes = $this->attributes(); $conditions = array(); foreach ( $atts as $name => $value ) { if ( ! is_string( $name ) ) { continue; // A value without a name, e.g. [yangra_conditional logged_in]. } $operator = 'is'; $base = $name; if ( ! isset( $attributes[ $base ] ) && self::NOT_SUFFIX === substr( $name, -strlen( self::NOT_SUFFIX ) ) ) { $operator = 'is_not'; $base = substr( $name, 0, -strlen( self::NOT_SUFFIX ) ); } if ( ! isset( $attributes[ $base ] ) ) { continue; } $conditions[] = array( 'id' => substr( md5( $name ), 0, 12 ), 'type' => $attributes[ $base ], 'operator' => $operator, 'values' => $this->values( (string) $value ), ); } return array( 'version' => 1, 'enabled' => true, 'action' => 'show', 'groups' => array() === $conditions ? array() : array( array( 'id' => 'shortcode000', 'conditions' => $conditions, ), ), ); } /** * Comma-separated values as a list, without spaces or empty values. * * @param string $value Attribute value. * @return array */ private function values( string $value ): array { $values = array(); foreach ( explode( ',', $value ) as $item ) { $item = trim( $item ); if ( '' !== $item && ! in_array( $item, $values, true ) ) { $values[] = $item; } } return $values; } }