Source: classes/controller/frontend.php

<?php

namespace wpbuddy\rich_snippets;

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


/**
 * Class Admin.
 *
 * Starts up all the frontend things.
 *
 * @package wpbuddy\rich_snippets
 *
 * @since   2.0.0
 */
class Frontend_Controller {

	/**
	 * If debug mode is on|off.
	 *
	 * @since 2.0.0
	 *
	 * @var bool
	 */
	protected $debug = false;


	/**
	 * If frontend caching is active.
	 *
	 * @since 2.11.0
	 *
	 * @var bool
	 */
	protected $caching = true;


	/**
	 * The caching time in hours.
	 *
	 * @since 2.11.0
	 *
	 * @var int
	 */
	protected $caching_time = 6;


	/**
	 * Current post ID.
	 *
	 * @since 2.0.0
	 *
	 * @var int
	 */
	protected $current_post_id = 0;


	/**
	 * If Values_Model has been initialized.
	 *
	 * @since 2.2.5
	 *
	 * @var bool
	 */
	protected $values_model_initialized = false;


	/**
	 * Magic method for setting up the class.
	 *
	 * @since 2.0.0
	 */
	public function __construct() {

		$this->debug        = defined( 'WPB_RS_DEBUG' ) && WPB_RS_DEBUG;
		$this->caching      = (bool) get_option( 'wpb_rs/setting/frontend_caching', false ) && ! $this->debug;
		$this->caching_time = intval( get_option( 'wpb_rs/setting/frontend_caching_time', 6 ) );

		if ( false !== stripos( get_class( $this ), 'rich_snippets\\pro\\' ) ) {
			if ( ! get_option( 'wpb_rs/verified', false ) ) {
				return;
			}
		} else {
			if ( ! get_option( 'wpb_rs/active', false ) ) {
				return;
			}
		}

		add_action( 'wp', array( $this, 'set_object_vars' ), 10, 1 );

		if ( (bool) get_option( 'wpb_rs/setting/snippets_in_footer', true ) ) {
			add_action( 'wp_footer', array( $this, 'print_snippets' ) );
			add_action( 'amp_post_template_head', array( $this, 'print_snippets' ) );
		} else {
			add_action( 'wp_head', array( $this, 'print_snippets' ) );
			add_action( 'amp_post_template_footer', array( $this, 'print_snippets' ) );
		}

		add_filter( 'post_class', array( $this, 'remove_hentry' ) );

		add_filter( 'comment_class', array( $this, 'remove_vcard' ) );

		add_action( 'woocommerce_init', array( $this, 'remove_wc_structured_data' ) );

		add_action( 'init', array( $this, 'remove_yoast_structured_data' ) );

		add_action( 'admin_bar_menu', array( $this, 'check_page_menu' ), 150 );

		/**
		 * Frontend init hook.
		 *
		 * Allow other plugins to perform any actions.
		 *
		 * @hook  wpbuddy/rich_snippets/frontend/init
		 *
		 * @param {Frontend_Controller} $frontend
		 *
		 * @since 2.0.0
		 *
		 */
		do_action_ref_array( 'wpbuddy/rich_snippets/frontend/init', array( &$this ) );
	}


	/**
	 * Set objects vars of this class.
	 *
	 * @param \WP $wp
	 *
	 * @since 2.1.1
	 *
	 */
	public function set_object_vars( $wp ) {

		/**
		 * Fetch the current post ID.
		 */
		$this->current_post_id = Helper_Model::instance()->get_current_post_id();
	}

	/**
	 * Prints the schema.
	 *
	 * @since 2.0.0
	 */
	public function print_snippets() {

		if ( (bool) get_post_meta( $this->current_post_id, 'snip_hide_schemas', true ) ) {
			printf(
				'<!--%s-->',
				__( 'SNIP INFO: All schemas are disabled on this page. Uncheck the "Hide all schemas on this post" checkbox on this post to resolve the issue.', 'rich-snippets-schema' )
			);

			return;
		}

		/**
		 * Check for global snippets.
		 */
		if ( $this->have_global_snippets() ) {

			# Run through all snippets.
			foreach ( $this->get_global_snippet_post_ids() as $global_snippet_post_id ) {
				if ( ! $this->is_global_snippet_active( $global_snippet_post_id ) ) {
					continue;
				}

				$this->print_rich_snippets( $global_snippet_post_id );
			}
		}

		/**
		 * Check for snippets attached to a single post.
		 */
		if ( $this->singular_has_snippet( $this->current_post_id ) ) {
			$this->print_rich_snippets( $this->current_post_id );
		}
	}


