<?php

class Rover_IDX_Content
	{
	public $rover_html							= null;

	private Rover_IDX $rover_idx;

	private $rover_head							= [
												'title'					=> null,
												'meta_desc'				=> null,
												'og_images'				=> null,
												'meta_robots'			=> null,
												'meta_keywords'			=> null,
												'canonical_url'			=> null,
												];

	private $rover_body_class_meta				= null;
	private $rover_body_class_pt				= null;
	private $rover_title						= null;
	private $rover_meta_desc					= null;
	private	$rover_og_images					= null;
	private $rover_meta_robots					= null;
	private $rover_meta_keywords				= null;
	private	$rover_canonical_url				= null;
	private $rover_component					= null;
	private	$rover_redirect						= null;

	private $rover_dynamic_post					= null;		#	The WP_Post object
	private $wp_hook_mode						= null;

	private $dynamic_sidebar					= null;

	private $fetching_api_key					= false;
	private $curl_handle						= null;
	private static $shared_curl_handle			= null;


	private	const ENDPOINT_KEY					= 'roveridx_endpoint';
	private const FRONT_COMPONENTS				= [
												'ROVER_COMPONENT_404'							=> true,
												'ROVER_COMPONENT_AFFORDABILITY_CALCULATOR'		=> true,
												'ROVER_COMPONENT_CTA'							=> true,
												'ROVER_COMPONENT_FULL_PAGE'						=> true,
												'ROVER_COMPONENT_HOME_WORTH'					=> true,
												'ROVER_COMPONENT_HOME_WORTH_TWO_PAGE'			=> true,
												'ROVER_COMPONENT_LISTINGS'						=> true,
												'ROVER_COMPONENT_MORTGAGE_CALCULATOR'			=> true,
												'ROVER_COMPONENT_PLUGIN'						=> true,
												'ROVER_COMPONENT_PROP_DETAILS'					=> true,
												'ROVER_COMPONENT_RESULTS'						=> true,
												'ROVER_COMPONENT_RESULTS_AS_MAP'				=> true,
												'ROVER_COMPONENT_RESULTS_AS_TABLE'				=> true,
												'ROVER_COMPONENT_SEARCH_PANEL'					=> true,
												'ROVER_COMPONENT_SITE_SEARCH'					=> true,
												'ROVER_COMPONENT_TESTIMONIALS'					=> true,
												];

	public function __construct(Rover_IDX $rover_idx)
		{
		$this->rover_idx						= $rover_idx;

		$this->wp_hook_mode						= (isset($this->rover_idx->roveridx_theming['wp_hook_mode']))
														? $this->rover_idx->roveridx_theming['wp_hook_mode']
														: null;


		#	Share handle across all instances
		#	Connection Pooling Alternative
		#	On a server with persistent PHP processes (like PHP-FPM with long-running workers), use a static curl handle:

		if (self::$shared_curl_handle === null) {
			self::$shared_curl_handle			= curl_init();
			}

		#	curl_close() is a no-op since php 8.0, and deprecated in 8.5
		#	so don't register a shutdown function to close the handle

		$this->curl_handle						= self::$shared_curl_handle;
		}

	public function __destruct() {

		#	Do not close shared handle

#		if ($this->curl_handle !== null) {
#			curl_close($this->curl_handle);
#			}
		}

	public function setup_404()
		{
		if ($this->wp_hook_mode === 'router_style')
			{
			#	This is handled throught add_action( 'parse_request', ...
#			add_filter( 'router_style', 				[ $this, 'dynamic_page_request' ] );
			}
		else if ($this->wp_hook_mode === 'the_posts')
			{
			add_filter( 'the_posts', 			[ $this, 'dynamic_page_the_posts' ], 10, 2 );
			}
		else if ($this->wp_hook_mode === 'pre_get_posts')
			{
			add_action( 'pre_get_posts', 		[ $this, 'dynamic_page_pre_get_posts' ] );
			}
		else
			{
			add_action( 'template_redirect',	[ $this, 'dynamic_page_template_redirect' ], 0 );
			}

		if (isset($this->rover_idx->roveridx_theming['wp_prevent_404_guess']) && $this->rover_idx->roveridx_theming['wp_prevent_404_guess']  == 'yes')
			add_filter('do_redirect_guess_404_permalink', '__return_false');
		}

	public function dynamic_page_router_style_parse_request($query_vars)	{

		#	stop fighting the WordPress query lifecycle and instead intercept routing before WordPress decides
		#	it is a 404, then hand WordPress a normal-looking result.

		if ($this->wp_hook_mode === 'router_style')
			{
			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'Starting');

			if (false !== $this->rover_idx->check_url_for_idx_keys())
				{
				if ($this->has_404_content())
					{
					global								$wp_query, $post;

					#	Claim the request

					$query_vars->rover_dynamic			= 1;
					$query_vars->pagename				= '__rover__';
					$query_vars->dynamic_page_id		= '__rover__';

					#	Make it globally visible immediately
					$GLOBALS['post']					= $this->rover_dynamic_post;

					#	Prime WP_Query early
					$wp_query->post						= $this->rover_dynamic_post;
					$wp_query->posts					= [ $this->rover_dynamic_post ];
					$wp_query->queried_object			= $this->rover_dynamic_post;
					$wp_query->queried_object_id		= $this->rover_dynamic_post->ID;

					$wp_query->query_vars['page_id']	= $this->rover_dynamic_post->ID;
					$wp_query->query_vars['post_type']	= 'page';
					$wp_query->query_vars['pagename']	= (string) $this->rover_idx->rover_404_slugs;

					$wp_query->is_page					= true;
					$wp_query->is_singular				= true;
					$wp_query->is_404					= false;
					$wp_query->found_posts				= 1;
					$wp_query->post_count				= 1;
					$wp_query->max_num_pages			= 1;

					#	Lock in the object cache so get_post() always finds it
					wp_cache_set( $this->rover_dynamic_post->ID, $this->rover_dynamic_post, 'posts' );

					#	Keep the_posts — but now it’s just a safety net

					add_filter( 'the_posts',			[ $this, 'dynamic_inject_router_style_post' ], 10, 2 );
					}
				}
			}

		return $query_vars;
		}

	public function dynamic_page_router_style_pre_get_posts($query)	{

		#	stop fighting the WordPress query lifecycle and instead intercept routing before WordPress decides
		#	it is a 404, then hand WordPress a normal-looking result.

		if ($this->wp_hook_mode === 'router_style')
			{
			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'Starting');

			if (false !== $this->rover_idx->check_url_for_idx_keys())
				{
				if ($this->has_404_content())
					{
					global								$wp_query, $post;

					#	Claim the request

#					$query_vars->rover_dynamic			= 1;
#					$query_vars->pagename				= '__rover__';
#					$query_vars->dynamic_page_id		= '__rover__';
					$query->set( 'rover_dynamic',		1 );
					$query->set( 'pagename',			(string) $this->rover_idx->rover_404_slugs );
					$query->set( 'dynamic_page_id',		'__rover__' );

					#	Make it globally visible immediately
					$GLOBALS['post']					= $this->rover_dynamic_post;

					#	Prime WP_Query early
					$wp_query->post						= $this->rover_dynamic_post;
					$wp_query->posts					= [ $this->rover_dynamic_post ];
					$wp_query->queried_object			= $this->rover_dynamic_post;
					$wp_query->queried_object_id		= $this->rover_dynamic_post->ID;

					$wp_query->query_vars['page_id']	= $this->rover_dynamic_post->ID;
					$wp_query->query_vars['post_type']	= 'page';
					$wp_query->query_vars['pagename']	= (string) $this->rover_idx->rover_404_slugs;

					$wp_query->is_page					= true;
					$wp_query->is_singular				= true;
					$wp_query->is_404					= false;
					$wp_query->found_posts				= 1;
					$wp_query->post_count				= 1;
					$wp_query->max_num_pages			= 1;

					#	Lock in the object cache so get_post() always finds it
#					wp_cache_set( $this->rover_dynamic_post->ID, $this->rover_dynamic_post, 'posts' );

					#	Keep the_posts but now as just a safety net

					add_filter( 'the_posts',			[ $this, 'dynamic_inject_router_style_post' ], 10, 2 );
					}
				}
			}
		}

	public function dynamic_page_the_posts($posts)	{

		#	Working within the WordPress query loop, add our content IF this request is a 404

		$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'Starting');

		remove_filter('the_posts',				[ $this, 'dynamic_page_the_posts' ] );	#	Avoid firing twice

