Source: objects/position-rule.php

<?php

namespace wpbuddy\rich_snippets;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
} // Exit if accessed directly

/**
 * Class Position_Rule
 *
 * @package wpbuddy\rich_snippets
 *
 * @since   2.0.0
 */
final class Position_Rule {

	/**
	 * The param.
	 *
	 * @since 2.0.0
	 *
	 * @var null|string
	 */
	public $param = null;


	/**
	 * The operator.
	 *
	 * @since 2.0.0
	 *
	 * @var null|string
	 */
	public $operator = null;


	/**
	 * The value.
	 *
	 * @since 2.0.0
	 *
	 * @var mixed
	 */
	public $value = null;


	/**
	 * Checks if the rule matches with the current page.
	 *
	 * @return bool
	 */
	public function match(): bool {

		/**
		 * Position Rule match filter (bail early).
		 *
		 * Allows to bail early for a single rule in a ruleset.
		 *
		 * @hook  wpbuddy/rich_snippets/rule/match/bail_early
		 *
		 * @param {null|bool} $bail_early If and how to bail early.
		 * @param {Position_Rule} $position_rule The rule object.
		 *
		 * @returns {null|bool} NULL if default behaviour should be turned ON. Otherwise true or false.
		 *
		 * @since 2.0.0
		 */
		$bail_early = apply_filters( 'wpbuddy/rich_snippets/rule/match/bail_early', null, $this );

		if ( is_bool( $bail_early ) ) {
			return $bail_early;
		}

		$ret = false;

		$method_name = sprintf( 'match_%s', $this->param );

		if ( method_exists( $this, $method_name ) ) {
			$ret = $this->{$method_name}();
		}

		/**
		 * Position Rule match filter by method name.
		 *
		 * Allows to filter the result of a rule match by method name.
		 *
		 * @hook  wpbuddy/rich_snippets/rule/{$method_name}
		 *
		 * @param {bool} $match_result The return value.
		 * @param {Position_Rule} $position_rule The current rule object.
		 *
		 * @returns {bool} The match result.
		 *
		 * @since 2.0.0
		 */
		$ret = boolval( apply_filters( 'wpbuddy/rich_snippets/rule/' . $method_name, $ret, $this ) );

		/**
		 * Position Rule match filter
		 *
		 * Allows to filter the match result of a Rule.
		 *
		 * @hook  wpbuddy/rich_snippets/rule/match
		 *
		 * @param {bool} $match_result The match result.
		 * @param {Position_Rule} $position_rule The current rule object.
		 *
		 * @returns {bool} The modified match result.
		 *
		 * @since 2.0.0
		 */
		return boolval( apply_filters( 'wpbuddy/rich_snippets/rule/match', $ret, $this ) );
	}


	/**
	 * Returns the current main query.
	 *
	 * @return \WP_Query
	 * @since 2.0.0
	 *
	 */
	private function get_query() {

		# are we in the main query?
		if ( is_main_query() ) {
			global $wp_query;

			return $wp_query;
		}

		global $wp_the_query;

		return $wp_the_query;
	}


	/**
	 * Compares a value with the internal value of the Rule.
	 *
	 * @param mixed $value
	 *
	 * @return bool
	 */
	private function compare( $value ): bool {

		# non-scalar types are not supported.
		if ( ! is_scalar( $value ) ) {
			return false;
		}

		switch ( $this->operator ) {
			case '!=':
				return $this->value !== $value;
				break;
			case '==':
			default:
				return $this->value === $value;
		}
	}


	/**
	 * Checks if the post ID matches.
	 *
	 * @return bool
	 * @since 2.0.0
	 *
	 */
	private function match_post(): bool {

		$query = $this->get_query();

		if ( ! $query->is_singular() ) {
			return false;
		}

		$current_post = $query->post;

		if ( ! is_a( $current_post, '\WP_Post' ) ) {
			return false;
		}

		# allow identical comparison
		$this->value = absint( $this->value );

		return $this->compare( $current_post->ID );
	}


