Source: includes/frontend/class-templates.php

<?php
namespace jb\frontend;

use WP_Block_Template;
use WP_Filesystem_Base;
use WP_Post;
use function WP_Filesystem;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'jb\frontend\Templates' ) ) {

	/**
	 * Class Templates
	 *
	 * @package jb\frontend
	 */
	class Templates {

		/**
		 * @var bool
		 */
		public $template_replaced = false;

		/**
		 * Templates constructor.
		 */
		public function __construct() {
			// handle WordPress native post template and add before and after post content
			add_action( 'wp_loaded', array( &$this, 'change_wp_native_post_content' ) );

			/**
			 * Handlers for single job template
			 */
			add_filter( 'single_template', array( &$this, 'cpt_template' ) );
			add_filter( 'archive_template', array( &$this, 'cpt_archive_template' ), 100, 2 );
			add_action( 'wp_footer', array( $this, 'output_structured_data' ) );
		}

		/**
		 * Change WP native job post content
		 *
		 * @since 1.0
		 */
		public function change_wp_native_post_content() {
			$template = JB()->options()->get( 'job-template' );
			if ( empty( $template ) ) {
				// add scripts and styles, but later because wp_loaded is earlier
				add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_single_job' ), 9999 );

				add_filter( 'the_content', array( &$this, 'before_job_content' ), 99999 );
				add_filter( 'the_content', array( &$this, 'after_job_content' ), 99999 );
			}
		}

		/**
		 * Enqueue single job assets
		 *
		 * @since 1.0
		 */
		public function enqueue_single_job() {
			wp_enqueue_script( 'jb-single-job' );
			wp_enqueue_style( 'jb-job' );
		}

		/**
		 * Add JB block before the job post content
		 *
		 * @param string $content
		 *
		 * @return string
		 *
		 * @since 1.0
		 */
		public function before_job_content( $content ) {
			global $post;

			if ( $post && 'jb-job' === $post->post_type && is_singular( 'jb-job' ) && is_main_query() && ! post_password_required() ) {
				ob_start();
				?>

				<div class="jb">
					<?php
					/**
					 * Fires before displaying job data on front.
					 *
					 * Note: When the "Job Template" setting = "WordPress native post template".
					 *
					 * @since 1.1.0
					 * @hook jb_before_job_content
					 *
					 * @param {int} $post_id Post ID.
					 */
					do_action( 'jb_before_job_content', $post->ID );

					if ( JB()->options()->get( 'job-breadcrumbs' ) ) {
						JB()->get_template_part( 'job/breadcrumbs', array( 'job_id' => $post->ID ) );
					}

					JB()->get_template_part( 'job/notices', array( 'job_id' => $post->ID ) );
					JB()->get_template_part( 'job/info', array( 'job_id' => $post->ID ) );
					JB()->get_template_part( 'job/company', array( 'job_id' => $post->ID ) );
					?>
				</div>

				<?php
				$content = ob_get_clean() . $content;
			}

			return $content;
		}

		/**
		 * Add JB block after the job post content
		 *
		 * @param string $content
		 *
		 * @return string
		 *
		 * @since 1.0
		 */
		public function after_job_content( $content ) {
			global $post;

			if ( $post && 'jb-job' === $post->post_type && is_singular( 'jb-job' ) && is_main_query() && ! post_password_required() ) {
				ob_start();
				?>

				<div class="jb">
					<?php
					JB()->get_template_part(
						'job/footer',
						array(
							'job_id' => $post->ID,
							'title'  => get_the_title( $post->ID ),
						)
					);

					/**
					 * Fires after displaying job data on front.
					 *
					 * Note: When the "Job Template" setting = "WordPress native post template".
					 *
					 * @since 1.1.0
					 * @hook jb_after_job_content
					 *
					 * @param {int} $post_id Post ID.
					 */
					do_action( 'jb_after_job_content', $post->ID );
					?>
				</div>

				<?php
				$content .= ob_get_clean();
			}

			return $content;
		}

		/**
		 * Check if the Job has custom template, or load by default page template
		 *
		 * @param string $single_template
		 *
		 * @return string
		 *
		 * @since 1.0
		 */
		public function cpt_template( $single_template ) {
			global $post;

			$template = JB()->options()->get( 'job-template' );
			if ( empty( $template ) ) {
				return $single_template;
			}

			if ( 'jb-job' === $post->post_type ) {
				// check if block theme and change templale
				if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) {
					add_filter( 'get_block_templates', array( $this, 'jb_change_single_job_block_templates' ), 10, 1 );
				}
				add_filter( 'twentytwenty_disallowed_post_types_for_meta_output', array( &$this, 'add_cpt_meta' ), 10, 1 );
				add_filter( 'template_include', array( &$this, 'cpt_template_include' ), 10, 1 );
				add_filter( 'has_post_thumbnail', array( &$this, 'hide_post_thumbnail' ), 10, 2 );
			}

			return $single_template;
		}

		/**
		 * Change archive template
		 *
		 * @param string $template
		 * @param string $type
		 *
		 * @return string
		 *
		 * @since 1.0
		 */
		public function cpt_archive_template( $template, $type ) {
			if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) {
				if ( isset( get_queried_object()->taxonomy ) && ( 'jb-job-type' === get_queried_object()->taxonomy || 'jb-job-category' === get_queried_object()->taxonomy ) ) {
					if ( 'archive' === $type && 'default' === JB()->options()->get( 'job-archive-template' ) ) {
						add_filter( 'render_block_data', array( $this, 'jb_change_archive_template' ), 10, 2 );
					}
				}
			} elseif ( isset( get_queried_object()->taxonomy ) && ( 'jb-job-type' === get_queried_object()->taxonomy || 'jb-job-category' === get_queried_object()->taxonomy ) ) {
				if ( 'archive' === $type && 'default' === JB()->options()->get( 'job-archive-template' ) ) {
					$template = untrailingslashit( JB_PATH ) . '/templates/job-archive.php';
				}
			}

			return $template;
		}

		/**
		 * @param $pre_render
		 * @param $parsed_block
		 *
		 * @return mixed
		 */
		public function jb_change_archive_template( $pre_render, $parsed_block ) {
			$tax_id = get_queried_object_id();
			$attrs  = array();

			if ( 'jb-job-category' === get_queried_object()->taxonomy ) {
				$attrs = array(
					'category' => array(
						$tax_id,
					),
				);
			} elseif ( 'jb-job-type' === get_queried_object()->taxonomy ) {
				$attrs = array(
					'type' => array(
						$tax_id,
					),
				);
			}

			if ( 'core/group' === $parsed_block['blockName'] ) {
				$key_path = $this->search_path( 'core/query', $parsed_block );
				if ( false !== $key_path ) {
					$attrs_path = str_replace( '_blockName', '_attrs', $key_path );
					$this->set( $key_path, $parsed_block, 'jb-block/jb-jobs-list' );
					$this->set( $attrs_path, $parsed_block, $attrs );
				}
			}

			return $parsed_block;
		}

		/**
		 * @param string|array $path
		 * @param array        $parsed_block
		 * @param null         $value
		 *
		 * @return array|mixed|null
		 */
		private function set( $path, &$parsed_block = array(), $value = null ) {
			$path = explode( '_', $path );
			$temp = &$parsed_block;

			foreach ( $path as $key ) {
				$temp = &$temp[ $key ];
			}
			$temp = $value;

			return $temp;
		}

		/**
		 * @param $needle
		 * @param $haystack
		 *
		 * @return bool|int|string
		 */
		private function search_path( $needle, $haystack ) {
			if ( ! is_array( $haystack ) ) {
				return false;
			}

			foreach ( $haystack as $key => $value ) {
				if ( 'core/query' === $value && $value === $needle ) {
					return $key;
				}

				if ( is_array( $value ) ) {
					$key_result = $this->search_path( $needle, $value );
					if ( false !== $key_result ) {
						return $key . '_' . $key_result;
					}
				}
			}

			return false;
		}

		/**
		 * Callback for twentytwenty WP native theme to disable showing post meta
		 *
		 * @param array $types
		 *
		 * @return array
		 *
		 * @since 1.0
		 */
		public function add_cpt_meta( $types ) {
			$types[] = 'jb-job';
			return $types;
		}

		/**
		 * Hide job thumbnail
		 *
		 * @param bool $has_thumbnail
		 * @param int|WP_Post|array $post
		 *
		 * @return bool
		 *
		 * @since 1.0
		 */
		public function hide_post_thumbnail( $has_thumbnail, $post ) {
			if ( ! $post ) {
				$post = get_post( get_the_ID() );
			}

			if ( isset( $post->post_type ) && 'jb-job' === $post->post_type ) {
				$has_thumbnail = false;
			}

			return $has_thumbnail;
		}

		/**
		 * If it's job individual page loading by default Page template from theme
		 *
		 * @param string $template
		 *
		 * @return string
		 *
		 * @since 1.0
		 */
		public function cpt_template_include( $template ) {
			if ( JB()->frontend()->is_job_page() ) {

				$template_setting = JB()->options()->get( 'job-template' );
				if ( 'default' === $template_setting ) {
					if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) {
						// add scripts and styles, but later because wp_loaded is earlier
						add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_single_job' ), 9999 );

						add_filter( 'the_content', array( &$this, 'before_job_content' ), 99999 );
						add_filter( 'the_content', array( &$this, 'after_job_content' ), 99999 );
						return $template;
					}

					$t              = get_template_directory() . DIRECTORY_SEPARATOR . 'singular.php';
					$child_template = get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'singular.php';
					if ( file_exists( $child_template ) ) {
						$t = $child_template;
					}

					// load page.php if singular isn't found
					if ( ! file_exists( $t ) ) {
						$t              = get_template_directory() . DIRECTORY_SEPARATOR . 'page.php';
						$child_template = get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'page.php';
						if ( file_exists( $child_template ) ) {
							$t = $child_template;
						}
					}

					// load index.php if page isn't found
					if ( ! file_exists( $t ) ) {
						$t              = get_template_directory() . DIRECTORY_SEPARATOR . 'index.php';
						$child_template = get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'index.php';
						if ( file_exists( $child_template ) ) {
							$t = $child_template;
						}
					}

					if ( ! file_exists( $t ) ) {
						return $template;
					}

					add_action( 'wp_head', array( &$this, 'on_wp_head_finish' ), 99999999 );
					add_filter( 'the_content', array( &$this, 'cpt_content' ), 10, 1 );
					add_filter( 'post_class', array( &$this, 'hidden_title_class' ), 10, 1 );
				} else {
					$t              = get_template_directory() . DIRECTORY_SEPARATOR . 'jobboardwp' . DIRECTORY_SEPARATOR . $template_setting . '.php';
					$child_template = get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'jobboardwp' . DIRECTORY_SEPARATOR . $template_setting . '.php';
					if ( file_exists( $child_template ) ) {
						$t = $child_template;
					}
				}

				/**
				 * Filters the individual job page template.
				 *
				 * @since 1.0
				 * @hook jb_template_include
				 *
				 * @param {string} $template Path to the individual job page template.
				 *
				 * @return {array} Path to the individual job page template.
				 */
				return apply_filters( 'jb_template_include', $t );
			}

			return $template;
		}

		/**
		 * Change block template for single job
		 *
		 * @param WP_Block_Template[] $query_result Array of found block templates.
		 *
		 * @return array
		 *
		 * @since 1.2.4
		 */
		public function jb_change_single_job_block_templates( $query_result ) {
			$theme = wp_get_theme();

			global $wp_filesystem;

			if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
				require_once ABSPATH . 'wp-admin/includes/file.php';

				$credentials = request_filesystem_credentials( site_url() );
				WP_Filesystem( $credentials );
			}

			$template_contents = $wp_filesystem->get_contents( wp_normalize_path( JB_PATH . 'templates/block-templates/single.html' ) );
			$template_contents = str_replace(
				array(
					'~theme~',
					'~jb_single_job_content~',
				),
				array(
					$theme->get_stylesheet(),
					'[jb_job id="' . get_the_ID() . '" /]',
				),
				$template_contents
			);

			$new_block                 = new WP_Block_Template();
			$new_block->type           = 'wp_template';
			$new_block->theme          = $theme->get_stylesheet();
			$new_block->slug           = 'single';
			$new_block->id             = $theme->get_stylesheet() . '//single';
			$new_block->title          = 'single';
			$new_block->description    = '';
			$new_block->source         = 'plugin';
			$new_block->status         = 'publish';
			$new_block->has_theme_file = true;
			$new_block->is_custom      = true;
			$new_block->content        = $template_contents;
			$new_block->area           = 'uncategorized';

			$query_result[] = $new_block;

			return $query_result;
		}

		/**
		 * Clear the post title
		 *
		 * @since 1.0
		 */
		public function on_wp_head_finish() {
			add_filter( 'the_title', array( $this, 'clear_title' ), 10, 2 );
		}

		/**
		 * Return empty title
		 *
		 * @param string $title
		 * @param int $post_id
		 *
		 * @return string
		 *
		 * @since 1.0
		 */
		public function clear_title( $title, $post_id ) {
			$post = get_post( $post_id );

			if ( ! empty( $post ) && 'jb-job' === $post->post_type ) {
				$title = '';
			}

			return $title;
		}

		/**
		 * Set default content of the job page
		 *
		 * @param string $content
		 *
		 * @return string
		 *
		 * @since 1.0
		 */
		public function cpt_content( $content ) {
			global $post;

			remove_filter( 'the_title', array( $this, 'clear_title' ) );
			remove_filter( 'has_post_thumbnail', array( &$this, 'hide_post_thumbnail' ) );

			if ( JB()->frontend()->is_job_page() ) {
				$this->template_replaced = true;

				$content = JB()->frontend()->shortcodes()->single_job(
					array(
						'id'            => $post->ID,
						'ignore_status' => true,
					)
				);
			}

			return $content;
		}

		/**
		 * Add hidden class if users need to add some custom CSS on page template to hide a header when title is hidden
		 *
		 * @param array $classes
		 *
		 * @return array
		 *
		 * @since 1.0
		 */
		public function hidden_title_class( $classes ) {
			$classes[] = 'jb-hidden-title';
			return $classes;
		}

		/**
		 * Add structured data to the footer of job listing pages.
		 *
		 * @since 1.0
		 */
		public function output_structured_data() {
			if ( ! is_singular( 'jb-job' ) ) {
				return;
			}

			if ( JB()->options()->get( 'disable-structured-data' ) ) {
				return;
			}

			$structured_data = JB()->common()->job()->get_structured_data( get_post() );

			if ( empty( $structured_data ) ) {
				return;
			}

			// test via this page https://developers.google.com/search/docs/advanced/structured-data
			echo '<!-- Job Board Structured Data -->' . "\r\n";
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- structured data output, early escaped
			echo '<script type="application/ld+json">' . _wp_specialchars( wp_json_encode( $structured_data ), ENT_NOQUOTES, 'UTF-8', true ) . '</script>';
		}

		/**
		 * Dropdown menu template
		 *
		 * @param string $element
		 * @param string $trigger
		 * @param array $items
		 *
		 * @since 1.0
		 */
		public function dropdown_menu( $element, $trigger, $items = array() ) {
			// !!!!Important: all links in the dropdown items must have "class" attribute
			?>

			<div class="jb-dropdown" data-element="<?php echo esc_attr( $element ); ?>" data-trigger="<?php echo esc_attr( $trigger ); ?>">
				<ul>
					<?php foreach ( $items as $k => $v ) { ?>
						<li><?php echo wp_kses( $v, JB()->get_allowed_html() ); ?></li>
					<?php } ?>
				</ul>
			</div>

			<?php
		}
	}
}