#		if (!is_404()) {	return;		}

		return $this->init_404_content($posts);	#	return $post
		}

	public function dynamic_page_pre_get_posts($posts)	{

		#	Working within the WordPress query loop, add our content IF this request is a 404

		$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'Starting');

		remove_filter('pre_get_posts',			[ $this, 'dynamic_page_pre_get_posts' ] );	#	Avoid firing twice

#		if (!is_404()) {	return;		}

		return $this->init_404_content($posts);	#	return $post
		}

	public function dynamic_page_template_redirect()	{

		#	Working within the WordPress query loop, add our content IF this request is a 404

		$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'Starting');

		remove_filter('template_redirect',		[ $this, 'dynamic_page_template_redirect' ] );	#	Avoid firing twice

#		if (!is_404()) {	return;		}

		$this->init_404_content($posts = null);			#	do not return $post
		}

	public function dynamic_inject_router_style_post($posts, $query)	{

		if ( ! $query->is_main_query() ) {
			return $posts;
			}

		if ($this->wp_hook_mode === 'router_style')
			{
			global								$wp_query, $post;

			#	HARD REQUIREMENTS FOR BLOCK THEMES
			$GLOBALS['post']					= $this->rover_dynamic_post;

			#	Populate main query
			if ( empty( $wp_query->post ) ) {
				$wp_query->post					= $this->rover_dynamic_post;
				$wp_query->posts				= [$this->rover_dynamic_post];
				$wp_query->queried_object		= $this->rover_dynamic_post;
				$wp_query->queried_object_id 	= $this->rover_dynamic_post->ID;

				$wp_query->set( 'page_id', $this->rover_dynamic_post->ID );
				$wp_query->set( 'post_type', 'page' );
				$wp_query->set( 'pagename', (string) $this->rover_idx->rover_404_slugs );

				$wp_query->is_page				= true;
				$wp_query->is_singular			= true;
				$wp_query->is_404				= false;
				$wp_query->found_posts			= 1;
				$wp_query->post_count			= 1;
				$wp_query->max_num_pages		= 1;
				}

			#	Optional: filter posts for themes/plugins expecting the_posts
#				add_filter('the_posts', function($posts, $query) use ($post) {
#					if (!$query->is_main_query()) return $posts;
#					return [$post];
#					}, 10, 2);

			return [$this->rover_dynamic_post];
			}

		return $posts;
		}

	private function has_404_content()
		{
		if (!is_null($this->rover_dynamic_post))			#	We've already fetched the content
			return $this->rover_dynamic_post;

		$component								= 'ROVER_COMPONENT_404';
		if (is_string($this->rover_idx->found_slug) && (substr($this->rover_idx->found_slug, 0, 6) === "rover-"))
			$component							= $this->rover_idx->found_slug;
		else if (is_string($this->rover_idx->found_slug) && (substr($this->rover_idx->found_slug, 0, 4) === "idx-"))
			$component							= $this->rover_idx->found_slug;

		$the_rover_content						= $this->rover_content(	$component, array('region' => $this->rover_idx->rover_404_regions));

		$this->rover_html						= (isset($the_rover_content['the_html']))
														? $the_rover_content['the_html']
														: null;
		$this->rover_component					= $component;			#	$the_rover_content['the_component'];
		$this->rover_redirect					= (isset($the_rover_content['the_redirect']))
														? $the_rover_content['the_redirect']
														: null;

		if ($this->rover_idx->COMMON->is_debuggable())
			{
			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'rover_component is ['.$this->rover_component.']');
			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, ((isset($this->rover_html)) ? strlen($this->rover_html) : 0).' bytes received from rover_content');
			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'redirect is ['.$this->rover_redirect.']');
			}

		if (empty($this->rover_html) && empty($this->rover_redirect))
			return false;

		$this->setup_dynamic_meta($the_rover_content);
		$this->rover_dynamic_post				= $this->generate_404_content();

		if ($this->wp_hook_mode === 'router_style') {

			setup_postdata( $this->rover_dynamic_post );

#			return $dynamic_post;
			}

		if (!wp_is_block_theme())
			add_filter( 'template_include', [ $this, 'template_include' ], 99 );			#	php-based themes

		#	Add this to avoid this error:
		#	Warning: Attempt to read property "post_type" on null in .../wp-includes/link-template.php on line 4188
		add_filter('pre_get_shortlink', function($shortlink, $id, $context, $allow_slugs) {
			if ($id == $this->rover_dynamic_post->ID ||
				($context === 'query' && get_queried_object_id() == $this->rover_dynamic_post->ID)) {
				return $this->rover_dynamic_post->guid; // or return false to disable shortlinks
				}
			return $shortlink;
			}, 10, 4);

		return true;
		}

	private function init_404_content($original_posts)
		{
		global									$wp, $wp_query, $post;

		if ($this->has_404_content())
			{
			if ($this->wp_hook_mode === 'router_style')	#	We've already setup $this->rover_dynamic_post
				return $this->rover_dynamic_post;

			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, '*** This is a Dynamic Page ***');

			$this->redirect_if_necessary();

			$dynamic_post						= null;
			status_header( 200 );

			$wp_query->is_404					= false;

			#	Trick wp_query into thinking this is a page (necessary for wp_title() at least)
			#	Not sure if it's cheating or not to modify global variables in a filter
			#	but it appears to work and the codex doesn't directly say not to.

			$wp_query->post						= $this->rover_dynamic_post;
			$wp_query->posts					= [ $this->rover_dynamic_post ];

			$wp_query->queried_object			= $this->rover_dynamic_post;
			$wp_query->queried_object_id		= $this->rover_dynamic_post->ID;

			$wp_query->is_rover_page			= true;		#	Used for domain '8'

			$wp_query->found_posts				= 1;
			$wp_query->post_count				= 1;
			$wp_query->max_num_pages			= 1;

#			add_filter( 'template_include', [ $this, 'template_include' ], 99 );				#	php-based themes

			#	We want this to be a page - more flexible for setting templates dynamically

			$wp_query->is_page					= true;

			$wp_query->is_single				= false;	#	Applicable to Posts
			$wp_query->is_singular				= true;		#	Applicable to Pages

			$wp_query->is_attachment			= false;
			$wp_query->is_archive				= false;
			$wp_query->is_category				= false;
			$wp_query->is_tag					= false;
			$wp_query->is_tax					= false;
			$wp_query->is_author				= false;
			$wp_query->is_date					= false;
			$wp_query->is_year					= false;
			$wp_query->is_month					= false;
			$wp_query->is_day					= false;
			$wp_query->is_time					= false;
			$wp_query->is_search				= false;
			$wp_query->is_feed					= false;
			$wp_query->is_comment_feed			= false;
			$wp_query->is_trackback				= false;
			$wp_query->is_home					= false;
			$wp_query->is_embed					= false;
			$wp_query->is_404					= false;
			$wp_query->is_paged					= false;
			$wp_query->is_admin					= false;
			$wp_query->is_preview				= false;
			$wp_query->is_robots				= false;
			$wp_query->is_posts_page			= false;
			$wp_query->is_post_type_archive		= false;

			// Longer permalink structures may not match the fake post slug and cause a 404 error so we catch the error here
			unset($wp_query->query["error"]);
			$wp_query->query_vars["error"]		= "";

			$wp_query->is_404					= false;

			/* Update globals		*/
#				$GLOBALS['wp_query']			= $wp_query;
#				$wp->register_globals();

			return [$this->rover_dynamic_post];
			}
		else
			{
			#	This is a real 404 - let WP do it's thing
			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, '*** Not a Dynamic Page ***');

			status_header( 404 );
			$wp_query->is_404					= true;
			}

		return $original_posts;
		}

	private function generate_404_content()
		{
		$the_guid_parts							= $this->rover_redirect;
		if (empty($this->rover_redirect))
			{
			$the_guid_parts						= $this->rover_idx->curr_path;
			}

		#	If Rover is creating this content, tell WP to skip the annoying 'wpautop'
		#	function, which loves to wrap double line-breaks in <p> tags

		remove_filter( 'the_content', 'wpautop' );
		remove_filter( 'the_excerpt', 'wpautop' );

		$post									= new stdClass();

		$post->ID								= $this->rover_idx->COMMON->get_rover_post_id();
		$post->post_author						= get_current_user_id();

		#	The safe name for the post.  This is the post slug.

		$post->post_name						= (string) $this->rover_idx->rover_404_slugs;
		$post->post_type						= 'page';
		$post->post_parent						= 0;

		#	guid is used by WordPress canonical and permalink logic. For synthetic posts, it should be stable and unique:

		$post->guid								= get_bloginfo("wpurl") . $the_guid_parts;

		$post->post_title						= (isset($this->rover_head['title']) && !empty($this->rover_head['title']))
														? $this->rover_head['title']
														: (string) $this->rover_idx->rover_404_slugs;

		$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'Creating content for '.$this->rover_idx->rover_404_regions.' ('.strlen($this->rover_html).' bytes) ['.$post->guid.']');


		remove_action( 'wp_head', 'feed_links_extra', 3 );		#	Removes the links to the extra feeds such as category feeds
		remove_action( 'wp_head', 'feed_links', 2 );			#	Removes links to the general feeds: Post and Comment Feed
		remove_action( 'wp_head', 'rsd_link');					#	Removes the link to the Really Simple Discovery service endpoint, EditURI link
		remove_action( 'wp_head', 'wlwmanifest_link');			#	Removes the link to the Windows Live Writer manifest file.
		remove_action( 'wp_head', 'index_rel_link');			#	Removes the index link
		remove_action( 'wp_head', 'parent_post_rel_link');		#	Removes the prev link
		remove_action( 'wp_head', 'start_post_rel_link');		#	Removes the start link
		remove_action( 'wp_head', 'adjacent_posts_rel_link');	#	Removes the relational links for the posts adjacent to the current post.
		remove_action( 'wp_head', 'wp_generator');				#	Removes the WordPress version - WordPress 2.8.4
		remove_action( 'wp_head', 'rel_canonical' );			#	Remove default canonical url - which will be wrong

		add_filter( 'body_class', [ $this, 'body_class' ] );
		add_filter( 'the_title', function( $title, $id = null ) {

			#	WordPress applies the_title filter twice
			#	- First as the corresponding post or page title.
			#	- Then as the Menu item title itself.  This happens in the Walker_Nav_Menu class.

			#	It is important that we ONLY affect the page title, and not the Nav title

			if (!is_null($id) && $id == $this->rover_idx->post_id && isset($this->rover_head['title']))
				return strip_tags( $this->rover_head['title'] );

			return $title;
			} );