	/**
	 * Checks if the current post matches a post type.
	 *
	 * @return bool
	 * @since 2.0.0
	 *
	 */
	private function match_post_type(): bool {

		$query = $this->get_query();

		if ( ! $query->is_singular() ) {
			return false;
		}

		$current_post = $query->post;

		if ( ! is_a( $current_post, '\WP_Post' ) ) {
			return false;
		}

		return $this->compare( $current_post->post_type );
	}


	/**
	 * Checks if the current post matches a specific template.
	 *
	 * @param string $post_type
	 *
	 * @return bool
	 * @since 2.0.0
	 *
	 */
	private function match_post_template( string $post_type = '' ): bool {

		$query = $this->get_query();

		if ( empty( $post_type ) ) {
			# extract the post type
			$post_type = strstr( $this->value, ':', true );
		}

		if ( ! $query->is_singular( $post_type ) ) {
			return false;
		}

		$file = get_page_template_slug( $query->post->ID );

		# if the default template is in use, get_page_template_slug() will return an empty string
		if ( empty( $file ) ) {
			$file = 'default';
		}

		return $this->compare(
			sprintf(
				'%s%s%s',
				$post_type,
				! empty( $post_type ) ? ':' : '',
				$file
			)
		);
	}


	/**
	 * Match a page template.
	 *
	 * @return bool
	 * @since 2.0.0
	 *
	 */
	private function match_page_template(): bool {

		$this->value = 'page:' . $this->value;

		return $this->match_post_template( 'page' );
	}


	/**
	 * Checks if the current post has a special post status.
	 *
	 * @return bool
	 * @since 2.0.0
	 *
	 */
	private function match_post_status(): bool {

		$query = $this->get_query();

		if ( ! $query->is_singular() ) {
			return false;
		}

		$current_post = $query->post;

		if ( ! is_a( $current_post, '\WP_Post' ) ) {
			return false;
		}

		return $this->compare( get_post_status( $current_post ) );

	}


	/**
	 * Checks if the current post has a special post format.
	 *
	 * @return bool
	 * @since 2.0.0
	 *
	 */
	private function match_post_format(): bool {

		$query = $this->get_query();

		if ( ! $query->is_singular() ) {
			return false;
		}

		$current_post = $query->post;

		if ( ! is_a( $current_post, '\WP_Post' ) ) {
			return false;
		}

		return $this->compare( get_post_format( $current_post ) );
	}


	/**
	 * Checks if the current page is a term.
	 *
	 * @param string $taxonomy
	 *
	 * @return bool
	 * @since 2.0.0
	 *
	 */
	private function match_term( string $taxonomy ) {

		$query = $this->get_query();

		# categories are only allowed in 'post' post types
		if ( $query->is_singular() ) {
			$post_type = get_post_type( $query->get_queried_object_id() );
			if ( ! is_object_in_taxonomy( $post_type, $taxonomy ) ) {
				return false;
			}

			$current_post = $query->post;

			if ( ! is_a( $current_post, '\WP_Post' ) ) {
				return false;
			}

			$comp = has_term( $this->value, $taxonomy, $current_post ) ? "{$taxonomy}:{$this->value}" : '';

			$this->value = "{$taxonomy}:{$this->value}";

			return $this->compare( $comp );

		} elseif ( $query->is_category() || $query->is_tag() || $query->is_tax() ) {
			return $this->compare( $query->queried_object_id );
		}

		return false;
	}


	/**
	 * Checks if the post has a specific category.
	 *
	 * @return bool
	 * @since 2.0.0
	 *
	 */
	private function match_post_category(): bool {

		return $this->match_term( 'category' );

	}


	/**
	 * Checks if a current post has a specific taxonomy.
	 *
	 * @return bool
	 * @since 2.0.0
	 *
	 */
	private function match_post_taxonomy(): bool {

		# extract the post type
		$taxonomy = strstr( $this->value, ':', true );

		# remove taxonomy from the value and make to INT
		$this->value = absint( str_replace( $taxonomy . ':', '', $this->value ) );

		return $this->match_term( $taxonomy );
	}