	/**
	 * Prints rich snippets.
	 *
	 * @param int $post_id The post ID where snippets were defined.
	 *
	 * @since 2.0.0
	 */
	public function print_rich_snippets( int $post_id ) {

		echo '<!--';
		_e(
			'Code generated by SNIP (Structured Data Plugin) for WordPress. See rich-snippets.io for more information.',
			'rich-snippets-schema'
		);
		printf( __( 'Post ID is %d.', 'rich-snippets-schema' ), $post_id );
		echo '-->';

		if ( $this->caching ) {
			$cache = get_transient( Cache_Model::get_cache_key() );
		} else {
			$cache = [];
		}

		if ( is_array( $cache )
		     && isset( $cache[ $post_id ] )
		     && isset( $cache[ $post_id ]['snippets'] )
		     && is_array( $cache[ $post_id ]['snippets'] )
		) {
			echo implode( '', $cache[ $post_id ]['snippets'] );

			return;
		}

		if ( ! $this->values_model_initialized ) {
			# Init value hooks
			$this->init_values_model();
		}

		$rich_snippets = Snippets_Model::get_snippets( $post_id );

		$cache = ! is_array( $cache ) ? [] : $cache;

		foreach ( $rich_snippets as $snippet_uid => $rich_snippet ) {
			$rich_snippet->prepare_for_output( array(
				'current_post_id' => $this->current_post_id,
				'snippet_post_id' => $post_id,
				'input'           => 'snippet_' . $post_id . '_',
				'queried_object'  => get_queried_object(),
			) );

			if ( ! $rich_snippet->has_properties() ) {
				continue;
			}

			$output = sprintf(
				'<script data-snippet_id="%s" type="application/ld+json">%s</script>',
				esc_attr( $snippet_uid ),
				$rich_snippet->__toString()
			);

			$cache[ $post_id ]['snippets'][ $snippet_uid ] = $output;

			echo $output;
		}

		if ( $this->caching ) {
			set_transient( Cache_Model::get_cache_key(), $cache, $this->caching_time * HOUR_IN_SECONDS );
		}
	}


	/**
	 * Checks if a singular post has snippets.
	 *
	 * @param int $post_id
	 *
	 * @return bool
	 * @since 2.0.0
	 *
	 */
	public function singular_has_snippet( $post_id ): bool {

		return count( Snippets_Model::get_snippets( $post_id ) ) > 0;
	}


	/**
	 * Removes "hentry" class from post classes.
	 *
	 * @param array $classes
	 *
	 * @return array
	 * @since 2.0.0
	 *
	 */
	public function remove_hentry( $classes ) {

		$k = array_search( 'hentry', $classes );
		if ( false !== $k && (bool) get_option( 'wpb_rs/setting/remove_hentry', true ) ) {
			unset( $classes[ $k ] );
		}

		return $classes;
	}


	/**
	 * Removes vcard CSS classes from comments.
	 *
	 * @param array $classes
	 *
	 * @return array
	 * @since 2.0.0
	 *
	 */
	public function remove_vcard( $classes ) {

		$k = array_search( 'vcard', $classes );
		if ( false !== $k && (bool) get_option( 'wpb_rs/settings/remove_vcard', true ) ) {
			unset( $classes[ $k ] );
		}

		return $classes;
	}


	/**
	 * Maybe deactivates WooCommerce structured data.
	 *
	 * @since 2.0.0
	 */
	public function remove_wc_structured_data() {

		$deactivate = (bool) get_option( 'wpb_rs/setting/remove_wc_schema', false );

		if ( $deactivate ) {
			remove_action( 'wp_footer', array( WC()->structured_data, 'output_structured_data' ), 10 );
		}
	}