#		add_filter( 'wp_title', '__return_false' );

		add_filter( 'wp_head', [ $this, 'head_meta_items' ], 5 );

		add_filter( 'wp_robots', function( $robots ) {

			if (isset($this->rover_head['meta_robots']))
				{
				if (strpos($this->rover_head['meta_robots'], 'noindex') !== false)
					$robots['noindex']			= true;
				if (strpos($this->rover_head['meta_robots'], 'nofollow') !== false)
					$robots['nofollow']			= true;
				if (strpos($this->rover_head['meta_robots'], 'noarchive') !== false)
					$robots['noarchive']		= true;
				}

  			return $robots;
			});

		$post->post_content						= $this->rover_html . $this->formatted_debug();
		$post->post_excerpt						= '';

		#	Fake post ID to prevent WP from trying to show comments for
		#	a post that doesn't really exist.

		$this->rover_idx->post_id				= $post->ID;

		$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'is using post_id '.$post->ID);

		#	means a page, not a post.

		$post->post_status						= 'publish';
		$post->comment_status					= 'closed';
		$post->ping_status						= 'closed';		// $this->ping_status;
		$post->filter							= 'raw';		// important!

		$post->comment_count					= 0;

		$post->post_date						= current_time('mysql');
		$post->post_date_gmt					= current_time('mysql', 1);

		$post									= new WP_Post( $post );

#		Save this post (by it's ID) for the duration of this request
#
#		https://developer.wordpress.org/reference/classes/wp_object_cache/
#		By default, the object cache is non-persistent. This means that data stored in the cache resides
#		in memory only and only for the duration of the request. Cached data will not be stored persistently
#		across page loads unless you install a persistent caching plugin.

		if (!$this->rover_idx->COMMON->is_rover_panel($this->rover_component))
			{
			wp_cache_add( $post->ID, $post, 'posts' );
#			wp_cache_add( $post->ID, $post, 'post_meta' );
			}

		return $post;
		}

	public function head_meta_items() {

		$meta_html						= array();

		if (isset($this->rover_head['title']) && !empty($this->rover_head['title']))
			{
			$stripped_title				= strip_tags( $this->rover_head['title'] );
			$meta_html[]				= "<title>".$stripped_title."</title>";
			$meta_html[]				= "<meta property='og:title' content='".$stripped_title."'>";
			}

		if (isset($this->rover_head['meta_desc']) && !empty($this->rover_head['meta_desc']))
			{
			$meta_html[]				= "<meta name='description' content='".esc_attr($this->rover_head['meta_desc'])."' />";
			$meta_html[]				= "<meta property='og:description' content='".esc_attr($this->rover_head['meta_desc'])."'>";
			}

		if (isset($this->rover_head['meta_keywords']) && !empty($this->rover_head['meta_keywords']))
			$meta_html[]				= "<meta name='keywords' content='".esc_attr($this->rover_head['meta_keywords'])."' />";

		if (isset($this->rover_head['canonical_url']) && !empty($this->rover_head['canonical_url']))
			{
			$meta_html[]				= "<link rel='canonical' class='rover idx' href='".$this->rover_head['canonical_url']."' />";
			$meta_html[]				= "<meta property='og:url' content='".$this->rover_head['canonical_url']."'>";
			}

		$meta_html[]					= "<meta name='generator' content='Rover IDX ".roverVERSIONS::VERSION."' />";

		if (isset($this->rover_head['og_images']) && !is_null($this->rover_head['og_images']))
			{
			foreach(explode(',', $this->rover_head['og_images']) as $one_img)
				$meta_html[]			= "<meta property='og:image' content='".$one_img."'>";
			}

		$meta_html[]					= "<meta property='og:updated_time' content='".date('Y-m-dTH:i:s+00:00')."' />";

		if (count($meta_html))
			{
			echo "\n\n<!-- This site is optimized with the Rover IDX plugin version ".roverVERSIONS::FULL." - https://wordpress.org/plugins/rover-idx/ -->\n";
			echo implode("\n", $meta_html);
			echo "\n<!-- / Rover IDX meta items -->\n\n";
			}
		}


	public function body_class($classes) {

		if (is_null($this->rover_body_class_meta) && is_null($this->rover_body_class_pt))
			return $classes;

		if (is_array($this->rover_body_class_meta))		#	This is an array
			{
			$classes							= [...$classes, ...$this->rover_body_class_meta];
			}

		if (!is_null($this->rover_body_class_pt))		#	This is a string
			{
			foreach($classes as $key => $one_class)
				{
				if (substr($one_class, 0, 14) === 'page-template-')		#	skip this page-template-default
					{
					unset($classes[$key]);
					}
				}

			$classes[]							= $this->rover_body_class_pt;
			}

		return array_values($classes);			#	Re-index the array
		}

	public function update_site_settings($atts)	{

		if (is_array($atts) && count($atts))
			{
			$the_rover_content					= $this->rover_content(
																'ROVER_COMPONENT_UPDATE_SITE_SETTINGS',
																$atts
																);
			}

		}

	public function rebuild_regions($force = false)	{


		$perform_refresh						= false;
		$region_refresh							= get_option(roverOPTIONS::REGION_REFRESH);

		if ($force === true)
			{
			$perform_refresh					= true;
			}
		else if ($region_refresh !== false)
			{
			$perform_refresh					= true;
			}
		else if ($this->rover_idx->COMMON->request_is_debuggable())
			{
			#	A way to force a region rebuild when the setting is missing or invalid,
			#	without depending on whether the rest api points are `rest_forbidden`

			$perform_refresh					= true;
			}
		else
			{
			if (strtotime('now') < strtotime('+6 hours', $perform_refresh))
				$perform_refresh				= true;
			else
				$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, ' it is not time to rebuild_regions ['.date('Y-m-d H:i:s', strtotime($perform_refresh)).']');
			}

		if ($perform_refresh)
			{
			update_option(roverOPTIONS::REGION_REFRESH, strtotime('now'));

			$rebuild							= json_decode($this->build_regions_string(), true);

			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, ' build_regions_string ['.print_r($rebuild, true).']');

			if (is_array($rebuild))
				{
				if (isset($rebuild['domain_id']))
					{
					$this->rover_idx->update_region_settings(__FUNCTION__, __LINE__, 'domain_id', $rebuild['domain_id']);
					}

				if (isset($rebuild['regions']))
					{
					$this->rover_idx->update_region_settings(__FUNCTION__, __LINE__, 'regions', $rebuild['regions']);
					$this->rover_idx->update_region_settings(__FUNCTION__, __LINE__, 'region', $rebuild['regions']);
					}
				}
			}
		}

	public function build_regions_string()	{


		$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'starting');

		$the_rover_content						= $this->rover_content(
																'ROVER_COMPONENT_REBUILD_REGIONS',
																array('region' => 'REBUILD', 'regions' => 'REBUILD')
																);

		$regions_str							= $the_rover_content['the_html'];

		$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'regions string ['.$regions_str.']');

		return $regions_str;
		}

	private function get_endpoint_resolve() {

		static $endpoint						= null;

		if ($endpoint !== null) {
			return $endpoint;
			}

		return $endpoint						= get_option(self::ENDPOINT_KEY);
		}

	private function get_api_key()	{

		static $cached_key						= null;
		static $already_checked					= false;

		// If we've already determined the key, return it immediately
		if ($already_checked) {
			return $cached_key;
			}

		if ( $this->fetching_api_key )
			{
			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'already fetching');
			return null;
			}

		$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'starting');

		#	if necessary, fetch a new api key

		if (empty($this->rover_idx->roveridx_regions['api_key']))
			{
			$this->fetching_api_key				= true;

			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'Fetching new API key');

			$the_rover_content					= $this->rover_content(
																'ROVER_COMPONENT_GET_API_KEY',
																array('not-region' => 'Not used', 'not-regions' => 'Not Used')
																);

			$api_key							= $the_rover_content['the_html'];