	/**
	 * Compares a page type (and any special pages).
	 *
	 * @return bool
	 * @since 2.0.0
	 *
	 */
	private function match_page_type(): bool {

		$query = $this->get_query();

		$current_page = '';

		if ( 'all' === $this->value ) {
			$current_page = $this->value;
		} elseif ( 'top_level' === $this->value ) {
			# check if current post is a top level post

			$current_post = $query->post;

			if ( ! is_a( $current_post, '\WP_Post' ) ) {
				return false;
			}

			$current_page = empty( $current_post->post_parent ) ? 'top_level' : 'sub_level';

		} elseif ( 'parent' === $this->value ) {
			# check if current post has children

			$current_post = $query->post;

			if ( ! is_a( $current_post, '\WP_Post' ) ) {
				return false;
			}

			$children = get_posts( array(
				'post_type'      => $current_post->post_type,
				'post_parent'    => $current_post->ID,
				'posts_per_page' => 1,
				'fields'         => 'ids',
			) );

			$current_page = count( $children ) > 0 ? 'parent' : 'no_parent';
		} elseif ( 'child' === $this->value ) {
			# check if the current post has a parent

			$current_post = $query->post;

			if ( ! is_a( $current_post, '\WP_Post' ) ) {
				return false;
			}

			$current_page = ! empty( $current_post->post_parent ) ? 'child' : 'no_child';
		} elseif ( $query->is_front_page() ) {
			$current_page = 'front_page';
		} elseif ( $query->is_home() ) {
			$current_page = 'posts_page';
		} elseif ( $query->is_archive() && 'archive' === $this->value ) {
			$current_page = 'archive';
		} elseif ( $query->is_category() ) {
			# Note: is_tax() does not return TRUE for category archive pages.
			# @see https://codex.wordpress.org/Function_Reference/is_tax
			$current_page = 'archive_category';
		} elseif ( $query->is_tag() ) {
			# Note: is_tax() does not return TRUE for tag archive pages.
			# @see https://codex.wordpress.org/Function_Reference/is_tax
			$current_page = 'archive_post_tag';
		} elseif ( $query->is_tax() ) {
			$taxonomy = $query->get_queried_object();
			if ( $taxonomy instanceof \WP_Term ) {
				$current_page = 'archive_' . $taxonomy->taxonomy;
			}
		} elseif ( $query->is_search() ) {
			$current_page = 'search';
		}

		return $this->compare( $current_page );
	}


	/**
	 * Checks if the current page has a particular parent page.
	 *
	 * @return bool
	 * @since 2.0.0
	 *
	 */
	private function match_page_parent(): bool {

		$query = $this->get_query();

		if ( ! $query->is_singular() ) {
			return false;
		}

		$current_post = $query->post;

		if ( ! is_a( $current_post, '\WP_Post' ) ) {
			return false;
		}

		$this->value = absint( $this->value );

		return $this->compare( $current_post->post_parent );
	}


	/**
	 * Check if any of the terms is a child of a given term.
	 *
	 * @return bool
	 * @since 2.10.0
	 *
	 */
	private function match_child_terms(): bool {
		# extract the taxonomy
		$taxonomy = strstr( $this->value, ':', true );

		# remove taxonomy from the value and make to INT
		$term_id     = absint( str_replace( $taxonomy . ':', '', $this->value ) );
		$this->value = $term_id;

		$query = $this->get_query();

		if ( ! $query->is_singular() ) {
			return false;
		}

		$current_post = $query->post;

		if ( ! is_a( $current_post, '\WP_Post' ) ) {
			return false;
		}

		$post_terms = get_terms( [ 'object_ids' => $current_post->ID, 'taxonomy' => $taxonomy ] );

		if ( ! is_array( $post_terms ) ) {
			return false;
		}

		$i = 0;

		foreach ( $post_terms as $post_term ) {
			if ( empty( $post_term->parent ) ) {
				continue;
			}

			if ( term_is_ancestor_of( $this->value, $post_term->term_id, $taxonomy ) ) {
				$i ++;

				if ( '==' === $this->operator ) {
					return true;
				}
			}

		}

		if ( '!=' === $this->operator && $i <= 0 ) {
			return true;
		}

		return false;
	}
}