	/**
	 * Remove Structured Data generated by Yoast SEO.
	 *
	 * @since 2.11.0
	 */
	public function remove_yoast_structured_data() {
		$deactivate = (bool) get_option( 'wpb_rs/setting/remove_yoast_schema', false );

		if ( $deactivate ) {
			add_filter( 'wpseo_json_ld_output', '__return_false' );
		}
	}


	/**
	 * Initializes values model.
	 *
	 * @since 2.19.0
	 */
	public function init_values_model() {
		if ( ! $this->values_model_initialized ) {
			new Values_Model();
		}

		$this->values_model_initialized = true;
	}


	/**
	 * Add new admin bar menu option.
	 *
	 * @param \WP_Admin_Bar $wp_admin_bar
	 *
	 * @since 2.1.9.0
	 */
	public function check_page_menu( $wp_admin_bar ) {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}
		$wp_admin_bar->add_menu( array(
			'id'     => 'snip-test-link',
			'parent' => null,
			'group'  => null,
			'title'  => __( 'Test Structured Data', 'rich-snippets-schema' ),
			'href'   => add_query_arg( [
				'url'        => ( is_ssl() ? 'https' : 'http' ) . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'],
				'user_agent' => 2,
			], 'https://search.google.com/test/rich-results' ),
			'meta'   => [
				'title'  => __( 'Test the current pages Structured Data', 'rich-snippets-schema' ),
				'target' => '_blank'
			]
		) );
	}

	/**
	 * Checks if the current page has any global snippets.
	 *
	 * @return bool
	 * @since 2.0.0
	 *
	 */
	public function have_global_snippets(): bool {

		return count( $this->get_global_snippet_post_ids() ) > 0;
	}


	/**
	 * Returns the global schema post IDs.
	 *
	 * @return int[] Array with post Ids.
	 * @since 2.0.0
	 *
	 */
	public function get_global_snippet_post_ids(): array {

		if ( $this->caching ) {
			$cache = get_transient( 'wpb_rs/cache/global_snippets_ids' );

			if ( is_array( $cache ) ) {
				return $cache;
			}
		}

		$query = new \WP_Query( array(
			'post_type'      => 'wpb-rs-global',
			'fields'         => 'ids',
			'post_status'    => 'publish',
			'posts_per_page' => - 1,
		) );

		if ( ! $query->have_posts() ) {
			if ( $this->caching ) {
				set_transient( 'wpb_rs/cache/global_snippets_ids', array(), $this->caching_time * HOUR_IN_SECONDS );
			}

			return array();
		}

		$ids = $query->get_posts();

		if ( $this->caching ) {
			set_transient( 'wpb_rs/cache/global_snippets_ids', $ids, $this->caching_time * HOUR_IN_SECONDS );
		}

		return $ids;
	}


	/**
	 * Checks the rules.
	 *
	 * @param int $id
	 *
	 * @return bool
	 * @since 2.0.0
	 * @since 2.8.0 renamed from is_schema_active()
	 *
	 */
	public function is_global_snippet_active( int $id ): bool {

		$cache_key = Cache_Model::get_cache_key();

		if ( $this->caching ) {
			$cache = get_transient( $cache_key );
		}

		if ( isset( $cache )
		     && is_array( $cache )
		     && isset( $cache[ $id ] )
		     && isset( $cache[ $id ]['match_result'] )
		) {

			/**
			 * Cached match result filter.
			 *
			 * Allows to change the cached value of a matching rule.
			 *
			 * @hook  wpb_rs/cache/rule_{$id}
			 *
			 * @param {bool} $match_result The match result from the cache.
			 * @param {bool} $default_result The default result.
			 *
			 * @since 2.0.0
			 *
			 * @returns {bool}
			 */
			$value = apply_filters( 'wpb_rs/cache/rule_' . $id, $cache[ $id ]['match_result'], true );

			return boolval( $value );
		}

		$match_result = \wpbuddy\rich_snippets\Rules_Model::get_ruleset( $id )->match();

		$cache[ $id ]['match_result'] = $match_result;

		if ( $this->caching ) {
			set_transient( $cache_key, $cache, $this->caching_time * HOUR_IN_SECONDS );
		}

		return boolval( apply_filters( 'wpb_rs/cache/rule_' . $id, $match_result, false ) );
	}

}