#			$api_key							= json_decode($the_rover_content['the_html'], true);

			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'Received new API key ['.$api_key.']');

			#	Unusual error path may case ROVER_COMPONENT_GET_API_KEY to return
			#	api_key=%3Cdiv+style%3D%22display%3Anone%22%3Ejson_decode%28%29+failed+%5B%3Cspan+style%3D%22color%3Ared%3B%22%3Erequest+%28%29+is+missing+required+parameters+%5Bcomponent%5D%3C%2Fspan%3E%5D%3C%2Fdiv%3E

			if (!empty($api_key) && (strpos($api_key, 'json_decode') === false))
				{
				if ($this->rover_idx->update_region_settings(__FUNCTION__, __LINE__, 'api_key', $api_key))
					$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'Saved new API key to Region options');

				$cached_key						= $api_key;
				$already_checked				= true;

				return $api_key;
				}
			}

		if (empty($this->rover_idx->roveridx_regions['api_key']))
			{
			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'failed');
			return null;
			}

		$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'Returning ['.$this->rover_idx->roveridx_regions['api_key'].']');

		#	Cache the result (even if null) so we don't check again
		$cached_key								= $this->rover_idx->roveridx_regions['api_key'] ?? null;
		$already_checked						= true;

		return $cached_key;
		}

	private function check_js_version($rover_content)	{


		if (isset($rover_content['the_js_ver']) && !empty($rover_content['the_js_ver']))
			{
			$newest_js_ver						= $rover_content['the_js_ver'];
			$current_js_ver						= (isset($this->rover_idx->roveridx_theming['js_version']))
														? $this->rover_idx->roveridx_theming['js_version']
														: roverVERSIONS::JS;

			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, ' latest_js_ver ['.$newest_js_ver.'] / ['.$current_js_ver.']');

			if ((version_compare($newest_js_ver, $current_js_ver) !== 0))
				{
				if (!is_array($theme_opts = get_option(roverOPTIONS::THEME)))
					$theme_opts					= array();

				if (is_array($theme_opts))
					{
					$theme_opts['js_version']		= $newest_js_ver;
					update_option(roverOPTIONS::THEME, $theme_opts);

					$this->rover_idx->roveridx_theming	= $theme_opts;

					$this->rover_idx->refresh_css();
					}
				}
			}
		}

	private function check_endpoint_resolve($rover_content)	{

		if (isset($rover_content['the_endpoint_resolve']) && !empty($rover_content['the_endpoint_resolve']))
			{
			$curr_endpoint_resolve				= $this->get_endpoint_resolve();

			if ($curr_endpoint_resolve === false)
				update_option(self::ENDPOINT_KEY, $rover_content['the_endpoint_resolve']);
			else if (strcmp($curr_endpoint_resolve, $rover_content['the_endpoint_resolve']) !== 0)
				update_option(self::ENDPOINT_KEY, $rover_content['the_endpoint_resolve']);
			}
		}

	private function setup_dynamic_meta($the_rover_content = null)	{

		$robots									= array();

		if (!is_null($the_rover_content))
			{
			#	T I T L E
			if (isset($the_rover_content['the_title']) && !empty($the_rover_content['the_title']))
				$this->rover_head['title']		= $the_rover_content['the_title'];

			#	D E S C R I P T I O N
			if (isset($the_rover_content['the_meta_desc']) && !empty($the_rover_content['the_meta_desc']))
				$this->rover_head['meta_desc']	= $the_rover_content['the_meta_desc'];

			#	O G  I M A G E S
			if (isset($the_rover_content['the_og_images']))
				$this->rover_head['og_images']	= $the_rover_content['the_og_images'];

			if (isset($the_rover_content['the_meta_attribs']) && is_array($the_rover_content['the_meta_attribs']))
				{
				#	B O D Y  C L A S S
				if (isset($the_rover_content['the_meta_attribs']['body_class']) && !empty($the_rover_content['the_meta_attribs']['body_class']))
					{
					$this->rover_body_class_meta		= implode(' ', $the_rover_content['the_meta_attribs']['body_class']);
					}

				#	R O B O T S
				if (isset($the_rover_content['the_meta_attribs']['robots_noindex']) && ($the_rover_content['the_meta_attribs']['robots_noindex'] == 1))
					$robots[]					= 'noindex';
				if (isset($the_rover_content['the_meta_attribs']['robots_nofollow']) && ($the_rover_content['the_meta_attribs']['robots_nofollow'] == 1))
					$robots[]					= 'nofollow';
				if (isset($the_rover_content['the_meta_attribs']['robots_noarchive']) && ($the_rover_content['the_meta_attribs']['robots_noarchive'] == 1))
					$robots[]					= 'noarchive';

				if (count($robots))
					$this->rover_head['meta_robots']	= implode(', ', $robots);

				#	C A N O N I C A L  U R L
				if (isset($the_rover_content['the_meta_attribs']['canonical_url']) && !empty($the_rover_content['the_meta_attribs']['canonical_url']))
					$this->rover_head['canonical_url']	= $the_rover_content['the_meta_attribs']['canonical_url'];

				#	K E Y W O R D S
				if (isset($the_rover_content['the_meta_attribs']['keywords']) && !empty($the_rover_content['the_meta_attribs']['keywords']))
					$this->rover_head['meta_keywords']	= $the_rover_content['the_meta_attribs']['keywords'];
				}

			}
		}

	#
	#	Choose page_template for classic php-based themes.  Use pre_get_block_templates for block based themes
	#
	public function template_include($template)
		{

		if (wp_is_block_theme())
			return $template;

		$path_to_template						= array();
		$template_exists						= false;
		$html_fragment							= substr($this->rover_html, 0, 100);
		$pt_key									= null;

		$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'Component is '.$this->rover_component.' ['.$html_fragment.']');

		$page_template							= @$this->rover_idx->roveridx_theming['template'];

		if (in_array(
				$this->rover_component,
				array(
					'rover-control-panel',
					'rover-custom-listing-panel',
					'rover-login-panel'
					)
				)
			)
			{
			$path_to_template[]					= __DIR__ . '/templates/naked_page.php';
			}
		else if ((strcmp($page_template, 'rover-naked') === 0) || (is_array($_GET) && isset($_GET['print'])))
			{
			#	User is printing page - Retrieve stripped template from Rover
			$path_to_template[]					= __DIR__ . '/templates/naked_page.php';
			}
		else
			{
			if (!is_null($framework = $this->detect_framework_type($html_fragment)))
				{
				$pt_key							= $framework['key'];
				$path_to_template[]				= get_stylesheet_directory() . '/' . $this->rover_idx->roveridx_theming[$framework['template']];
				$path_to_template[]				= get_template_directory() . '/' . $this->rover_idx->roveridx_theming[$framework['template']];

				$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'Recommending template ['.$this->rover_idx->roveridx_theming['property_template'].']');
				}
			}

		if (
			$path_to_template == array() &&
			(isset($this->rover_idx->roveridx_theming['template']))	&&
			(!empty($this->rover_idx->roveridx_theming['template']))
			)
			{
			$path_to_template[]					= get_stylesheet_directory() . '/' . $this->rover_idx->roveridx_theming['template'];
			$path_to_template[]					= get_template_directory() . '/' . $this->rover_idx->roveridx_theming['template'];
			}

		foreach($path_to_template as $one_path)
			{
			if (file_exists($one_path))
				{
				/*	Success!	*/
				$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'Setting template to ['.$one_path.']');
				return $one_path;
				}
			}


		/*	Page templates are not set in Styling >> Quick Start.  Use default theme page template	*/

		$path_to_template						= null;
		$path_to_template						= get_page_template();

		if (!file_exists($path_to_template))
			{
			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'Specified page template ['.$path_to_template.'] does not exist.  Using default template ['.$template.'].');
			return $template;
			}

		$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'Using template ['.$path_to_template.']');

		if (!empty($path_to_template) && file_exists($path_to_template))
			{
			if (!is_null($pt_key))
				{
				#	We don't want to go down this path every time, just because the website designer
				#	hasn't selected a 'template' page.  So set it, and let them change it if they ever
				#	get around to it.

				if (!is_array($current_theme_options = get_option(roverOPTIONS::THEME)))
					$current_theme_options		= array();

				$current_theme_options['theme']	= 'unused';
				$current_theme_options[$pt_key]	= $path_to_template;

				update_option(roverOPTIONS::THEME, $current_theme_options );

				$this->rover_idx->roveridx_theming	= $current_theme_options;
				}

			return $path_to_template;
			}
		else
			{
			if (empty($path_to_template))
				$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'Fatal error: [path_to_template] is empty!');
			if (file_exists($path_to_template))
				$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'Fatal error: ['.$path_to_template.'] does not exist!');
			}

		return $template;
		}

	#
	#	Choose page_template for block based themes
	#

	public function pre_get_block_templates($block_templates, $query, $template_type)
		{

		$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'DEBUG_BLOCKTEMPLATE pre_get_block_templates ['.print_r($block_templates, true).'] / / template_type ['.$template_type.'] / query ['.print_r($query, true).']');

		if ( $template_type !== 'wp_template' ) {
			return $block_templates;
			}

		$html_fragment							= (!is_null($this->rover_html))
														? substr($this->rover_html, 0, 100)
														: '';
		$pt_key									= null;

		$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'DEBUG_BLOCKTEMPLATE Component is '.$this->rover_component.' ['.$html_fragment.']');

		if (!is_null($framework = $this->detect_framework_type($html_fragment)))
			{
			$pt_key								= $framework['key'];

			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'DEBUG_BLOCKTEMPLATE Recommending template ['.$framework['template'].']');
			}

		if (
			is_null($pt_key) &&
			(isset($this->rover_idx->roveridx_theming['template']))	&&
			(!empty($this->rover_idx->roveridx_theming['template']))
			)
			{
			$pt_key								= $this->rover_idx->roveridx_theming['template'];

			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'DEBUG_BLOCKTEMPLATE Recommending template ['.$this->rover_idx->roveridx_theming['template'].']');
			}

		if (!is_null($pt_key))
			{
			$registry							= WP_Block_Templates_Registry::get_instance();

			$template_id						= get_stylesheet() . '//' . $pt_key;

			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'DEBUG_BLOCKTEMPLATE Checking if template_id ['.$template_id.'] is registered');

			if ( $registry->is_registered( $template_id ) ) {

				$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'DEBUG_BLOCKTEMPLATE Returning template_id ['.$template_id.']');

				return [ $registry->get( $template_id ) ];
				}


			#	Check for database-based page templates
			$db_templates						= $this->get_db_block_templates();
			if ( isset( $db_templates[ $pt_key ] ) ) {
				$template						= $db_templates[ $pt_key ];
				// Convert to WP_Block_Template object if needed
				$template_obj					= new WP_Block_Template();
				$template_obj->id				= get_stylesheet() . '//' . $pt_key;
				$template_obj->slug				= $pt_key;
				$template_obj->theme			= get_stylesheet();
				$template_obj->type				= 'wp_template';
				$template_obj->status			= 'publish';
				$template_obj->source			= 'theme';
				$template_obj->path				= '';								# DB template has no file
				$template_obj->content			= $template->post_content;

				$this->rover_body_class_pt		= 'page-template-'.str_replace('page-template-', '', $pt_key);

				$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'DEBUG_BLOCKTEMPLATE Returning db-based page template ['.get_stylesheet() . '//' . $pt_key.']');

				return [ $template_obj ];
				}

			#	Check for file-based page templates
			$file_templates						= $this->get_file_block_templates();
			if ( isset( $file_templates[ $pt_key ] ) ) {
				$template						= $file_templates[ $pt_key ];

				$template_obj					= new WP_Block_Template();
				$template_obj->id				= get_stylesheet() . '//' . $pt_key;
				$template_obj->slug				= $pt_key;
				$template_obj->theme			= get_stylesheet();
				$template_obj->type				= 'wp_template';
				$template_obj->status			= 'publish';
				$template_obj->source			= 'theme';
				$template_obj->path				= get_stylesheet_directory() . '/templates/' . $pt_key . '.html';
				$template_obj->content			= $template->post_content;

				$this->rover_body_class_pt		= 'page-template-'.str_replace('page-template-', '', $pt_key);

				$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'DEBUG_BLOCKTEMPLATE Returning file-based page template ['.get_stylesheet() . '//' . $pt_key.']');

				return [ $template_obj ];
				}

			}

		return $block_templates;
		}

	private function redirect_if_necessary()
		{

		if (!empty($this->rover_redirect))
			{
			#	This is a non-active listing page, and a crawler is the requestor.
			#	We can redirect to the Home page, or a 404 page.  Simply doing nothing
			#	will fall through to the 404 page.

			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'rover_redirect is ['.$this->rover_redirect.'] ');

			if ($this->rover_redirect == "404")
				{
				return true;												#	Redirect to 404 page
				}
			else if ($this->rover_redirect == "home")
				{
				$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'Redirecting to '.get_site_url());

				wp_redirect( get_site_url(), 301 );							#	Redirect to 'Home' page
				exit;
				}
			else 		#	specific
				{
				$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'Redirecting to '.$this->rover_redirect);

				$redirect_type				= (strpos($this->rover_redirect, "rover-login-panel") !== false)
													? 302
													: 301;

				wp_redirect( $this->rover_redirect, $redirect_type );		#	Redirect to specific page
				exit;
				}
			}

		return false;
		}

	private function region_is_valid($atts)	{

		if (!isset($atts['region']))
			return false;
		else if (empty($atts['region']))
			return false;

		return true;
		}

	private function translate_component($component)	{


		$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, ' ['.$component.']');

		if ($component == 'ROVER_COMPONENT_404')
			{
			#	For certain types of pages, skip the 404 engine

			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, ' Comparing ['.$this->rover_idx->rover_404_slugs.'] with [agent-detail]');
			if ($this->rover_idx->rover_404_slugs == 'agent-detail')
				{
				$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, ' ['.$this->rover_idx->rover_404_slugs.'] returning [ROVER_COMPONENT_AGENT_DETAIL_PAGE]');
				return 'ROVER_COMPONENT_AGENT_DETAIL_PAGE';
				}
			}

		return $component;
		}

	private function formatted_debug()		{


		if (is_array($this->rover_idx->debug_html) && count($this->rover_idx->debug_html))
			{
			return '<div class="rover-debug-html" style="display:none;"><div>'.implode('<br>', $this->rover_idx->debug_html).'</div></div>';
			}

		return null;
		}

	public function rover_content($component, $atts = null)	{

		global									$post, $wp_version;

		if ($this->rover_idx->COMMON->is_debuggable())
			{
			error_log('-------------------------------------------------------------------');
			}

		$rover_content							= null;
		$page									= (isset($post))
														? $post->ID
														: $this->rover_idx->COMMON->get_rover_post_id();
		$api_key								= $this->get_api_key();
		$post_str								= null;

		$vars_array								= array(
													'component'			=>	$this->translate_component($component),
													'is_wp'				=>	true,
													'signature'			=>	'67d14e7729d3a8446ebf5e5e97f684db',
													'cookies'			=>	$this->cookies(),
													'domain_id'			=>	$this->rover_idx->roveridx_regions['domain_id'],
													'domain'			=>	get_site_url(),
													'page'				=>	$page,
													'api_key'			=>	$api_key,
													'user_agent'		=>	((isset($_SERVER['HTTP_USER_AGENT'])) ? $_SERVER['HTTP_USER_AGENT'] : null),
													'user_ip'			=>	((isset($_SERVER['REMOTE_ADDR'])) ? $_SERVER['REMOTE_ADDR'] : null),
													'server_ip'			=>	((isset($_SERVER['SERVER_ADDR'])) ? $_SERVER['SERVER_ADDR'] : null),
													'path_url'			=>	$this->rover_idx->curr_path,
													'query_url'			=>	http_build_query($_REQUEST),
													'force_crawler'		=>	intval(@$_GET['crawler']),					#	'?crawler=1'
													'wp_permalinks'		=>	get_option('permalink_structure'),
													'version_php'		=>	phpversion(),
													'version_rover'		=>	roverVERSIONS::FULL,
													'version_wp'		=>	$wp_version,
													);

		if (!$this->fetching_api_key)
			{
			if (
				is_null($api_key) &&
				isset(self::FRONT_COMPONENTS[$component])
				)
				{
				return $this->content_display_error(
						$data_attribs			= 'data-missing_api_key="true"',
						$error_text				= 'Important settings are missing from this installation.  Please go to General > Advanced and press the Reset button.'
						);
				}

			if (
				!$this->region_is_valid($atts)
				)
				{
				$perform_region_rebuild			= false;

				if (count($this->rover_idx->all_selected_regions) === 0)
					{
					if (isset(self::FRONT_COMPONENTS[$component]))
						{
						$perform_region_rebuild	= true;
						}
					}
				else
					{
					$atts['region']				= implode(',', array_keys($this->rover_idx->all_selected_regions));

					if (empty($atts['region']))
						{
						$perform_region_rebuild	= true;
						$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, '`region` missing - performing rebuild_regions()');
						}
					else
						{
						$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'no `region` specified - using all_selected_regions ['.$atts['region'].']');
						}
					}

				if ($perform_region_rebuild)
					{
					$this->rebuild_regions();

					return $this->content_display_error(
							$data_attribs		= 'data-missing_region="true"',
							$error_text			= 'Important settings are missing from this installation.  Please go to General > Advanced and press the Reset button.'
							);
					}
				}
			}

		if ( is_user_logged_in() )
			{
			$current_user						= wp_get_current_user();
			$guid								= get_user_meta($current_user->ID, 'rover_guid', $single = true);
			if (!empty($guid))
				$vars_array['guid']				= $guid;
			}

		$atts									= (is_array($atts))
														? array_merge($atts, $vars_array)
														: $vars_array;

		if (isset($_GET[roverKEYS::DEBUG_KEY]) === true)
			{
			$atts[roverKEYS::DEBUG_KEY]				= intval($_GET[roverKEYS::DEBUG_KEY]);
			}

		if ($this->rover_idx->COMMON->is_debuggable())
			{
			if (is_array($atts))
				{
				foreach ($atts as $atts_key => $atts_val)
					{
					$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, $atts_key.' => '.print_r($atts_val, true));
					}
				}

#			$btd = debug_backtrace();
#			$btd_str = null;
#			foreach ($btd as $btdKey => $btdVal)
#				$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, '['.$btdKey.'] File: '.$btdVal['file'].' / Function: '.$btdVal['function'].' / Line: '.$btdVal['line']);
			}

		if ($this->is_local_component($component))
			{
			$ret_data							= $this->local_content($component);
			}
		else
			{
			$post_str							= http_build_query($atts);

			$url								= 'https://ep4.roveridx.com/php/request.php';

			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, $url.PHP_EOL.$post_str);

#			if ($this->test_for_modsec($post_str))
#				{
#				return $this->content_display_error(
#						$data_attribs			= 'data-modsec="true"',
#						$error_text				= 'This request appears to be an attempt at SQL injection attacks, cross-site scripting, or a path traversal attacks.'
#						);
#				}

			$curl_timeout						= (in_array($component, array('rover-seo-regenerate-sitemap', 'rover-report-panel')))
														? 120
														: 60;

			$curl_start							= microtime(true);

			if ($this->curl_handle === null)
				$this->curl_handle				= curl_init();

			$ch_opts							= array(
														CURLOPT_URL				=> $url,
														CURLOPT_RETURNTRANSFER	=> true,
														CURLOPT_CONNECTTIMEOUT	=> 30,

														CURLOPT_TIMEOUT			=> $curl_timeout,
														CURLOPT_HTTPHEADER		=> array(
																						'Content-Type: application/x-www-form-urlencoded',
																						'Content-Length: '.strlen($post_str),
																						'Connection: keep-alive'
																						),
														CURLOPT_POST			=> true,
														CURLOPT_POSTFIELDS		=> $post_str,
														CURLOPT_FAILONERROR		=> true
														);

#			Most servers only support compression in responses, not requests.
#
#			if (strlen($post_str) > 1024) {			#	Only compress if > 1KB
#				$post_str_compressed 				= gzencode($post_str);
#				if ($post_str_compressed && strlen($post_str_compressed) < strlen($post_str))	{
#					$ch_opts[CURLOPT_POSTFIELDS]	= $post_str_compressed;
#					$ch_opts[CURLOPT_HTTPHEADER][]	= 'Content-Encoding: gzip';
#					$post_str						= $post_str_compressed;
#					}
#				}

			#	Use HTTP/2 if available, fall back to HTTP/1.1
			if (defined('CURL_HTTP_VERSION_2_0')) {
				$ch_opts[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
				}
			else if (defined('CURL_HTTP_VERSION_1_1')) {
				$ch_opts[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
				}

#			Ticket #: 11553745
#			Even with CURLOPT_TCP_FASTOPEN defined, we do not know if net.ipv4.tcp_fastopen is enabled

#			if (defined('CURLOPT_TCP_FASTOPEN'))
#				{
#				$fastopen_enabled				= ini_get('net.ipv4.tcp_fastopen');
#
#				if (intval($fastopen_enabled) > 0)
#					{
#					rover_idx_error_log(__FILE__, __FUNCTION__, __LINE__, 'net.ipv4.tcp_fastopen is '.$fastopen_enabled);
#					$ch_opts[CURLOPT_TCP_FASTOPEN]	= 1;
#					}
#				}

#			if (defined('CURLOPT_RESOLVE'))
#				{
#				if (($curr_endpoint_resolve = $this->get_endpoint_resolve()) !== false) {
#					if (strpos($curr_endpoint_resolve, ":") !== false)
#						$ch_opts[CURLOPT_RESOLVE]	= array($curr_endpoint_resolve);
#					}
#				}

			#	https://php.watch/articles/curl-php-accept-encoding-compression
			#	https://www.brotli.pro/
			#	https://zachrussell.net/tools/brotli-test/
			#	Ticket #17089153 - Litespeed settings blocking connection to plugin

			if (defined('CURLOPT_ENCODING'))
				{
				#	Ticket #: 17089153
				#	Error # 23 - "Unable to communicate with server. Failed writing received data to disk/application"

				$ch_opts[CURLOPT_ENCODING]		= (isset($this->rover_idx->roveridx_regions['forced_encoding']))
														? $this->rover_idx->roveridx_regions['forced_encoding']
														: "";		#	deflate, gzip, br, zstd
				}

			if (defined('CURLOPT_IPRESOLVE') && defined('CURL_IPRESOLVE_V4'))
				$ch_opts[CURLOPT_IPRESOLVE]		= CURL_IPRESOLVE_V4;

			curl_reset($this->curl_handle);
			curl_setopt_array( $this->curl_handle, $ch_opts );

			$ret_data							= curl_exec($this->curl_handle);
			$curl_errno							= curl_errno($this->curl_handle);
			$curl_error							= curl_error($this->curl_handle);

			$curl_timers						= curl_getinfo($this->curl_handle);

			$curl_stop							= microtime(true);
			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'curl took ['.floatval(round($curl_stop - $curl_start, 5)).'] seconds');

			if ($curl_errno > 0)
				{
				return $this->return_communication_error($curl_errno, $curl_error);
				}
			}

		try {
			$rover_content						= json_decode($ret_data, true);

			if (!is_null($this->dynamic_sidebar))
				$rover_content['the_html']		= str_replace('ROVER_DYNAMIC_SIDEBAR', $this->dynamic_sidebar, $rover_content['the_html']);

			$this->rover_head['og_images']		= null;
			if (isset($rover_content['the_og_images']) && !empty($rover_content['the_og_images']))
				$this->rover_head['og_images']	= $rover_content['the_og_images'];

#			$this->dump_curl_timers($curl_timers);

			if ($this->rover_idx->COMMON->is_debuggable())
				{
				if (
					is_array($rover_content) &&
					isset($rover_content['the_html']) &&
					is_string($rover_content['the_html']) &&
					(($the_html_len = strlen($rover_content['the_html'])) > 0)
					)
					{
					$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'the_html is '.$the_html_len.' bytes');
					if (isset($rover_content['the_og_images']))
						$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'the_og_images are '.strlen($rover_content['the_og_images']).' bytes');
					}
				else
					{
					$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'the_html is zero bytes');
					}
				}

			$this->check_js_version($rover_content);
			$this->check_endpoint_resolve($rover_content);
			}
		catch (JsonException $e) {
			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'json_decode() failed: ' . $e->getMessage());

			$this->add_content_error('['.$component.'] json_decode('.__LINE__.') failed: ' . $e->getMessage());
			$this->add_content_error('<div data-post_str>'.$post_str.'</div>');

			if ($this->is_local_component($component)) {
				$this->add_content_error('<div>is_local_component()</div>');
				}

			$rover_content = array();
			$rover_content['the_html'] = $ret_data;
			}

		return $rover_content;
		}

	public function content_display_error($data_attribs, $error_text)
		{
		return array(
				'the_html'			=> '<div style="color:red;margin:40px auto;text-align:center;" '.$data_attribs.'>'.$error_text.'</div>'
				);
		}

	private function add_curl_content_error($opt)
		{

		$this->rover_idx->debug_html[]				= '<div data-'.$opt.'>'.$opt.' is not defined on this server</div>';
		}

	private function add_content_error($str)
		{

		$this->rover_idx->debug_html[]				= '<div data-str>'.$str.'</div>';
		}

	private function dump_curl_timers($curl_timers)
		{

		if ($this->rover_idx->COMMON->is_debuggable())
			{
			foreach($curl_timers as $curl_key => $curl_val)
				{
				$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'curl_timers ['.$curl_key.'] => ['.((is_array($curl_val)) ? implode(',', $curl_val): $curl_val).'] seconds');
				}
			}

		if ($this->rover_idx->COMMON->is_debuggable())
			{
			$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'curl_timer summary ********');

			$the_sum							= 0;
			foreach(array('namelookup_time_us', 'connect_time_us', 'appconnect_time_us', 'pretransfer_time_us', 'redirect_time_us', 'starttransfer_time_us') as $key)
				{
				$the_time						= (isset($curl_timers[$key]))
														? ($curl_timers[$key] / 1000000)
														: 0;
				$the_sum						+= $the_time;

				$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'curl_timers ['.str_pad($key, 22).'] => ['.str_pad($the_time, 8).'] seconds');
				}

			if (isset($curl_timers['total_time_us']))
				{
				$total_time_us					= $curl_timers['total_time_us'] / 1000000;
				$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'curl_timers ['.str_pad('total_time_us', 22).'] => ['.str_pad($total_time_us, 8).'] seconds');
				$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'curl_timers ['.str_pad('*_time_us adds up to', 22).'] => ['.str_pad($the_sum, 8).'] seconds');
				$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'curl_timers ['.str_pad('unaccounted for', 22).'] => ['.str_pad($total_time_us - $the_sum, 8).'] seconds');
				}
			}
		}

	private function cookies()
		{
		$the_cookies							= array();

		if (is_array($_COOKIE) && count($_COOKIE))
			{
			$rover_cookie_key					= 'rover_';
			$len								= strlen($rover_cookie_key);
			foreach ($_COOKIE as $key => $value)
				{
				$sub							= substr($key, 0, $len);
				if (strcasecmp($rover_cookie_key, $sub) === 0)
					{
					$the_cookies[]				= $key.'='.urlencode($value);
					}
				}
			}

#		$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, ' is '.implode(';', $the_cookies));

		return implode(';', $the_cookies);
		}

	private function return_communication_error($curl_errno, $curl_error)	{


		$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'curl_exec error ['.$curl_errno.']: '.$curl_error);

		$the_html								= array();
		$the_html[]								= '<div style="color:red;margin:40px auto;text-align:center;">';
		$the_html[]								=	'<svg style="height:100px;width:100px;display:block;margin:0 auto;text-align:center;" aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" class="bolt">';
		$the_html[]								=		'<path fill="currentColor" d="M377.8 167.9c-8.2-14.3-23.1-22.9-39.6-22.9h-94.4l28.7-87.5c3.7-13.8.8-28.3-7.9-39.7C255.8 6.5 242.5 0 228.2 0H97.7C74.9 0 55.4 17.1 52.9 37.1L.5 249.3c-1.9 13.8 2.2 27.7 11.3 38.2C20.9 298 34.1 304 48 304h98.1l-34.9 151.7c-3.2 13.7-.1 27.9 8.6 38.9 8.7 11.1 21.8 17.4 35.9 17.4 16.3 0 31.5-8.8 38.8-21.6l183.2-276.7c8.4-14.3 8.4-31.5.1-45.8zM160.1 457.4L206.4 256H47.5L97.7 48l127.6-.9L177.5 193H334L160.1 457.4z" class=""></path>';
		$the_html[]								=	'</svg>';
		$the_html[]								=	'['.$curl_errno.'] - Unable to communicate with server.  '.curl_strerror($curl_errno).'<br><br>';
		$the_html[]								=	'<div style="display:none">';
		$the_html[]								=		'<div class="php-version">'.print_r(phpversion(), true).'</div>';
		$the_html[]								=		'<div class="php-info">'.ob_start();phpinfo();print(ob_end_clean()).'</div>';
		$the_html[]								=		'<div class="curl-version">'.print_r(curl_version(), true).'</div>';
		$the_html[]								=	'</div>';
		$the_html[]								= '</div>';

		if ($curl_errno == '23')
			{
			#	Ticket #: 17089153
			#	Error # 23 - "Unable to communicate with server. Failed writing received data to disk/application"
			#
			#	This webhost does not correctly support brotli, even though they passed:
			#		'Accept-Encoding:		gzip, deflate, br, zstd'
			#	And our LiteSpeed server wants to encode using brotli
			#
			#	BlueHost seems to be a common host with this issue.
			#
			#	So we will retry, but save a setting that this cause this site to force 'deflate' as the CURLOPT_ENCODING

			if (is_array($rr = get_option(roverOPTIONS::REGIONS)))
				{
				if (!isset($rr['forced_encoding']))
					{
					$rr['forced_encoding']		= 'deflate';

					update_option(roverOPTIONS::REGIONS, $rr);

					$this->rover_idx->roveridx_regions	= get_option(roverOPTIONS::REGIONS);
					}
				}
			}

		return array(
					'the_html'	=> implode('', $the_html)
					);
		}

	private function is_local_component($component)		{

		if (in_array(
					$component,
					array(
						'rover-debug-page'
						)
					))
			{
			return true;		#	Force 'remote' for WP Plugin setup panels
			}

		return false;
		}

	private function local_content($component)		{


		$the_html								= array();
		$the_title								= null;
		$the_meta_desc							= null;
		$the_meta_attribs						= null;

		switch($component)
			{
			case 'rover-debug-page':
				$the_title						= 'Rover Debug Page';

				$the_html[]						= '<h3>Region settings</h3>';
				foreach($this->rover_idx->roveridx_regions as $key => $val)
					$the_html[]					= '['.$key.'] => ['.$val.']';

				$the_html[]						= '<h3>Theme settings</h3>';
				foreach($this->rover_idx->roveridx_theming as $key => $val)
					$the_html[]					= '['.$key.'] => ['.$val.']';

				$the_html[]						= '<h3>Regions</h3>';
				foreach($this->rover_idx->all_selected_regions as $key => $val)
					$the_html[]					= '['.$key.'] => ['.$val.']';

				$curr_theme						= wp_get_theme();
				$the_html[]						= '<h3>Templates for ['.$curr_theme->get('Name').'] ['.$curr_theme->get('ThemeURI').'] ['.$curr_theme->get('Version').']</h3>';
				foreach($curr_theme->get_page_templates() as $key => $val)
					$the_html[]					= '['.$key.'] => ['.$val.']';
				break;
			}

		return json_encode( array(
					'the_html'					=> '<div style="margin:100px 20%;">'.implode('<br>', $the_html).'</div>',
					'the_title'					=> $the_title,
					'the_meta_desc'				=> $the_meta_desc,
					'the_meta_attribs'			=> $the_meta_attribs
					));
		}

	private function test_for_modsec($post_str)
		{

		if (false)
			{
		/*	Test for modsec	*/

			#	This method is not secure

			#	Attackers can bypass patterns like this by injecting:
			#		comments: UN/**/ION SELECT
			#		obfuscation: SeLeCt
			#		encoding: %55%4e%49%4f%4e
			#		backticks or quotes
			#		alternative SQL verbs

			#	Regex is MUCH slower than simple substring checks.

			#		Your pattern contains multiple capture groups
			#		Alternation (|)
			#		Character classes
			#		Dot-star .* greedy matches → worst-case backtracking
			#		A single preg_match() on a long string can take hundreds of microseconds.
			#		A single str_contains() check on the same string takes <1 microsecond.
			#		slower than simple string checks by ~10× to 50×

			$pattern							= "(insert[[:space:]]+into.+values|select.*from.+[a-z|A-Z|0-9]|select.+from|bulk[[:space:]]+insert|union.+select|convert.+\\\\(.*from))";

			if (preg_match($pattern, $post_str) == 1)
				{
				$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'security alert!');
				return true;

				wp_mail("info@roveridx.com",
						get_site_url().': post_str will trigger modsec',
						$post_str);
				}
			}
		else
			{
			$post_str							= strtolower($post_str);
			$blacklist							= [
												'insert into',
												'select ',
												' union ',
												' bulk insert ',
												' convert(',
												];

			foreach ($blacklist as $needle) {
				$pos							= strpos($post_str, $needle);
				if ($pos !== false)
					{
					if ($needle === 'select ')
						{
						if ($this->modsec_investigate_select($post_str, $pos))
							{
							$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'security alert!');
							return true;		#	suspicious
							}
						}
					else
						{
						$this->rover_idx->COMMON->error_log(__FILE__, __FUNCTION__, __LINE__, 'security alert!');
						return true;			#	suspicious
						}
					}
				}
			}

		return false;
		}

	private function get_db_block_templates( $template_type = 'wp_template' ) {

		$posts = get_posts([
			'post_type'							=> $template_type,
			'posts_per_page'					=> -1,
			'post_status'						=> 'any',
			'suppress_filters'					=> true,				#	ensures no hooks run that modify The Loop.
			'fields'							=> 'all',				#	can be 'ids' if you only need IDs
			]);

		$templates								= [];
		foreach ( $posts as $tpl ) {
			$templates[ $tpl->post_name ] = $tpl;
			}

		return $templates;
		}

	#	Look for page templates in wp-content/themes/<theme>/templates/*.html
	private function get_file_block_templates() {

		$dir									= get_stylesheet_directory() . '/templates/';
		$templates								= [];

		if ( !is_dir( $dir ) ) {
			return $templates;
		}

		foreach ( glob( $dir . '*.html' ) as $file ) {
			$slug								= basename( $file, '.html' );
			$templates[ $slug ]					= $file;
			}

		return $templates;
		}

	private function detect_framework_type($html_fragment) {

		$frameworks								= [
												'idx-prop-detail-framework'			=> ['key' => 'property_template',		'template' => 'property_template'],
												'rover-market-conditions-framework'	=> ['key' => 'mc_template',				'template' => 'mc_template'],
												'rover-report-framework'			=> ['key' => 'rep_template',			'template' => 'rep_template'],
												'rover-agent-framework'				=> ['key' => 'agent_detail_template',	'template' => 'agent_detail_template'],
												];

		foreach ($frameworks as $framework => $config) {
			if (strpos($html_fragment, $framework) !== false) {

				if (
					(isset($this->rover_idx->roveridx_theming[$config['template']]))	&&
					(!empty($this->rover_idx->roveridx_theming[$config['template']]))
					)
					{
					return $config;
					}
				}
			}

		return null;
		}

	private function modsec_investigate_select($post_str, $pos)
		{
		$after									= substr($post_str, $pos + 6, 50); 	# look at 50 chars after 'select'

		#	Cheap pattern check
		if (
			(trim($after)[0] ?? '') === '*' ||
			preg_match('/^[a-z0-9_,\s]+/i', $after)
			)
			{
			#	 What this catches
			#		select * from users
			#		select id,name from table
			#		select 1,2,3
			#		select username from accounts
			#
			#	What it ignores
			#		harmless words like:
			#		"select your gender"
			#		"please select a file"

			return true;
			}

		return false;
		}
	}

?>