<?php
/**
 * Sitemap class.
 *
 * @package All_in_One_SEO_Pack
 * @version 2.3.13
 */

if ( ! class_exists( 'All_in_One_SEO_Pack_Sitemap' ) ) {

	/**
	 * Class All_in_One_SEO_Pack_Sitemap
	 *
	 * @since ?
	 * @since 2.4 Include images in sitemap.
	 */
	class All_in_One_SEO_Pack_Sitemap extends All_in_One_SEO_Pack_Module {
		/**
		 * Cache Struct (Strict|Structure)
		 *
		 * Is set by `get_option( 'permalink_structure' )`.
		 *
		 * @since ?
		 * @var array
		 */
		public $cache_struct = null;

		/**
		 * Is set by `get_option( 'home' )`. Seems the equivalent of `home_url()`.
		 *
		 * @since ?
		 * @var string?
		 */
		public $cache_home = null;

		/**
		 * (Plugin) Comment String
		 *
		 * A Label String to add to the sitemap to indicate AIOSEOP generated the sitemap.
		 *
		 * @todo Set value could be moved from the constructor to the variable's default/initial value;
		 *
		 * @since ?
		 * @var string
		 */
		public $comment_string;

		/**
		 * Start Memory Usage
		 *
		 * Is set by PHP's `memory_get_peak_usage()`.
		 *
		 * @since ?
		 * @var int
		 */
		public $start_memory_usage = 0;

		/**
		 * Max Posts
		 *
		 * For Sitemap setting "Maximum Posts per Sitemap Page" when "Enable Sitemap Indexes" is checked/enabled.
		 *
		 * @since ?
		 * @var int
		 */
		public $max_posts = 50000;

		/**
		 * Priority
		 *
		 * @todo Needs more detail.
		 *
		 * @since ?
		 * @var array
		 */
		public $prio;

		/**
		 * Priority Selected
		 *
		 * @todo Needs more detail.
		 *
		 * @since ?
		 * @var array
		 */
		public $prio_sel;

		/**
		 * Frequency
		 *
		 * @todo Needs more detail.
		 *
		 * @since ?
		 * @var array
		 */
		public $freq;

		/**
		 * Frequency Selected
		 *
		 * @todo Needs more detail.
		 *
		 * @since ?
		 * @var array
		 */
		public $freq_sel;

		/**
		 * Extra Sitemaps
		 *
		 * @todo Needs more detail.
		 *
		 * @since ?
		 * @var mixed|void
		 */
		public $extra_sitemaps;

		/**
		 * Excludes
		 *
		 * Excludes slugs and IDs.
		 *
		 * @todo Needs more detail. Does this include all Categories and Pages/Posts? ...and what about Tags?
		 *
		 * @since ?
		 * @var array
		 */
		public $excludes = array();

		/**
		 * Image Extensions.
		 *
		 * The allowed image extensions.
		 *
		 * @var array $image_extensions The allowed image extensions.
		 */
		private static $image_extensions = array(
			'jpg',
			'jpeg',
			'png',
			'gif',
		);

		/**
		 * Image IDs => URLs
		 *
		 * @since 2.11
		 *
		 * @var null|array $image_ids_urls {
		 *     @type string $base_url The URL for the original sized image.
		 *     @type string ${$id}    Contains the URLs associated to the IDs.
		 * }
		 */
		private $image_ids_urls = null;

		/**
		 * All_in_One_SEO_Pack_Sitemap constructor.
		 *
		 * @since ?
		 */
		public function __construct() {
			if ( get_class( $this ) === 'All_in_One_SEO_Pack_Sitemap' ) { // Set this up only when instantiated as this class.
				$this->name           = __( 'XML Sitemap', 'all-in-one-seo-pack' ); // Human-readable name of the plugin.
				$this->prefix         = 'aiosp_sitemap_';                          // Option prefix.
				$this->file           = __FILE__;                                      // The current file.
				$this->extra_sitemaps = array();
				$this->extra_sitemaps = apply_filters( $this->prefix . 'extra', $this->extra_sitemaps );
			}
			parent::__construct();
			// TODO This could be move up to the class field default/initial values.
			$this->comment_string = 'Sitemap %s generated by ' . AIOSEOP_PLUGIN_NAME . ' %s by Michael Torbert of Semper Fi Web Design on %s';

			$this->default_options = array(
				'rss_sitemap' => array( 'name' => __( 'Create RSS Sitemap', 'all-in-one-seo-pack' ) ),
				'daily_cron'  => array(
					'name'            => __( 'Schedule Updates', 'all-in-one-seo-pack' ),
					'type'            => 'select',
					'initial_options' => array(
						0         => __( 'No Schedule', 'all-in-one-seo-pack' ),
						'daily'   => __( 'Daily', 'all-in-one-seo-pack' ),
						'weekly'  => __( 'Weekly', 'all-in-one-seo-pack' ),
						'monthly' => __( 'Monthly', 'all-in-one-seo-pack' ),
					),
					'default'         => 0,
				),
				'indexes'     => array(
					'name'    => __( 'Enable Sitemap Indexes', 'all-in-one-seo-pack' ),
					'default' => 'on',
				),
				'max_posts'   => array(
					'name'     => __( 'Maximum Posts Per Sitemap Page', 'all-in-one-seo-pack' ),
					'type'     => 'text',
					'default'  => 1000,
					'condshow' => array(
						"{$this->prefix}indexes" => 'on',
						"{$this->prefix}indexes" => 'on',
					),
				),
				'posttypes'   => array(
					'name'    => __( 'Post Types', 'all-in-one-seo-pack' ),
					'type'    => 'multicheckbox',
					'default' => 'all',
				),
				'taxonomies'  => array(
					'name'    => __( 'Taxonomies', 'all-in-one-seo-pack' ),
					'type'    => 'multicheckbox',
					'default' => 'all',
				),
				'archive'     => array( 'name' => __( 'Include Date Archive Pages', 'all-in-one-seo-pack' ) ),
				'author'      => array( 'name' => __( 'Include Author Pages', 'all-in-one-seo-pack' ) ),
				'images'      => array( 'name' => __( 'Exclude Images', 'all-in-one-seo-pack' ) ),
				'rewrite'     => array(
					'name'    => __( 'Dynamically Generate Sitemap', 'all-in-one-seo-pack' ),
					'default' => 'On',
				),
			);

			$status_options = array(
				'link' => array(
					'default' => '',
					'type'    => 'html',
					'label'   => 'none',
					'save'    => false,
				),
			);

			$this->layout = array(
				'status'  => array(
					'name'      => __( 'Sitemap Status', 'all-in-one-seo-pack' ),
					'help_link' => 'https://semperplugins.com/documentation/xml-sitemaps-module/',
					'options'   => array_keys( $status_options ),
				),
				'default' => array(
					'name'      => $this->name,
					'help_link' => 'https://semperplugins.com/documentation/xml-sitemaps-module/',
					'options'   => array_keys( $this->default_options ),
				),
			);

			$prio = array();
			for ( $i = 0; $i <= 10; $i ++ ) {
				$str          = sprintf( '%0.1f', $i / 10.0 );
				$prio[ $str ] = $str;
			}
			$arr_no         = array( 'no' => __( 'Do Not Override', 'all-in-one-seo-pack' ) );
			$arr_sel        = array( 'sel' => __( 'Select Individual', 'all-in-one-seo-pack' ) );
			$this->prio_sel = array_merge( $arr_no, $arr_sel, $prio );
			$this->prio     = array_merge( $arr_no, $prio );

			$freq = array();
			foreach ( array( 'always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never' ) as $f ) {
				$freq[ $f ] = $f;
			}
			$this->freq_sel = array_merge( $arr_no, $arr_sel, $freq );
			$this->freq     = array_merge( $arr_no, $freq );

			foreach (
				array(
					'prio' => __( 'priority', 'all-in-one-seo-pack' ),
					'freq' => __( 'frequency', 'all-in-one-seo-pack' ),
				) as $k => $v
			) {
				$s  = "{$k}_options";
				$$s = array();
				foreach (
					array(
						'homepage'   => __( 'homepage', 'all-in-one-seo-pack' ),
						'post'       => __( 'posts', 'all-in-one-seo-pack' ),
						'taxonomies' => __( 'taxonomies', 'all-in-one-seo-pack' ),
						'archive'    => __( 'archive pages', 'all-in-one-seo-pack' ),
						'author'     => __( 'author pages', 'all-in-one-seo-pack' ),
					) as $opt => $val
				) {
					$arr = $$s;
					if ( ( 'post' === $opt ) || ( 'taxonomies' === $opt ) ) {
						$iopts = $this->{"{$k}_sel"};
					} else {
						$iopts = $this->$k;
					}

					$arr[ $k . '_' . $opt ] = array(
						'name'            => $this->ucwords( $val ),
						'type'            => 'select',
						'initial_options' => $iopts,
						'default'         => 'no',
					);
					if ( ( 'archive' === $opt ) || ( 'author' === $opt ) ) {
						$arr[ $k . '_' . $opt ]['condshow'] = array( $this->prefix . $opt => 'on' );
					}
					$$s = $arr;
				}
			}

			$addl_options = array(
				'addl_instructions' => array(
					'default' => '<div>' . __( 'Enter information below for any additional links for your sitemap not already managed through WordPress.', 'all-in-one-seo-pack' ) . '</div><br />',
					'type'    => 'html',
					'label'   => 'none',
					'save'    => false,
				),
				'addl_url'          => array(
					'name' => __( 'Page URL', 'all-in-one-seo-pack' ),
					'type' => 'url',
					'save' => false,
				),
				'addl_prio'         => array(
					'name'            => __( 'Page Priority', 'all-in-one-seo-pack' ),
					'type'            => 'select',
					'initial_options' => $prio,
					'save'            => false,
				),
				'addl_freq'         => array(
					'name'            => __( 'Page Frequency', 'all-in-one-seo-pack' ),
					'type'            => 'select',
					'initial_options' => $freq,
					'save'            => false,
				),
				'addl_mod'          => array(
					'name'        => __( 'Last Modified', 'all-in-one-seo-pack' ),
					'type'        => 'date',
					'save'        => false,
					'placeholder' => 'yyyy-mm-dd',
					'class'       => 'aiseop-date',
				),
				'addl_pages'        => array(
					'name' => __( 'Additional Pages', 'all-in-one-seo-pack' ),
					'type' => 'custom',
					'save' => true,
				),
				'Submit'            => array(
					'type'  => 'submit',
					'class' => 'button-primary',
					'name'  => __( 'Add URL', 'all-in-one-seo-pack' ) . ' &raquo;',
					'style' => 'margin-left: 20px;',
					'label' => 'none',
					'save'  => false,
					'value' => 1,
				),
			);

			$excl_options = array(
				'excl_terms' => array(
					'name'  => __( 'Excluded Terms', 'all-in-one-seo-pack' ),
					'type'  => 'multiselect',
					'class' => 'aioseop-exclude-terms',
				),
				'excl_pages' => array(
					'name' => __( 'Excluded Pages', 'all-in-one-seo-pack' ),
					'type' => 'text',
				),
			);

			$this->layout['addl_pages'] = array(
				'name'      => __( 'Additional Pages', 'all-in-one-seo-pack' ),
				'help_link' => 'https://semperplugins.com/documentation/xml-sitemaps-module/#additional-pages',
				'options'   => array_keys( $addl_options ),
			);

			$this->layout['excl_pages'] = array(
				'name'      => __( 'Excluded Items', 'all-in-one-seo-pack' ),
				'help_link' => 'https://semperplugins.com/documentation/xml-sitemaps-module/#excluded-items',
				'options'   => array_keys( $excl_options ),
			);

			$this->layout['priorities'] = array(
				'name'      => __( 'Priorities', 'all-in-one-seo-pack' ),
				'help_link' => 'https://semperplugins.com/documentation/xml-sitemaps-module/#priorities-and-frequencies',
				// TODO Fix undefined variable.
				'options'   => array_keys( $prio_options ),
			);

			$this->layout['frequencies'] = array(
				'name'      => __( 'Frequencies', 'all-in-one-seo-pack' ),
				'help_link' => 'https://semperplugins.com/documentation/xml-sitemaps-module/#priorities-and-frequencies',
				// TODO Fix undefined variable.
				'options'   => array_keys( $freq_options ),
			);

			$this->default_options = array_merge( $status_options, $this->default_options, $addl_options, $excl_options, $prio_options, $freq_options );

			add_action(
				'after_doing_aioseop_updates',
				array(
					$this,
					'do_sitemaps',
				)
			); // Update static sitemap when AIOSEOP is upgrade to new version.
			add_action( 'init', array( $this, 'load_sitemap_options' ) );
			add_action( $this->prefix . 'settings_update', array( $this, 'do_sitemaps' ) );
			add_filter( $this->prefix . 'display_settings', array( $this, 'update_post_data' ) );
			add_filter( $this->prefix . 'display_options', array( $this, 'filter_display_options' ) );
			add_filter( $this->prefix . 'update_options', array( $this, 'filter_options' ) );
			add_filter( $this->prefix . 'output_option', array( $this, 'display_custom_options' ), 10, 2 );
			add_action( $this->prefix . 'daily_update_cron', array( $this, 'daily_update' ) );
			add_action( 'init', array( $this, 'make_dynamic_xsl' ) );

			// TODO is this required for dynamic sitemap?
			add_action( 'transition_post_status', array( $this, 'update_sitemap_from_posts' ), 10, 3 );
			add_action( 'after_doing_aioseop_updates', array( $this, 'scan_sitemaps' ) );
			add_action( 'admin_init', array( $this, 'sitemap_notices' ) );
		}

		/**
		 * Sitemap Notices
		 *
		 * @todo Move admin notice functions. Possibly to where it is first saved & loaded (`load_sitemap_options`).
		 *
		 * @global AIOSEOP_Notices $aioseop_notices
		 *
		 * @since 2.4.1
		 */
		public function sitemap_notices() {
			if ( ! current_user_can( 'aiosp_manage_seo' ) ) {
				return;
			}

			global $aioseop_notices;
			$options = $this->options;

			if (
					(
							isset( $options[ "{$this->prefix}indexes" ] ) &&
							'on' !== $options[ "{$this->prefix}indexes" ]
					) ||
					(
							isset( $options[ "{$this->prefix}indexes" ] ) &&
							'on' === $options[ "{$this->prefix}indexes" ] &&
							1000 < $options[ "{$this->prefix}max_posts" ]
					)
			) {
				$num_terms   = 0;
				$post_counts = $this->get_total_post_count(
					array(
						'post_type'   => $options[ "{$this->prefix}posttypes" ],
						'post_status' => 'publish',
					)
				);

				$term_counts = $this->get_all_term_counts( array( 'taxonomy' => $options[ "{$this->prefix}taxonomies" ] ) );
				if ( isset( $term_counts ) && is_array( $term_counts ) ) {
					$num_terms = array_sum( $term_counts );
				}

				$sitemap_urls = $post_counts + $num_terms;

				if ( 1000 < $sitemap_urls ) {
					$aioseop_notices->activate_notice( 'sitemap_max_warning' );
				} else {
					$aioseop_notices->deactivate_notice( 'sitemap_max_warning' );
				}
			} else {
				$aioseop_notices->deactivate_notice( 'sitemap_max_warning' );
			}
		}

		/**
		 * Update Sitemap from Posts
		 *
		 * @since 2.3.6
		 *
		 * @param $new_status
		 * @param $old_status
		 * @param $post
		 */
		public function update_sitemap_from_posts( $new_status, $old_status, $post ) {
			// ignore WP API requests.
			if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
				return;
			}

			if ( $this->option_isset( 'rewrite' ) ) {
				// TODO if dynamic, delete transient (we currently don't do transients).
				return;
			}

			$posttypes = array();
			if ( ! empty( $this->options[ "{$this->prefix}posttypes" ] ) ) {
				$posttypes = $this->options[ "{$this->prefix}posttypes" ];
			}

			if ( ! in_array( $post->post_type, $posttypes, true ) ) {
				return;
			}

			$statuses_for_updating = array( 'new', 'publish', 'trash' );
			if ( ! in_array( $new_status, $statuses_for_updating, true ) ) {
				return;
			}

			if ( defined( 'AIOSEOP_UNIT_TESTING' ) ) {
				$this->do_sitemaps();
			} elseif ( ! has_action( 'shutdown', array( $this, 'do_sitemaps' ) ) ) {
				/**
				 * Defer do_sitemaps until after everything is done.
				 * And run it only once regardless of posts updated.
				 */
				add_action( 'shutdown', array( $this, 'do_sitemaps' ) );
			}
		}

		/**
		 * Add Cron Schedules
		 *
		 * Add new intervals of a week and a month.
		 *
		 * @since ?
		 *
		 * @link https://codex.wordpress.org/Plugin_API/Filter_Reference/cron_schedules
		 *
		 * @param $schedules
		 * @return mixed
		 */
		public function add_cron_schedules( $schedules ) {
			$schedules['weekly']  = array(
				'interval' => 604800, // 1 week in seconds.
				'display'  => __( 'Once Weekly', 'all-in-one-seo-pack' ),
			);
			$schedules['monthly'] = array(
				'interval' => 2629740, // 1 month in seconds.
				'display'  => __( 'Once Monthly', 'all-in-one-seo-pack' ),
			);

			return $schedules;
		}

		/**
		 * Cron Update
		 *
		 * @since ?
		 */
		public function cron_update() {
			add_filter( 'cron_schedules', array( $this, 'add_cron_schedules' ) );
			if ( ! wp_next_scheduled( $this->prefix . 'daily_update_cron' ) ) {
				wp_schedule_event( time(), $this->options[ $this->prefix . 'daily_cron' ], $this->prefix . 'daily_update_cron' );
			}
		}

		/**
		 * Daily Update
		 *
		 * @since ?
		 */
		public function daily_update() {
			$last_run = get_option( $this->prefix . 'cron_last_run' );
			if ( empty( $last_run ) || ( time() - $last_run > 23.5 * 60 * 60 ) ) {
				// Sanity check.
				$this->do_sitemaps( __( 'Daily scheduled sitemap check has finished.', 'all-in-one-seo-pack' ) );
			}
			$last_run = time();
			update_option( $this->prefix . 'cron_last_run', $last_run );
		}

		/**
		 * Admin Enqueue Scripts
		 *
		 * Hook function to enqueue scripts and localize data to scripts.
		 *
		 * @since 3.0
		 *
		 * @see 'admin_enqueue_scripts' hook
		 * @link https://developer.wordpress.org/reference/hooks/admin_enqueue_scripts/
		 *
		 * @param string $hook_suffix The current admin page.
		 */
		public function admin_enqueue_scripts( $hook_suffix ) {
			parent::admin_enqueue_scripts( $hook_suffix );
			if ( $this->pagehook !== $hook_suffix ) {
				return;
			}

			wp_enqueue_script(
				'aioseop-selectize',
				'https://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.6/js/standalone/selectize.min.js',
				array( 'jquery' ),
				AIOSEOP_VERSION
			);

			wp_enqueue_script(
				'aioseop-search-terms',
				AIOSEOP_PLUGIN_URL . 'js/modules/aioseop_sitemap.js',
				array( 'jquery' ),
				AIOSEOP_VERSION,
				true
			);
		}

		/**
		 * Admin Enqueue Styles
		 *
		 * Load styles for module.
		 *
		 * @since 3.0
		 *
		 * @see 'admin_enqueue_scripts' hook
		 * @link https://developer.wordpress.org/reference/hooks/admin_enqueue_scripts/
		 *
		 * @param string $hook_suffix The current admin page.
		 */
		public function admin_enqueue_styles( $hook_suffix ) {
			parent::admin_enqueue_styles( $hook_suffix );
			if ( $this->pagehook !== $hook_suffix ) {
				return;
			}

			wp_enqueue_style(
				'aioseop-selectize',
				'https://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.6/css/selectize.css',
				false,
				AIOSEOP_VERSION,
				false
			);
			wp_enqueue_style(
				'aioseop-selectize-default',
				'https://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.6/css/selectize.default.min.css',
				false,
				AIOSEOP_VERSION,
				false
			);
		}

		/**
		 * Load Sitemap Options
		 *
		 * Initialize options, after constructor.
		 *
		 * @since ?
		 */
		public function load_sitemap_options() {
			// Load initial options / set defaults.
			$this->update_options();
			if ( ! empty( $this->options[ "{$this->prefix}indexes" ] ) ) {
				if ( $this->options[ "{$this->prefix}max_posts" ] && ( $this->options[ "{$this->prefix}max_posts" ] > 0 ) && ( $this->options[ "{$this->prefix}max_posts" ] < 50000 ) ) {
					$this->max_posts = $this->options[ "{$this->prefix}max_posts" ];
				}
			}

			if ( is_multisite() ) {
				$this->options[ "{$this->prefix}rewrite" ] = 'On';
			}

			if ( $this->options[ "{$this->prefix}rewrite" ] ) {
				$this->setup_rewrites();
			}

			/**
			 * Filters whether to display the URL to the XML Sitemap on our virtual robots.txt file.
			 *
			 * Defaults to true. Return __return_false in order to not display the URL.
			 *
			 * @since 3.0
			 *
			 * @param boolean Defaults to true.
			 */
			if ( apply_filters( 'aioseop_robotstxt_sitemap_url', true ) ) {
				add_action( 'do_robots', array( $this, 'do_robots' ), 100 );
			}

			if ( isset( $this->options[ $this->prefix . 'daily_cron' ] ) && $this->options[ $this->prefix . 'daily_cron' ] ) {
				add_action( 'wp', array( $this, 'cron_update' ) );
			} else {
				$time = wp_next_scheduled( $this->prefix . 'daily_update_cron' );
				if ( $time ) {
					wp_unschedule_event( $time, $this->prefix . 'daily_update_cron' );
				}
			}
		}

		/**
		 * Display Custom Options
		 *
		 * Displays boxes for add pages to sitemap option. Requires WordPress 4.1.
		 *
		 * @since ?
		 *
		 * @param $buf
		 * @param $args
		 * @return string
		 */
		public function display_custom_options( $buf, $args ) {
			if ( "{$this->prefix}addl_pages" === $args['name'] ) {
				$buf .= "<div id='{$this->prefix}addl_pages'>";
				if ( ! empty( $args['value'] ) ) {
					$buf .= "<table class='aioseop_table'>\n";
					foreach ( $args['value'] as $k => $v ) {
						if ( is_object( $v ) ) {
							$v = (array) $v;
						}
						$buf .= "\t<tr><td><a href='#' title='$k' class='dashicons dashicons-trash aiosp_delete_url' aria-label='" . __( 'Delete this additional URL', 'all-in-one-seo-pack' ) . "'></a></td><td>{$k}</td><td>{$v['prio']}</td><td>{$v['freq']}</td><td>{$v['mod']}</td></tr>\n";
					}
					$buf .= "</table>\n";
				}
			}
			$args['options']['type'] = 'hidden';
			if ( ! empty( $args['value'] ) ) {
				$args['value'] = wp_json_encode( $args['value'] );
			} else {
				$args['options']['type'] = 'html';
			}
			if ( empty( $args['value'] ) ) {
				$args['value'] = '';
			}
			$buf .= $this->get_option_html( $args );
			$buf .= '</div>';

			return $buf;
		}

		/**
		 * Add Post Types
		 *
		 * Add post type details for settings once post types have been registered.
		 *
		 * @todo This function is being used to set up option values. This could possibly be refactored to something better suited.
		 *
		 * @since ?
		 * @since 3.0 Add custom taxonomy support for Excluding Terms setting. (#240)
		 */
		public function add_post_types() {
			$post_type_titles = $this->get_post_type_titles( array( 'public' => true ) );
			$taxonomy_titles  = $this->get_taxonomy_titles( array( 'public' => true ) );
			if ( isset( $post_type_titles['attachment'] ) ) {
				$post_type_titles['attachment'] = __( 'Media / Attachments', 'all-in-one-seo-pack' );
			}
			$this->default_options['posttypes']['initial_options']  = array_merge( array( 'all' => __( 'All Post Types', 'all-in-one-seo-pack' ) ), $post_type_titles );
			$this->default_options['taxonomies']['initial_options'] = array_merge( array( 'all' => __( 'All Taxonomies', 'all-in-one-seo-pack' ) ), $taxonomy_titles );
			$this->default_options['posttypes']['default']          = array_keys( $this->default_options['posttypes']['initial_options'] );
			$this->default_options['taxonomies']['default']         = array_keys( $this->default_options['taxonomies']['initial_options'] );

			// Exclude Terms element items.
			$this->default_options['excl_terms']['initial_options'] = array();

			$taxonomies_active = array();
			if ( is_array( $this->options[ $this->prefix . 'taxonomies' ] ) ) {
				$taxonomies_active = $this->options[ $this->prefix . 'taxonomies' ];
			} elseif ( ! empty( $this->options[ $this->prefix . 'taxonomies' ] ) ) {
				$taxonomies_active = array( $this->options[ $this->prefix . 'taxonomies' ] );
			}

			$args_taxonomy_key = array_search( 'all', $taxonomies_active, true );
			if ( false !== $args_taxonomy_key ) {
				// Remove 'all' as an invalid post_type. Use registered post_types selected instead.
				unset( $taxonomies_active[ $args_taxonomy_key ] );
				// Adds all the taxonomies regardless if other taxonomies are selected; ensures all taxonomies are added.
				$taxonomies_active = array_merge( $taxonomies_active, get_taxonomies() );
			}

			$excl_terms_init_opts = array();
			foreach ( $taxonomies_active as $v1_taxonomy ) {
				$args_terms = array(
					'taxonomy'   => $v1_taxonomy,
					'hide_empty' => false,
				);

				$taxonomy_terms_tmp = $this->get_term_titles( $args_terms );
				foreach ( $taxonomy_terms_tmp as $k2_id => $v2_term ) {
					$excl_terms_init_opts[ $v1_taxonomy . '-' . $k2_id ] = $v2_term . ' (' . $v1_taxonomy . ')';
				}
			}
			$this->default_options['excl_terms']['initial_options'] = $excl_terms_init_opts;

			$post_name = ' ' . __( 'Post Type', 'all-in-one-seo-pack' );
			$tax_name  = ' ' . __( 'Taxonomy', 'all-in-one-seo-pack' );

			foreach ( $post_type_titles as $k => $v ) {
				$key                                      = 'prio_post_' . $k;
				$this->default_options                    = aioseop_array_insert_after(
					$this->default_options,
					'prio_post',
					array(
						$key => array(
							'name'            => $v . $post_name,
							'type'            => 'select',
							'initial_options' => $this->prio,
							'default'         => 'no',
							'condshow'        => array( "{$this->prefix}prio_post" => 'sel' ),
						),
					)
				);
				$this->layout['priorities']['options'][]  = $key;
				$key                                      = 'freq_post_' . $k;
				$this->default_options                    = aioseop_array_insert_after(
					$this->default_options,
					'freq_post',
					array(
						$key => array(
							'name'            => $v . $post_name,
							'type'            => 'select',
							'initial_options' => $this->freq,
							'default'         => 'no',
							'condshow'        => array( "{$this->prefix}freq_post" => 'sel' ),
						),
					)
				);
				$this->layout['frequencies']['options'][] = $key;
			}
			foreach ( $taxonomy_titles as $k => $v ) {
				$key                                      = 'prio_taxonomies_' . $k;
				$this->default_options                    = aioseop_array_insert_after(
					$this->default_options,
					'prio_taxonomies',
					array(
						$key => array(
							'name'            => $v . $tax_name,
							'type'            => 'select',
							'initial_options' => $this->prio,
							'default'         => 'no',
							'condshow'        => array( "{$this->prefix}prio_taxonomies" => 'sel' ),
						),
					)
				);
				$this->layout['priorities']['options'][]  = $key;
				$key                                      = 'freq_taxonomies_' . $k;
				$this->default_options                    = aioseop_array_insert_after(
					$this->default_options,
					'freq_taxonomies',
					array(
						$key => array(
							'name'            => $v . $tax_name,
							'type'            => 'select',
							'initial_options' => $this->freq,
							'default'         => 'no',
							'condshow'        => array( "{$this->prefix}freq_taxonomies" => 'sel' ),
						),
					)
				);
				$this->layout['frequencies']['options'][] = $key;
			}
			$this->update_options();
		}

		/**
		 * Add Page Hooks
		 *
		 * Set up settings, checking for sitemap conflicts, on settings page.
		 *
		 * @since ?
		 */
		public function add_page_hooks() {
			$this->flush_rules_hook();
			$this->add_post_types();
			parent::add_page_hooks();
			add_action( $this->prefix . 'settings_header', array( $this, 'do_sitemap_scan' ), 5 );
			add_filter( "{$this->prefix}submit_options", array( $this, 'filter_submit' ) );
		}

		/**
		 * Filter Submit
		 *
		 * Change settings page submit button to read "Update Sitemap".
		 *
		 * @since ?
		 *
		 * @param $submit
		 * @return mixed
		 */
		public function filter_submit( $submit ) {
			$submit['Submit']['value'] = __( 'Update Sitemap', 'all-in-one-seo-pack' ) . ' &raquo;';

			return $submit;
		}

		/**
		 * Updates Post Data
		 *
		 * Disable writing sitemaps to the filesystem for multisite.
		 *
		 * @since ?
		 *
		 * @param $options
		 * @return mixed
		 */
		public function update_post_data( $options ) {
			if ( is_multisite() ) {
				$options[ $this->prefix . 'rewrite' ]['disabled'] = 'disabled';
			}

			return $options;
		}

		/**
		 * Get Rewrite URL
		 *
		 * @since ?
		 *
		 * @param $url
		 * @return bool
		 */
		public function get_rewrite_url( $url ) {
			global $wp_rewrite;
			$url = wp_parse_url( esc_url( $url ), PHP_URL_PATH );
			$url = ltrim( $url, '/' );
			if ( ! empty( $wp_rewrite ) ) {
				$rewrite_rules = $wp_rewrite->rewrite_rules();
				foreach ( $rewrite_rules as $k => $v ) {
					if ( preg_match( "@^$k@", $url ) ) {
						return $v;
					}
				}
			}

			return false;
		}

		/**
		 * Get Filename
		 *
		 * Get the filename prefix for the sitemap file.
		 * If a value was provided when this prefix was configurable from the settings page, return that instead of the default.
		 *
		 * @since 2.6
		 *
		 * @return string
		 */
		protected function get_filename() {
			$filename = 'sitemap';
			if ( ! empty( $this->options[ "{$this->prefix}filename" ] ) ) {
				$filename = $this->options[ "{$this->prefix}filename" ];
				$filename = str_replace( '/', '', $filename );
			} elseif ( 'aiosp_video_sitemap_' === $this->prefix ) {
				$filename = 'video-sitemap';
			}
			/**
			 * Filters the filename: aiosp_sitemap_filename OR aiosp_video_sitemap_filename.
			 *
			 * @param string $filename The file name.
			 */
			return apply_filters( "{$this->prefix}filename", $filename );
		}

		/**
		 * Filter Display Options
		 *
		 * Add in options for status display on settings page, sitemap rewriting on multisite.
		 *
		 * @since 2.3.6
		 * @since 2.3.12.3 Refactored to use aioseop_home_url() for compatibility purposes.
		 * @since 3.0 Change 'excl_terms' to include taxonomy slugs with term id. (Pro #240)
		 * @since 3.0 Remove WP < 3.5 old Privacy Settings link
		 *
		 * @param $options
		 * @return mixed
		 */
		public function filter_display_options( $options ) {
			if ( is_multisite() ) {
				$options[ $this->prefix . 'rewrite' ] = 'On';
			}
			if ( isset( $options[ $this->prefix . 'max_posts' ] ) && ( ( $options[ $this->prefix . 'max_posts' ] <= 0 ) || ( $options[ $this->prefix . 'max_posts' ] >= 50000 ) ) ) {
				$options[ $this->prefix . 'max_posts' ] = 50000;
			}
			$url = aioseop_home_url( '/' . $this->get_filename() . '.xml' );

			/* translators: Link to documentation. */
			$options[ $this->prefix . 'link' ]  = sprintf( __( 'Click here to %s.', 'all-in-one-seo-pack' ), '<a href="' . esc_url( $url ) . '" target="_blank">' . __( 'view your XML sitemap', 'all-in-one-seo-pack' ) . '</a>' );
			$options[ $this->prefix . 'link' ] .= ' ' . __( 'Your sitemap has been created with content and images.', 'all-in-one-seo-pack' );

			if ( $options[ "{$this->prefix}rss_sitemap" ] ) {
				$url_rss = aioseop_home_url( '/' . $this->get_filename() . '.rss' );
				/* translators: Link to sitemap within current site. */
				$options[ $this->prefix . 'link' ] .= '<p>' . sprintf( __( 'Click here to %1$sview your RSS sitemap%2$s.', 'all-in-one-seo-pack' ), '<a href="' . esc_url( $url_rss ) . '" target="_blank">', '</a>' ) . '</p>';
			}

			if ( '0' !== get_option( 'blog_public' ) ) {
				$options[ $this->prefix . 'link' ] .= ' ' . __( 'Changes are automatically submitted to search engines.', 'all-in-one-seo-pack' );
			}

			if ( $this->option_isset( 'rewrite' ) ) {
				$rule  = $this->get_rewrite_url( $url );
				$rules = $this->get_rewrite_rules();
				// TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison.
				if ( ! in_array( $rule, $rules ) ) {
					$options[ $this->prefix . 'link' ] .= '<p>' . __( 'Dynamic sitemap generation does not appear to be using the correct rewrite rules; please disable any other sitemap plugins or functionality on your site and reset your permalinks.', 'all-in-one-seo-pack' ) . '</p>';
				}
			}
			if ( ! get_option( 'blog_public' ) ) {
				$privacy_link = '<a href="options-reading.php">' . __( 'Reading Settings', 'all-in-one-seo-pack' ) . '</a>';
				/* translators: Link to settings to disable "Discourage search engines from indexing this site". */
				$options[ $this->prefix . 'link' ] .= '<p class="aioseop_error_notice">' . sprintf( __( 'Warning: your privacy settings are configured to ask search engines to not index your site; you can change this under %s for your site.', 'all-in-one-seo-pack' ), $privacy_link );
			}

			$excl_terms = array();
			if ( isset( $options[ $this->prefix . 'excl_terms' ] ) && is_array( $options[ $this->prefix . 'excl_terms' ] ) ) {
				foreach ( $options[ $this->prefix . 'excl_terms' ] as $k1_taxonomy => $v1_tax_terms ) {
					foreach ( $v1_tax_terms['terms'] as $v2_term ) {
						$excl_terms[] = $k1_taxonomy . '-' . $v2_term;
					}
				}
			}

			$options[ $this->prefix . 'excl_terms' ] = $excl_terms;

			return $options;
		}

		/**
		 * Filter Options
		 *
		 * Handle 'all' option for post types / taxonomies, further sanitization of filename, rewrites on for multisite, setting up addl pages option.
		 *
		 * @todo This needs nonce support.
		 *
		 * @since ?
		 * @since 3.0 Change saving 'excl_terms' to database with tax_query format for custom taxonomy support. (Pro #240)
		 *
		 * @param $options
		 * @return mixed
		 */
		public function filter_options( $options ) {
			if ( ! isset( $this->default_options['posttypes']['initial_options'] ) ) {
				$this->add_post_types();
			}
			// TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison.
			if ( is_array( $options[ "{$this->prefix}posttypes" ] ) && in_array( 'all', $options[ "{$this->prefix}posttypes" ] ) && is_array( $this->default_options['posttypes']['initial_options'] ) ) {
				$options[ "{$this->prefix}posttypes" ] = array_keys( $this->default_options['posttypes']['initial_options'] );
			}
			// TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison.
			if ( is_array( $options[ "{$this->prefix}taxonomies" ] ) && in_array( 'all', $options[ "{$this->prefix}taxonomies" ] ) && is_array( $this->default_options['taxonomies']['initial_options'] ) ) {
				$options[ "{$this->prefix}taxonomies" ] = array_keys( $this->default_options['taxonomies']['initial_options'] );
			}
			if ( is_multisite() ) {
				$options[ $this->prefix . 'rewrite' ] = 'On';
			}
			if ( ! is_array( $options[ $this->prefix . 'addl_pages' ] ) ) {
				$options[ $this->prefix . 'addl_pages' ] = wp_specialchars_decode( stripslashes_deep( $options[ $this->prefix . 'addl_pages' ] ), ENT_QUOTES );
				$decoded                                 = json_decode( $options[ $this->prefix . 'addl_pages' ] );
				if ( null === $decoded ) {
					$decoded = maybe_unserialize( $options[ $this->prefix . 'addl_pages' ] );
				}
				if ( ! is_array( $decoded ) ) {
					$decoded = (array) $decoded;
				}
				if ( null === $decoded ) {
					$decoded = $options[ $this->prefix . 'addl_pages' ];
				}
				$options[ $this->prefix . 'addl_pages' ] = $decoded;
			}
			if ( is_array( $options[ $this->prefix . 'addl_pages' ] ) ) {
				foreach ( $options[ $this->prefix . 'addl_pages' ] as $k => $v ) {
					if ( is_object( $v ) ) {
						$options[ $this->prefix . 'addl_pages' ][ $k ] = (array) $v;
					}
				}
			}
			if ( isset( $options[ $this->prefix . 'addl_pages' ][0] ) ) {
				unset( $options[ $this->prefix . 'addl_pages' ][0] );
			}

			// TODO Refactor all these... use a nonce, dump the incoming _Post into an array and use that.
			if ( ! empty( $_POST[ $this->prefix . 'addl_url' ] ) ) {
				foreach ( array( 'addl_url', 'addl_prio', 'addl_freq', 'addl_mod' ) as $field ) {
					if ( ! empty( $_POST[ $this->prefix . $field ] ) ) {
						// TODO Add/Change to filter_input().
						$_POST[ $this->prefix . $field ] = esc_attr( wp_kses_post( $_POST[ $this->prefix . $field ] ) );
					} else {
						// TODO Add/Change to filter_input().
						$_POST[ $this->prefix . $field ] = '';
					}
				}
				if ( ! is_array( $options[ $this->prefix . 'addl_pages' ] ) ) {
					$options[ $this->prefix . 'addl_pages' ] = array();
				}

				if ( aiosp_common::is_url_valid( $_POST[ $this->prefix . 'addl_url' ] ) ) {
					$options[ $this->prefix . 'addl_pages' ][ $_POST[ $this->prefix . 'addl_url' ] ] = array(
						// TODO Add/Change to filter_input().
						'prio' => $_POST[ $this->prefix . 'addl_prio' ],
						'freq' => $_POST[ $this->prefix . 'addl_freq' ],
						'mod'  => $_POST[ $this->prefix . 'addl_mod' ],
					);
				}
			}

			if ( ! empty( $_POST[ $this->prefix . 'excl_terms' ] ) ) {
				$raw_excl_terms = filter_input( INPUT_POST, $this->prefix . 'excl_terms', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );

				// Parse taxonomy terms {$taxonomy_slug}-{$term_id}.
				$excl_terms = array();
				foreach ( $raw_excl_terms as $v1_tax_term ) {
					$term_id       = explode( '-', $v1_tax_term );
					$term_id       = intval( end( $term_id ) );
					$taxonomy_slug = sanitize_text_field( str_replace( '-' . $term_id, '', $v1_tax_term ) );

					// Initialize taxonomy => terms array if not yet set.
					if ( ! isset( $excl_terms[ $taxonomy_slug ] ) ) {
						$excl_terms[ $taxonomy_slug ] = array(
							'terms' => array(),
						);
					}

					$excl_terms[ $taxonomy_slug ]['taxonomy'] = $taxonomy_slug;
					$excl_terms[ $taxonomy_slug ]['terms'][]  = $term_id;
				}

				$options[ $this->prefix . 'excl_terms' ] = $excl_terms;
			}

			return $options;
		}

		/**
		 * Get Child Sitemap URLs
		 *
		 * Get sitemap urls of child blogs, if any.
		 *
		 * @since ?
		 *
		 * @return array
		 */
		public function get_child_sitemap_urls() {
			$siteurls = array();
			$blogs    = $this->get_child_blogs();
			if ( ! empty( $blogs ) ) {
				$option_name = $this->get_option_name();
				foreach ( $blogs as $blog_id ) {
					if ( $this->is_aioseop_active_on_blog( $blog_id ) ) {
						$options = get_blog_option( $blog_id, $this->parent_option );
						if (
							! empty( $options )
							&& ! empty( $options['modules'] )
							&& ! empty( $options['modules']['aiosp_feature_manager_options'] )
							&& ! empty( $options['modules']['aiosp_feature_manager_options']['aiosp_feature_manager_enable_sitemap'] )
							&& ! empty( $options['modules'][ $option_name ] )
						) {
							global $wpdb;
							$sitemap_options = $options['modules'][ $option_name ];
							$siteurl         = '';
							if ( defined( 'SUNRISE' ) && SUNRISE && is_object( $wpdb ) && isset( $wpdb->dmtable ) && ! empty( $wpdb->dmtable ) ) {
								// @codingStandardsIgnoreStart
								$domain = $wpdb->get_var( "SELECT domain FROM {$wpdb->dmtable} WHERE blog_id = '$blog_id' AND active = 1 LIMIT 1" );
								// @codingStandardsIgnoreEnd
								if ( $domain ) {
									if ( ! isset( $_SERVER['HTTPS'] ) ) {
										$_SERVER['HTTPS'] = 'Off';
									}
									$protocol = ( 'on' === strtolower( $_SERVER['HTTPS'] ) ) ? 'https://' : 'http://';
									$siteurl  = untrailingslashit( $protocol . $domain );
								}
							}
							if ( ! $siteurl ) {
								$siteurl = get_home_url( $blog_id );
							}
							$url        = $siteurl . '/' . $this->get_filename() . '.xml';
							$siteurls[] = $url;
						}
					}
				}
			}
			$siteurls = apply_filters( $this->prefix . 'sitemap_urls', $siteurls ); // Legacy.
			return apply_filters( $this->prefix . 'child_urls', $siteurls );
		}

		/**
		 * Gets Home Path
		 *
		 * If we're in wp-admin, use the WordPress function, otherwise we user our own version here.
		 * This only applies to static sitemaps.
		 *
		 * @since 2.3.6.1
		 *
		 * @return mixed|string
		 */
		public function get_home_path() {

			if ( function_exists( 'get_home_path' ) ) {
				return get_home_path();
			}

			$home    = set_url_scheme( get_option( 'home' ), 'http' );
			$siteurl = set_url_scheme( get_option( 'siteurl' ), 'http' );
			if ( ! empty( $home ) && 0 !== strcasecmp( $home, $siteurl ) ) {
				$wp_path_rel_to_home = str_ireplace( $home, '', $siteurl ); /* $siteurl - $home */
				$pos                 = strripos( str_replace( '\\', '/', $_SERVER['SCRIPT_FILENAME'] ), trailingslashit( $wp_path_rel_to_home ) );
				$home_path           = substr( $_SERVER['SCRIPT_FILENAME'], 0, $pos );
				$home_path           = trailingslashit( $home_path );
			} else {
				$home_path = ABSPATH;
			}

			return str_replace( '\\', '/', $home_path );
		}

		/**
		 * Whitelist Static Sitemaps
		 *
		 * Whitelists files from static sitemap conflict warning.
		 * For right now, this is just externally produced news sitemaps until we figure out something better.
		 *
		 * @since 2.3.10.2
		 *
		 * @param $file
		 * @return string
		 */
		public function whitelist_static_sitemaps( $file ) {

			$whitelist = array( 'sitemap_news.xml', 'sitemap-news.xml' );

			if ( in_array( $file, $whitelist, true ) ) {
				return '';
			}

			return $file;
		}

		/**
		 * Scan Match Files
		 *
		 * Scan for sitemaps on filesystem.
		 *
		 * @since ?
		 *
		 * @return array
		 */
		public function scan_match_files() {
			$scan1 = '';
			$files = array();

			$filename = $this->get_filename();
			if ( ! empty( $filename ) ) {
				$scan1 = get_home_path() . $filename . '*.xml';

				if ( empty( $scan1 ) ) {
					return $files;
				}
				$home_path = get_home_path();
				$filescan  = $this->scandir( $home_path );
				if ( ! empty( $filescan ) ) {
					foreach ( $filescan as $f ) {
						if ( ! empty( $scan1 ) && fnmatch( $scan1, $home_path . $f ) ) {

							$f       = $this->whitelist_static_sitemaps( $f );
							$files[] = $home_path . $f;
							continue;
						}
					}
				}
				return $files;
			}
		}

		/**
		 * Do Sitemap Scan
		 *
		 * Handle deleting / renaming of conflicting sitemap files.
		 *
		 * @todo Add/Fix nonce.
		 *
		 * @since ?
		 */
		public function do_sitemap_scan() {
			$msg = '';
			if ( ! empty( $this->options[ "{$this->prefix}rewrite" ] ) && ( get_option( 'permalink_structure' ) === '' ) ) {
				$msg = '<p>' . __( 'Warning: dynamic sitemap generation must have permalinks enabled.', 'all-in-one-seo-pack' ) . '</p>';
			}
			if ( ! empty( $_POST['aioseop_sitemap_rename_files'] ) || ! empty( $_POST['aioseop_sitemap_delete_files'] ) ) {
				// TODO Add/Change to filter_input().
				$nonce = $_POST['nonce-aioseop'];
				if ( ! wp_verify_nonce( $nonce, 'aioseop-nonce' ) ) {
					// TODO Change to wp_die().
					die( __( 'Security Check - If you receive this in error, log out and back in to WordPress', 'all-in-one-seo-pack' ) );
				}
				if ( ! empty( $_POST['aioseop_sitemap_conflict'] ) ) {
					$files = $this->scan_match_files();
					foreach ( $files as $f => $file ) {
						$files[ $f ] = realpath( $file );
					}
					foreach ( $_POST['aioseop_sitemap_conflict'] as $ren_file ) {
						$ren_file = realpath( get_home_path() . $ren_file );
						// TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison.
						if ( in_array( $ren_file, $files ) ) {
							if ( ! empty( $_POST['aioseop_sitemap_delete_files'] ) ) {
								if ( $this->delete_file( $ren_file ) ) {
									/* translators: Shows which sitemap files have been deleted. */
									$msg .= '<p>' . sprintf( __( 'Deleted %s.', 'all-in-one-seo-pack' ), $ren_file ) . '</p>';
								}
								continue;
							}
							$count = 0;
							do {
								$ren_to = $ren_file . '._' . sprintf( '%03d', $count ) . '.old';
								$count ++;
							} while ( $this->file_exists( $ren_to ) && ( $count < 1000 ) );
							if ( $count >= 1000 ) {
								/* translators: Shows which sitemap files couldn't be renamed. */
								$msg .= '<p>' . sprintf( __( "Couldn't rename file %s!", 'all-in-one-seo-pack' ), $ren_file ) . '</p>';
							} else {
								$ren = $this->rename_file( $ren_file, $ren_to );
								if ( $ren ) {
									/* translators: Shows which sitemap files were renamed. */
									$msg .= '<p>' . sprintf( __( 'Renamed %1$s to %2$s.', 'all-in-one-seo-pack' ), $ren_file, $ren_to ) . '</p>';
								}
							}
						} else {
							/* translators: Shows which sitemap files couldn't be found. */
							$msg .= '<p>' . sprintf( __( "Couldn't find file %s!", 'all-in-one-seo-pack' ), $ren_file ) . '</p>';
						}
					}
				}
			} else {
				$msg .= $this->scan_sitemaps();
			}

			if ( ! empty( $msg ) ) {
				$this->output_error( $msg );
			}
		}

		/**
		 * Scan Sitemaps
		 *
		 * Do the scan, return the results.
		 *
		 * @since ?
		 *
		 * @return string
		 */
		public function scan_sitemaps() {
			$msg   = '';
			$files = $this->scan_match_files();
			if ( ! empty( $files ) ) {
				$msg = $this->sitemap_warning( $files );
			}

			return $msg;
		}

		/**
		 * Get Problem Files
		 *
		 * Get the list of potentially conflicting sitemap files, identify whether they came from us, are blank, or are of unknown origin.
		 *
		 * @since ?
		 * @since 2.3.10 Add the ability to see empty sitemap files as well.
		 *
		 * @param $files
		 * @param $msg
		 * @return array
		 */
		public function get_problem_files( $files, &$msg ) {
			$problem_files = array();
			$use_wpfs      = true;
			$wpfs          = $this->get_filesystem_object();
			if ( ! is_object( $wpfs ) ) {
				$use_wpfs = false;
			} else {
				if ( 'direct' === $wpfs->method ) {
					$use_wpfs = false;
				}
			}

			foreach ( $files as $f ) {
				if ( $this->is_file( $f ) ) {
					$fn         = $f;
					$compressed = false;
					if ( $this->substr( $f, - 3 ) === '.gz' ) {
						$compressed = true;
					}
					if ( $use_wpfs ) {
						if ( $compressed ) {  // Inefficient but necessary.
							$file = $this->load_file( $fn );
							if ( ! empty( $file ) ) {
								$file = gzuncompress( $file, 4096 );
							}
						} else {
							$file = $this->load_file( $fn, false, null, - 1, 4096 );
						}
					} else {
						if ( $compressed ) {
							$file_resource = gzopen( $fn, 'rb' );
							$file          = gzread( $file_resource, 4096 );
							gzclose( $file_resource );
						} else {
							// TODO Change to `wp_remote_get()`.
							$file = file_get_contents( $fn, false, null, 0, 4096 );
						}
					}
					if ( ! empty( $file ) ) {
						$matches = array();
						if ( preg_match(
							'/<!-- ' . sprintf( $this->comment_string, '(.*)', '(.*)', '(.*)' ) . ' -->/',
							$file,
							$matches
						) ) {
							if ( ! empty( $this->options[ "{$this->prefix}rewrite" ] ) ) {
								/* translators: %1$s, %2$s, etc. are placeholders and should not be translated. %1$s expands to the name of a sitemap file, %2$s to the name of the plugin, All in One SEO Pack, %3$s is replaced with the plugin version number and %4$s with a date. */
								$msg .= '<p>' . sprintf(
									__( "Warning: a static sitemap '%1\$s' generated by %2\$s %3\$s on %4\$s already exists that may conflict with dynamic sitemap generation.", 'all-in-one-seo-pack' ),
									$f,
									AIOSEOP_PLUGIN_NAME,
									$matches[2],
									$matches[3]
								) . "</p>\n";

								$problem_files[] = $f;
							}
						} else {
							/* translators: Shows which 'unknown' file is conflicting with the current sitemap settings. */
							$msg .= '<p>' . sprintf( __( 'Potential conflict with unknown file %s.', 'all-in-one-seo-pack' ), $f ) . "</p>\n";

							$problem_files[] = $f;
						}
					} else {
						/* translators: Shows which files were removed. */
						$msg .= '<p>' . sprintf( __( 'Removed empty file %s.', 'all-in-one-seo-pack' ), $f ) . "</p>\n";

						$problem_files[] = $f;

						// This is causing all problem_files to be deleted automatically; which may be the intent.
						// TODO Either create a separate variable for this set of problem_files, or a final loop to clean problem_files before returning.
						foreach ( $problem_files as $f => $file ) {
							$files[ $f ] = realpath( $file );
							$this->delete_file( realpath( $file ) );
						}
						$problem_files = false; // Don't return anything. If it's blank, we'll take care of it here.
					}
				}
			}

			return $problem_files;
		}

		/**
		 * Sitemap Warning
		 *
		 * Display the warning and the form for conflicting sitemap files.
		 *
		 * @since ?
		 *
		 * @param $files
		 * @return string
		 */
		public function sitemap_warning( $files ) {
			$msg           = '';
			$conflict      = false;
			$problem_files = $this->get_problem_files( $files, $msg );
			if ( ! empty( $problem_files ) ) {
				$conflict = true;
			}
			if ( $conflict ) {
				foreach ( $problem_files as $p ) {
					$msg .= "<input type='hidden' name='aioseop_sitemap_conflict[]' value='" . esc_attr( basename( realpath( $p ) ) ) . "'>\n";
				}
				$msg .= "<input type='hidden' name='nonce-aioseop' value='" . wp_create_nonce( 'aioseop-nonce' ) . "'>\n";
				$msg .= "<input type='submit' name='aioseop_sitemap_delete_files' class='aioseop_delete_files_button button-primary' value='" . __( 'Delete Conflicting Files', 'all-in-one-seo-pack' ) . "' aria-label='" . __( 'Delete Conflicting Files', 'all-in-one-seo-pack' ) . "'>";
				$msg .= "<input type='submit' name='aioseop_sitemap_rename_files' class='aioseop_rename_files_button button-secondary' value='" . __( 'Rename Conflicting Files', 'all-in-one-seo-pack' ) . "' aria-label='" . __( 'Rename Conflicting Files', 'all-in-one-seo-pack' ) . "'> ";

				$msg = '<form action="" method="post">' . $msg . '</form>';
			}

			return $msg;
		}

		/**
		 * Debug Message
		 *
		 * Updates debug log messages.
		 *
		 * Deprecated as of 2.3.10 in favor of WP debug log. We should eventually remove this.
		 *
		 * @since ?
		 *
		 * @param $msg
		 */
		public function debug_message( $msg ) {
			aiosp_log( $msg );
		}

		/**
		 * Setup Rewrites
		 *
		 * Set up hooks for rewrite rules for dynamic sitemap generation.
		 *
		 * @since ?
		 */
		public function setup_rewrites() {
			add_filter( 'rewrite_rules_array', array( $this, 'rewrite_hook' ) );
			add_filter( 'query_vars', array( $this, 'query_var_hook' ) );
			add_action( 'parse_query', array( $this, 'sitemap_output_hook' ) );
			if ( ! get_transient( "{$this->prefix}rules_flushed" ) ) {
				add_action( 'wp_loaded', array( $this, 'flush_rules_hook' ) );
			}
		}

		/**
		 * Get Rewrite Rules
		 *
		 * Build and return our rewrite rules.
		 *
		 * @since ?
		 *
		 * @param string  $prefix_removed_rules_with  If rules are being removed, prefix them with this character
		 *                                            so that they are flushed properly and are not retained.
		 * @return array
		 */
		public function get_rewrite_rules( $prefix_removed_rules_with = null ) {

			$sitemap_rules = array(
				$this->get_filename() . '.xml'           => 'index.php?' . $this->prefix . 'path=root',
				'(.+)-' . $this->get_filename() . '(\d+).xml' => 'index.php?' . $this->prefix . 'path=$matches[1]&' . $this->prefix . 'page=$matches[2]',
				'(.+)-' . $this->get_filename() . '.xml' => 'index.php?' . $this->prefix . 'path=$matches[1]',
			);

			if ( isset( $this->options[ "{$this->prefix}rss_sitemap" ] ) && $this->options[ "{$this->prefix}rss_sitemap" ] ) {
				$sitemap_rules += array(
					$this->get_filename() . '.rss'       => 'index.php?' . $this->prefix . 'path=rss',
					$this->get_filename() . 'latest.rss' => 'index.php?' . $this->prefix . 'path=rss_latest',
				);
			} elseif ( ! empty( $prefix_removed_rules_with ) ) {
				$sitemap_rules += array(
					$prefix_removed_rules_with . $this->get_filename() . '.rss'            => 'index.php?' . $this->prefix . 'path=rss',
					$prefix_removed_rules_with . $this->get_filename() . 'latest.rss'      => 'index.php?' . $this->prefix . 'path=rss_latest',
				);
			}
			return $sitemap_rules;
		}

		/**
		 * Rewrite Hook
		 *
		 * Add in our rewrite rules.
		 *
		 * @since ?
		 *
		 * @param $rules
		 * @return array
		 */
		public function rewrite_hook( $rules ) {
			$sitemap_rules = $this->get_rewrite_rules();
			if ( ! empty( $sitemap_rules ) ) {
				$rules = $sitemap_rules + $rules;
			}

			return $rules;
		}

		/**
		 * Flush Rewrite Rule
		 *
		 * @since ?
		 */
		public function flush_rules_hook() {
			global $wp_rewrite;
			$sitemap_rules = $this->get_rewrite_rules( '|' );
			if ( ! empty( $sitemap_rules ) ) {
				$rules     = get_option( 'rewrite_rules' );
				$new_rules = array_keys( $sitemap_rules );
				foreach ( $new_rules as $rule ) {
					if ( ! isset( $rules[ $rule ] ) || ( $rules[ $rule ] !== $sitemap_rules[ $rule ] ) ) {
						$wp_rewrite->flush_rules();
						set_transient( "{$this->prefix}rules_flushed", true, 43200 );
					}
				}
			}
		}

		/**
		 * Query Var Hook
		 *
		 * Add our query variable for sitemap generation.
		 *
		 * @since ?
		 *
		 * @param $vars
		 * @return array
		 */
		public function query_var_hook( $vars ) {
			$vars[] = "{$this->prefix}path";
			if ( ! empty( $this->options[ "{$this->prefix}indexes" ] ) ) {
				$vars[] = "{$this->prefix}page";
			}

			return $vars;
		}

		/**
		 * Log Start
		 *
		 * Start timing and get initial memory usage for debug info.
		 *
		 * @since ?
		 */
		public function log_start() {
			$this->start_memory_usage = memory_get_peak_usage();
			timer_start();
		}


		/**
		 * Log Stats
		 *
		 * Stop timing and log memory usage for debug info.
		 *
		 * @since ?
		 * @since 3.0 Removed $compressed in issue #534
		 *
		 * @param string $sitemap_type
		 * @param bool   $dynamic
		 */
		public function log_stats( $sitemap_type = 'static', $dynamic = true ) {
			$time                 = timer_stop();
			$end_memory_usage     = memory_get_peak_usage();
			$sitemap_memory_usage = $end_memory_usage - $this->start_memory_usage;
			$end_memory_usage     = $end_memory_usage / 1024.0 / 1024.0;
			$sitemap_memory_usage = $sitemap_memory_usage / 1024.0 / 1024.0;
			$sitemap_type         = __( 'static', 'all-in-one-seo-pack' );
			if ( $dynamic ) {
				$sitemap_type = __( 'dynamic', 'all-in-one-seo-pack' );
			}
			$this->debug_message( sprintf( ' %01.2f MB memory used generating the %s sitemap in %01.3f seconds, %01.2f MB total memory used.', $sitemap_memory_usage, $sitemap_type, $time, $end_memory_usage ) );
		}

		/**
		 * Sitemaps Output Hook
		 *
		 * Handle outputting of dynamic sitemaps, logging.
		 *
		 * @since ?
		 * @since 3.0 Show 404 template for empty content. #2190
		 *
		 * @param $query
		 */
		public function sitemap_output_hook( $query ) {
			$page = 0;
			if ( $this->options[ "{$this->prefix}rewrite" ] && ! empty( $query->query_vars[ "{$this->prefix}path" ] ) ) {

				// Make dynamic sitemap.
				if ( ! empty( $query->query_vars[ "{$this->prefix}page" ] ) ) {
					$page = $query->query_vars[ "{$this->prefix}page" ] - 1;
				}
				$this->start_memory_usage = memory_get_peak_usage();
				$sitemap_type             = $query->query_vars[ "{$this->prefix}path" ];

				$blog_charset = get_option( 'blog_charset' );
				header( "Content-Type: text/xml; charset=$blog_charset", true );

				// Always follow and noindex the sitemap.
				header( 'X-Robots-Tag: noindex, follow', true );

				do_action( $this->prefix . 'add_headers', $query, $this->options );

				$content = $this->do_rewrite_sitemap( $sitemap_type, $page );

				// if the sitemap has no content, it's probabaly invalid and is being called directly.
				// @issue ( https://github.com/semperfiwebdesign/all-in-one-seo-pack/issues/2190 ).
				if ( empty( $content ) ) {
					$query->set_404();
					status_header( 404 );
					header( "Content-Type: text/html; charset=$blog_charset", true );
					nocache_headers();
					include( get_404_template() );
					exit();
				}

				echo $content;

				$this->log_stats( $sitemap_type );
				exit();

			}
		}

		/**
		 * Make Dynamic XSL
		 *
		 * @since ?
		 */
		public function make_dynamic_xsl() {
			// Make dynamic xsl file.
			if ( preg_match( '#(/sitemap\.xsl)$#i', $_SERVER['REQUEST_URI'] ) ) {
				$blog_charset = get_option( 'blog_charset' );
				header( "Content-Type: text/xml; charset=$blog_charset", true );
				include_once AIOSEOP_PLUGIN_DIR . '/inc/sitemap-xsl.php';
				exit();
			}
		}

		/**
		 * Get Sitemap Data
		 *
		 * @since ?
		 *
		 * @param     $sitemap_type
		 * @param int $page
		 * @return array
		 */
		public function get_sitemap_data( $sitemap_type, $page = 0 ) {
			$sitemap_data = array();

			if ( 0 === strpos( $sitemap_type, 'rss' ) ) {
				$sitemap_data = $this->get_sitemap_without_indexes();
			} elseif ( $this->options[ "{$this->prefix}indexes" ] ) {
				$posttypes = $this->options[ "{$this->prefix}posttypes" ];
				if ( empty( $posttypes ) ) {
					$posttypes = array();
				}
				$taxonomies = $this->options[ "{$this->prefix}taxonomies" ];
				if ( empty( $taxonomies ) ) {
					$taxonomies = array();
				}
				if ( 'root' === $sitemap_type ) {
					$sitemap_data = array_merge( $this->get_sitemap_index_filenames() );
				} elseif ( 'addl' === $sitemap_type ) {
					$sitemap_data = $this->get_addl_pages();
				} elseif ( 'archive' === $sitemap_type && $this->option_isset( 'archive' ) ) {
					$sitemap_data = $this->get_date_archive_prio_data();
				} elseif ( 'author' === $sitemap_type && $this->option_isset( 'author' ) ) {
					$sitemap_data = $this->get_author_prio_data();
				} elseif ( in_array( $sitemap_type, $posttypes ) ) {
					// TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison.
					$sitemap_data = $this->get_all_post_priority_data( $sitemap_type, 'publish', $page );
				} elseif ( in_array( $sitemap_type, $taxonomies ) ) {
					// TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison.
					$sitemap_data = $this->get_term_priority_data( get_terms( $this->get_tax_args( (array) $sitemap_type, $page ) ) );
				} else {
					// TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison.
					if ( is_array( $this->extra_sitemaps ) && in_array( $sitemap_type, $this->extra_sitemaps ) ) {
						$sitemap_data = apply_filters( $this->prefix . 'custom_' . $sitemap_type, $sitemap_data, $page, $this_options );
					}
				}
			} elseif ( 'root' === $sitemap_type ) {
				$sitemap_data = $this->get_sitemap_without_indexes();
			}

			return apply_filters( $this->prefix . 'data', $sitemap_data, $sitemap_type, $page, $this->options );
		}

		/**
		 * Do Rewrite Sitemap
		 *
		 * Output sitemaps dynamically based on rewrite rules.
		 *
		 * @since ?
		 * @since 3.0 Return a (string) value. #2190
		 *
		 * @param     $sitemap_type
		 * @param int $page
		 * @return string
		 */
		public function do_rewrite_sitemap( $sitemap_type, $page = 0 ) {
			$this->add_post_types();
			$comment = 'dynamically';
			// TODO Add esc_* or wp_kses function.
			return $this->do_build_sitemap( $sitemap_type, $page, '', $comment );
		}

		/**
		 * Get Sitemap URL
		 *
		 * Build a url to the sitemap.
		 *
		 * @since 2.3.6
		 * @since 2.3.12.3 Refactored to use aioseop_home_url() for compatibility purposes.
		 *
		 * @return string
		 */
		public function get_sitemap_url() {

			$url = aioseop_home_url( '/' . $this->get_filename() . '.xml' );

			return $url;
		}

		/**
		 * Do Notify
		 *
		 * Notify search engines, do logging.
		 *
		 * @since ?
		 */
		public function do_notify() {

			if ( '0' === get_option( 'blog_public' ) ) {
				// Don't ping search engines if blog is set to not public.
				return;
			}

			if ( apply_filters( 'aioseo_sitemap_ping', true ) === false ) {
				// API filter hook to disable sending sitemaps to search engines.
				return;
			}

			$notify_url = array(
				'google' => 'https://www.google.com/ping?sitemap=',
				'bing'   => 'https://www.bing.com/ping?sitemap=',
			);

			$notify_url = apply_filters( 'aioseo_sitemap_ping_urls', $notify_url );

			$url = $this->get_sitemap_url();
			if ( ! empty( $url ) ) {
				foreach ( $notify_url as $k => $v ) {
					// TODO Change urlencode() to rawurlencode().
					// @link ( http://php.net/manual/en/function.rawurlencode.php ).
					// @link ( http://www.faqs.org/rfcs/rfc3986.html ).
					$response = wp_remote_get( $notify_url[ $k ] . urlencode( $url ) );
					if ( is_array( $response ) && ! empty( $response['response'] ) && ! empty( $response['response']['code'] ) ) {
						if ( 200 !== intval( $response['response']['code'] ) ) {
							/* translators: Notifies the admin which sitemaps failed to notify with which search engine(s), and display the error code. */
							$this->debug_message( sprintf( __( 'Failed to notify %1$s about changes to your sitemap at %2$s, error code %3$s.', 'all-in-one-seo-pack' ), $k, $url, $response['response']['code'] ) );
						}
					} else {
						/* translators: Notifies the admin which sitemaps failed to notify with which search engine(s). */
						$this->debug_message( sprintf( __( 'Failed to notify %1$s about changes to your sitemap at %2$s, unable to access via wp_remote_get().', 'all-in-one-seo-pack' ), $k, $url ) );
					}
				}
			}
		}

		/**
		 * Do Robots
		 *
		 * Add Sitemap parameter to virtual robots.txt file.
		 *
		 * @since ?
		 */
		public function do_robots() {
			$url = $this->get_sitemap_url();
			// TODO Add esc_* or wp_kses function.
			echo "\nSitemap: $url\n";
		}

		/**
		 * Do Sitemaps
		 *
		 * Build static sitemaps on submit if rewrite rules are not in use, do logging.
		 *
		 * @since ?
		 *
		 * @param string $message
		 */
		public function do_sitemaps( $message = '' ) {
			if ( defined( 'AIOSEOP_UNIT_TESTING' ) ) {
				$aioseop_options = aioseop_get_options();
				$this->options   = $aioseop_options['modules'][ "{$this->prefix}options" ];
			}

			if ( ! empty( $this->options[ "{$this->prefix}indexes" ] ) ) {
				if ( $this->options[ "{$this->prefix}max_posts" ] && ( $this->options[ "{$this->prefix}max_posts" ] > 0 ) && ( $this->options[ "{$this->prefix}max_posts" ] < 50000 ) ) {
					$this->max_posts = $this->options[ "{$this->prefix}max_posts" ];
				} else {
					$this->max_posts = 50000;
				}
			} else {
				$this->max_posts = 50000;
			}
			if ( ! $this->options[ "{$this->prefix}rewrite" ] ) {
				if ( $this->options[ "{$this->prefix}indexes" ] ) {
					$this->do_indexed_sitemaps();
				} else {
					$this->log_start();

					$comment = sprintf( "file '%s' statically", $this->get_filename() );
					$sitemap = $this->do_simple_sitemap( $comment );
					$this->write_sitemaps( $this->get_filename(), $sitemap );

					if ( $this->options[ "{$this->prefix}rss_sitemap" ] ) {
						$rss = $this->do_simple_sitemap_rss( $comment );
						$this->write_sitemaps( $this->get_filename(), $rss, '.rss' );
					}

					$this->log_stats( 'root', false );
				}
			} else {
				delete_transient( "{$this->prefix}rules_flushed" );
			}
			$this->do_notify();
			if ( ! empty( $message ) && is_string( $message ) ) {
				$this->debug_message( $message );
			}
		}

		/**
		 * Add XML Mime Type.
		 *
		 * @since ?
		 *
		 * @param $mime
		 * @return mixed
		 */
		public function add_xml_mime_type( $mime ) {
			if ( ! empty( $mime ) ) {
				$mime['xml'] = 'text/xml';
			}

			return $mime;
		}

		/**
		 * Write Sitemaps
		 *
		 * Write multiple sitemaps to the filesystem.
		 *
		 * @since ?
		 *
		 * @param $filename
		 * @param $contents
		 */
		public function write_sitemaps( $filename, $contents, $extn = '.xml' ) {
			$this->write_sitemap( $filename . $extn, $contents );
		}

		/**
		 * Write Sitemap
		 *
		 * Write a single sitemap to the filesystem.
		 *
		 * @since ?
		 * @since 3.0 Removed $gzip in issue #534
		 *
		 * @param      $filename
		 * @param      $contents
		 * @return bool
		 */
		public function write_sitemap( $filename, $contents ) {
			add_filter( 'upload_mimes', array( $this, 'add_xml_mime_type' ) );
			$filename = $this->get_home_path() . sanitize_file_name( $filename );
			remove_filter( 'upload_mimes', array( $this, 'add_xml_mime_type' ) );

			return $this->save_file( $filename, $contents );
		}

		/**
		 * Gets Default Values
		 *
		 * Helper function for handling default values.
		 *
		 * @since ?
		 *
		 * @param        $defaults
		 * @param        $prefix
		 * @param        $cache
		 * @param        $item
		 * @param bool   $nodefaults
		 * @param string $type
		 * @return bool
		 */
		public function get_default_values( $defaults, $prefix, &$cache, $item, $nodefaults = false, $type = '' ) {
			if ( ! empty( $cache[ $item . $type ] ) ) {
				return $cache[ $item . $type ];
			}
			if ( ! empty( $defaults[ $item ] ) ) {
				$field = $this->prefix . $prefix . $item;
				if ( $this->option_isset( $prefix . $item ) && 'no' !== $this->options[ $field ] ) {
					if ( ( 'sel' === $this->options[ $field ] ) && ! empty( $type ) && isset( $this->options[ $this->prefix . $prefix . $item . '_' . $type ] ) ) {
						if ( 'no' === $this->options[ $this->prefix . $prefix . $item . '_' . $type ] ) {
							return false;
						}
						if ( 'sel' === $this->options[ $this->prefix . $prefix . $item . '_' . $type ] ) {
							return false;
						}
						$cache[ $item . $type ] = $this->options[ $this->prefix . $prefix . $item . '_' . $type ];
					} else {
						if ( 'no' === $this->options[ $field ] ) {
							return false;
						}
						if ( 'sel' === $this->options[ $field ] ) {
							return false;
						}
						$cache[ $item . $type ] = $this->options[ $field ];
					}

					return $cache[ $item . $type ];
				}
				if ( $nodefaults ) {
					return false;
				}

				return $defaults[ $item ];
			}

			return false;
		}

		/**
		 * Get Default Priority
		 *
		 * Get priority settings for sitemap entries.
		 *
		 * @since ?
		 *
		 * @param        $item
		 * @param bool   $nodefaults
		 * @param string $type
		 * @return bool
		 */
		public function get_default_priority( $item, $nodefaults = false, $type = '' ) {
			$defaults = array(
				'homepage'   => '1.0',
				'blog'       => '0.9',
				'sitemap'    => '0.8',
				'post'       => '0.7',
				'archive'    => '0.5',
				'author'     => '0.3',
				'taxonomies' => '0.3',
			);

			static $cache = array();

			return $this->get_default_values( $defaults, 'prio_', $cache, $item, $nodefaults, $type );
		}

		/**
		 * Get Default Frequency
		 *
		 * Get frequency settings for sitemap entries.
		 *
		 * @since ?
		 *
		 * @param        $item
		 * @param bool   $nodefaults
		 * @param string $type
		 * @return bool
		 */
		public function get_default_frequency( $item, $nodefaults = false, $type = '' ) {
			$defaults = array(
				'homepage'   => 'always',
				'blog'       => 'daily',
				'sitemap'    => 'hourly',
				'post'       => 'weekly',
				'archive'    => 'monthly',
				'author'     => 'weekly',
				'taxonomies' => 'monthly',
			);

			static $cache = array();

			return $this->get_default_values( $defaults, 'freq_', $cache, $item, $nodefaults, $type );
		}

		/**
		 * Get Sitemaps Index Filenames
		 *
		 * Build an index of sitemaps used.
		 *
		 * @since 2.3.6
		 * @since 2.3.12.3 Refactored to use aioseop_home_url() for compatibility purposes.
		 * @since 3.0 Changed to exclude noindex post types. #1382
		 * @return array
		 */
		public function get_sitemap_index_filenames() {
			global $aioseop_options;
			$files   = array();
			$options = $this->options;
			$prefix  = $this->get_filename();
			$suffix  = '.xml';

			if ( empty( $options[ "{$this->prefix}posttypes" ] ) ) {
				$options[ "{$this->prefix}posttypes" ] = array();
			}
			if ( empty( $options[ "{$this->prefix}taxonomies" ] ) ) {
				$options[ "{$this->prefix}taxonomies" ] = array();
			}
			$options[ "{$this->prefix}posttypes" ]  = array_diff( $options[ "{$this->prefix}posttypes" ], array( 'all' ) );
			$options[ "{$this->prefix}taxonomies" ] = $this->show_or_hide_taxonomy( array_diff( $options[ "{$this->prefix}taxonomies" ], array( 'all' ) ) );

			$files[] = array( 'loc' => aioseop_home_url( '/addl-' . $prefix . $suffix ) );

			// Get post types selected, and NoIndex post types & Index posts.
			$post_types = $options[ "{$this->prefix}posttypes" ];
			if ( is_array( $aioseop_options['aiosp_cpostnoindex'] ) ) {
				foreach ( $post_types as $index => $post_type ) {
					if ( in_array( $post_type, $aioseop_options['aiosp_cpostnoindex'], true ) ) {
						$args = array(
							'post_type'      => $post_type,
							'fields'         => 'ids',
							'posts_per_page' => 1,
							'meta_query'     => array(
								'relation' => 'OR',
								array(
									'key'     => '_aioseop_noindex',
									'value'   => 'off',
									'compare' => '=',
								),
							),
						);

						$q = new WP_Query( $args );
						if ( 0 === $q->post_count ) {
							unset( $post_types[ $index ] );
						}
					}
				}
			}

			if ( ! empty( $post_types ) ) {
				$prio = $this->get_default_priority( 'post' );
				$freq = $this->get_default_frequency( 'post' );

				// Get post counts from posts type. Exclude if NoIndex is on, and does not contain excluded terms.
				$args = array(
					'post_type'   => $post_types,
					'post_status' => 'publish',
					'meta_query'  => array(
						'relation' => 'OR',
						array(
							'key'     => '_aioseop_noindex',
							'value'   => 'on',
							'compare' => '!=',
						),
						array(
							'key'     => '_aioseop_noindex',
							'compare' => 'NOT EXISTS',
						),
					),
				);
				if ( $this->option_isset( 'excl_terms' ) ) {
					// Adds excluded terms to exclude from query.
					foreach ( $this->options[ $this->prefix . 'excl_terms' ] as $k1_taxonomy => $v1_tax_terms ) {
						if ( ! isset( $args['tax_query'] ) ) {
							$args['tax_query'] = array(
								'relation' => 'AND',
							);
						}
						$args['tax_query'][] = array(
							'taxonomy' => $k1_taxonomy,
							'terms'    => $v1_tax_terms['terms'],
							'operator' => 'NOT IN',
						);
					}
				}
				$post_counts = $this->get_all_post_counts( $args );

				foreach ( $post_types as $sm ) {
					if ( 0 === intval( $post_counts[ $sm ] ) ) {
						continue;
					}
					if ( ! empty( $this->options[ "{$this->prefix}indexes" ] ) ) {
						if ( $post_counts[ $sm ] > $this->max_posts ) {
							$count = 1;
							for ( $post_count = 0; $post_count < $post_counts[ $sm ]; $post_count += $this->max_posts ) {
								$files[] = array(
									'loc'        => aioseop_home_url( '/' . $sm . '-' . $prefix . ( $count ++ ) . $suffix ),
									'changefreq' => $freq,
									'priority'   => $prio,
								);
							}
						} else {
							$files[] = array(
								'loc'        => aioseop_home_url( '/' . $sm . '-' . $prefix . $suffix ),
								'changefreq' => $freq,
								'priority'   => $prio,
							);
						}
					} else {
						$files[] = array(
							'loc'        => aioseop_home_url( '/' . $sm . '-' . $prefix . $suffix ),
							'changefreq' => $freq,
							'priority'   => $prio,
						);
					}
				}
			}
			if ( $this->option_isset( 'archive' ) ) {
				$files[] = array(
					'loc'        => aioseop_home_url( '/' . 'archive-' . $prefix . $suffix ),
					'changefreq' => $this->get_default_frequency( 'archive' ),
					'priority'   => $this->get_default_priority( 'archive' ),
				);
			}
			if ( $this->option_isset( 'author' ) ) {
				$files[] = array(
					'loc'        => aioseop_home_url( '/' . 'author-' . $prefix . $suffix ),
					'changefreq' => $this->get_default_frequency( 'author' ),
					'priority'   => $this->get_default_priority( 'author' ),
				);
			}

			if ( ! empty( $options[ "{$this->prefix}taxonomies" ] ) ) {
				foreach ( $options[ "{$this->prefix}taxonomies" ] as $v1_taxonomy ) {
					$tax_args           = $this->get_tax_args( array( $v1_taxonomy ) );
					$tax_args['fields'] = 'count';

					$term_count = get_terms( $tax_args );
					if ( ! is_wp_error( $term_count ) && ( $term_count > 0 ) ) {
						if ( ! empty( $this->options[ "{$this->prefix}indexes" ] ) ) {
							if ( $term_count > $this->max_posts ) {
								$count = 1;
								for ( $tc = 0; $tc < $term_count; $tc += $this->max_posts ) {
									$files[] = array(
										'loc'        => aioseop_home_url( '/' . $v1_taxonomy . '-' . $prefix . ( $count ++ ) . $suffix ),
										'changefreq' => $this->get_default_frequency( 'taxonomies' ),
										'priority'   => $this->get_default_priority( 'taxonomies' ),
									);
								}
							} else {
								$files[] = array(
									'loc'        => aioseop_home_url( '/' . $v1_taxonomy . '-' . $prefix . $suffix ),
									'changefreq' => $this->get_default_frequency( 'taxonomies' ),
									'priority'   => $this->get_default_priority( 'taxonomies' ),
								);
							}
						} else {
							$files[] = array(
								'loc'        => aioseop_home_url( '/' . $v1_taxonomy . '-' . $prefix . $suffix ),
								'changefreq' => $this->get_default_frequency( 'taxonomies' ),
								'priority'   => $this->get_default_priority( 'taxonomies' ),
							);
						}
					}
				}
			}
			foreach ( $this->get_child_sitemap_urls() as $csm ) {
				$files[] = array(
					'loc'        => $csm,
					'changefreq' => $this->get_default_frequency( 'sitemap' ),
					'priority'   => $this->get_default_priority( 'sitemap' ),
				);
			}

			$files = apply_filters( 'aioseop_sitemap_index_filenames', $files, $prefix, $suffix );

			// Remove Additional Pages index if all pages are static and no extra pages are specified.
			if ( ! $this->does_addl_sitemap_contain_urls() ) {
				$page_to_remove = array( get_site_url() . '/addl-sitemap.xml' );
				$files          = $this->remove_urls_from_sitemap_page( $files, $page_to_remove );
			}

			return $files;
		}

		/**
		 * The does_addl_sitemap_contain_urls() function.
		 *
		 * Checks whether the Additional Pages index will contain URLs.
		 * This will not be the case if there is both a static homepage/posts page and there are no additional pages specified.
		 *
		 * @since 3.2.0
		 *
		 * @return bool
		 */
		private function does_addl_sitemap_contain_urls() {
			$is_addl_pages = ! empty( $this->options['aiosp_sitemap_addl_pages'] );
			if ( ! $is_addl_pages && ( 'page' === get_option( 'show_on_front' ) ) ) {
					return false;
			}
			return true;
		}

		/**
		 * Build the Sitemap
		 *
		 * @since ?
		 *
		 * @param        $sitemap_type
		 * @param int    $page
		 * @param string $filename
		 * @param string $comment
		 * @return string
		 */
		public function do_build_sitemap( $sitemap_type, $page = 0, $filename = '', $comment = '' ) {
			if ( empty( $filename ) ) {
				switch ( $sitemap_type ) {
					case 'root':
						// fall-through.
					case 'rss':
						// fall-through.
					case 'rss_latest':
						$filename = $this->get_filename();
						break;
					default:
						$filename = $this->get_filename() . '_' . $sitemap_type;
						break;
				}
			}
			if ( empty( $comment ) ) {
				$comment = "file '%s' statically";
			}
			$sitemap_data = $this->get_sitemap_data( $sitemap_type, $page );
			if ( ( 'root' === $sitemap_type ) && ! empty( $this->options[ "{$this->prefix}indexes" ] ) ) {
				return $this->build_sitemap_index( $sitemap_data, sprintf( $comment, $filename ) );
			} else {
				if ( empty( $sitemap_data ) ) {
					return '';
				}
				return $this->build_sitemap( $sitemap_data, $sitemap_type, sprintf( $comment, $filename ) );
			}
		}

		/**
		 * Do Write Sitemaps
		 *
		 * Write the sitemap.
		 *
		 * @since ?
		 *
		 * @param        $sitemap_type
		 * @param int    $page
		 * @param string $filename
		 * @param string $comment
		 */
		public function do_write_sitemap( $sitemap_type, $page = 0, $filename = '', $comment = '' ) {
			if ( empty( $filename ) ) {
				if ( 'root' === $sitemap_type ) {
					$filename = $this->get_filename();
				} else {
					$filename = $sitemap_type . '-' . $this->get_filename();
				}
			}
			if ( empty( $comment ) ) {
				$comment = "file '%s' statically";
			}
			$this->write_sitemaps( $filename, $this->do_build_sitemap( $sitemap_type, $page, $filename, $comment ) );
		}

		/**
		 * Do Indexed Sitemaps
		 *
		 * Build all the indexes.
		 *
		 * @since ?
		 */
		public function do_indexed_sitemaps() {
			$this->start_memory_usage = memory_get_peak_usage();
			$options                  = $this->options;

			$this->do_write_sitemap( 'root' );
			$this->do_write_sitemap( 'addl' );

			if ( $this->option_isset( 'archive' ) ) {
				$this->do_write_sitemap( 'archive' );
			}
			if ( $this->option_isset( 'author' ) ) {
				$this->do_write_sitemap( 'author' );
			}

			if ( ( ! isset( $options[ "{$this->prefix}posttypes" ] ) ) || ( ! is_array( $options[ "{$this->prefix}posttypes" ] ) ) ) {
				$options[ "{$this->prefix}posttypes" ] = array();
			}
			if ( ( ! isset( $options[ "{$this->prefix}taxonomies" ] ) ) || ( ! is_array( $options[ "{$this->prefix}taxonomies" ] ) ) ) {
				$options[ "{$this->prefix}taxonomies" ] = array();
			}
			$options[ "{$this->prefix}posttypes" ]  = array_diff( $options[ "{$this->prefix}posttypes" ], array( 'all' ) );
			$options[ "{$this->prefix}taxonomies" ] = array_diff( $options[ "{$this->prefix}taxonomies" ], array( 'all' ) );

			if ( ! empty( $options[ "{$this->prefix}posttypes" ] ) ) {
				$post_counts = $this->get_all_post_counts(
					array(
						'post_type'   => $options[ "{$this->prefix}posttypes" ],
						'post_status' => 'publish',
					)
				);
				foreach ( $options[ "{$this->prefix}posttypes" ] as $posttype ) {
					if ( 0 === $post_counts[ $posttype ] ) {
						continue;
					}
					if ( ! empty( $this->options[ "{$this->prefix}indexes" ] ) && ( $post_counts[ $posttype ] > $this->max_posts ) ) {
						$count = 1;
						for ( $post_count = 0; $post_count < $post_counts[ $posttype ]; $post_count += $this->max_posts ) {
							$this->do_write_sitemap( $posttype, $count - 1, "{$posttype}-" . $this->get_filename() . "{$count}" );
							$count ++;
						}
					} else {
						$this->do_write_sitemap( $posttype );
					}
				}
			}

			if ( ! empty( $options[ "{$this->prefix}taxonomies" ] ) ) {
				foreach ( $options[ "{$this->prefix}taxonomies" ] as $taxonomy ) {
					$term_count = wp_count_terms( $taxonomy, array( 'hide_empty' => true ) );
					if ( ! is_wp_error( $term_count ) && ( $term_count > 0 ) ) {
						if ( ! empty( $this->options[ "{$this->prefix}indexes" ] ) ) {
							if ( $term_count > $this->max_posts ) {
								$count = 1;
								for ( $tc = 0; $tc < $term_count; $tc += $this->max_posts ) {
									$this->do_write_sitemap( $taxonomy, $tc, "{$taxonomy}-" . $this->get_filename() . "{$count}" );
									$count ++;
								}
							} else {
								$this->do_write_sitemap( $taxonomy );
							}
						} else {
							$this->do_write_sitemap( $taxonomy );
						}
					}
				}
			}
			$this->log_stats( 'indexed', false );
		}

		/**
		 * Remove Posts Page
		 *
		 * @since 2.3.11
		 *
		 * @param $postspageid
		 * @return bool
		 */
		public function remove_posts_page( $postspageid ) {
			// TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison.
			if ( in_array( $postspageid, $this->excludes ) ) {
				return true;
			}

			// TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison.
			if ( in_array( get_post_field( 'post_name', $postspageid ), $this->excludes ) ) {
				return true;
			}

			return false;
		}

		/**
		 * Remove Homepage
		 *
		 * @since 2.3.11
		 *
		 * @param $homepage_id
		 * @return bool
		 */
		public function remove_homepage( $homepage_id ) {
			// TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison.
			if ( in_array( $homepage_id, $this->excludes ) ) {
				return true;
			}

			// TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison.
			if ( in_array( get_post_field( 'post_name', $homepage_id ), $this->excludes ) ) {
				return true;
			}

			return false;
		}

		/**
		 * The get_sitemap_without_indexes() function.
		 *
		 * Fetches data for sitemap without indexes.
		 *
		 * @since 2.3.6
		 * @since 2.3.12.3 Refactored to use aioseop_home_url() for compatibility purposes.
		 * @since 3.2.0 Improved function and variable naming.
		 *
		 * @return array
		 */
		public function get_sitemap_without_indexes() {
			$child_urls = $this->get_child_sitemap_urls();
			$options    = $this->options;

			if ( is_array( $options[ "{$this->prefix}posttypes" ] ) ) {
				$options[ "{$this->prefix}posttypes" ] = array_diff( $options[ "{$this->prefix}posttypes" ], array( 'all' ) );
			}
			if ( is_array( $options[ "{$this->prefix}taxonomies" ] ) ) {
				$options[ "{$this->prefix}taxonomies" ] = array_diff( $options[ "{$this->prefix}taxonomies" ], array( 'all' ) );
			}

			$urls = $this->get_all_post_priority_data( $options[ "{$this->prefix}posttypes" ] );

			// It's 0 if posts are on homepage, otherwise it's the id of the posts page.
			$posts       = (int) get_option( 'page_for_posts' );
			$postspageid = (int) get_option( 'page_for_posts' );

			$home = array(
				'loc'         => aioseop_home_url(),
				'changefreq'  => $this->get_default_frequency( 'homepage' ),
				'priority'    => $this->get_default_priority( 'homepage' ),
				'image:image' => $this->get_images_from_post( (int) get_option( 'page_on_front' ) ),
			);

			if ( $posts ) {
				$posts = $this->get_permalink( $posts );
				if ( $posts === $home['loc'] ) {
					$posts = null;
				} else {
					$posts = array(
						'loc'        => $posts,
						'changefreq' => $this->get_default_frequency( 'blog' ),
						'priority'   => $this->get_default_priority( 'blog' ),
					);
				}
			}

			if ( $this->option_isset( 'archive' ) ) {
				$urls = array_merge( $urls, $this->get_date_archive_prio_data() );
			}
			if ( $this->option_isset( 'author' ) ) {
				$urls = array_merge( $urls, $this->get_author_prio_data() );
			}
			foreach ( $urls as $k => $p ) {
				if ( untrailingslashit( $p['loc'] ) === untrailingslashit( $home['loc'] ) ) {
					$urls[ $k ]['priority'] = '1.0';
					$home                   = null;
					break;
				}
			}
			if ( ( null !== $posts ) && isset( $posts['loc'] ) ) {
				foreach ( $urls as $k => $p ) {
					if ( $p['loc'] === $posts['loc'] ) {
						$urls[ $k ]['changefreq'] = $this->get_default_frequency( 'blog' );
						$urls[ $k ]['priority']   = $this->get_default_priority( 'blog' );
						$posts                    = null;
						break;
					}
				}
			}
			if ( is_array( $posts ) && $this->remove_posts_page( $postspageid ) !== true ) {
				array_unshift( $urls, $posts );
			}

			if ( is_array( $home ) ) {
				array_unshift( $urls, $home );
			}
			$terms = get_terms( $this->get_tax_args( $options[ "{$this->prefix}taxonomies" ] ) );
			$urls2 = $this->get_term_priority_data( $terms );
			$urls3 = $this->get_addl_pages_only();
			$urls  = array_merge( $child_urls, $urls, $urls2, $urls3 );
			if ( is_array( $this->extra_sitemaps ) ) {
				foreach ( $this->extra_sitemaps as $sitemap_type ) {
					$sitemap_data = array();
					$sitemap_data = apply_filters( $this->prefix . 'custom_' . $sitemap_type, $sitemap_data, $page, $this_options );
					$urls         = array_merge( $urls, $sitemap_data );
				}
			}

			$urls = $this->get_homepage_timestamp( $urls );
			$urls = $this->get_posts_page_timestamp( $urls );

			return $urls;
		}

		/**
		 * Do Simple Sitemap
		 *
		 * Build a single, stand-alone sitemap without indexes.
		 *
		 * @since ?
		 *
		 * @param string $comment
		 * @return string
		 */
		public function do_simple_sitemap( $comment = '' ) {
			$sitemap_data = $this->get_sitemap_without_indexes();
			$sitemap_data = apply_filters( $this->prefix . 'data', $sitemap_data, 'root', 0, $this->options );

			return $this->build_sitemap( $sitemap_data, '', $comment );
		}

		/**
		 * Do Simple Sitemap RSS
		 *
		 * Build a single stand-alone RSS sitemap without indexes.
		 *
		 * @since 2.9
		 *
		 * @param string $comment
		 * @return string
		 */
		public function do_simple_sitemap_rss( $comment = '' ) {
			$sitemap_data = $this->get_sitemap_without_indexes();
			$sitemap_data = apply_filters( $this->prefix . 'data', $sitemap_data, 'rss', 0, $this->options );

			return $this->build_sitemap( $sitemap_data, 'rss', $comment );
		}

		/**
		 * Get Sitemap XSL
		 *
		 * Gets the sitemap URL.
		 *
		 * Has a filter for using something other than the dynamically generated one.
		 * Using the filter you need the full path to the custom xsl file.
		 *
		 * @since 2.3.6
		 * @since 2.3.12.3 Refactored to use aioseop_home_url() for compatibility purposes.
		 *
		 * @see   https://semperplugins.com/documentation/aioseop_sitemap_xsl_url/
		 */
		public function get_sitemap_xsl() {

			return esc_url( apply_filters( 'aioseop_sitemap_xsl_url', aioseop_home_url( '/sitemap.xsl' ) ) );
		}

		/**
		 * Output RSS
		 *
		 * Output the RSS for a sitemap, full or latest.
		 *
		 * @since 2.9
		 *
		 * @param        $urls
		 * @param string $sitemap_type The type of RSS sitemap viz. rss or rss_latest.
		 * @param string $comment
		 */
		private function output_rss( $urls, $sitemap_type, $comment ) {
			echo '<?xml version="1.0" encoding="UTF-8"?>' . "\r\n\r\n";
			// TODO Add esc_* function.
			echo '<!-- ' . sprintf( $this->comment_string, $comment, AIOSEOP_VERSION, date( 'D, d M Y H:i:s e' ) ) . " -->\r\n";

			echo '<rss version="2.0"><channel>';
			if ( is_multisite() ) {
				// TODO Add esc_* function.
				echo
					'<title>' . aiosp_common::make_xml_safe( 'title', get_blog_option( get_current_blog_id(), 'blogname' ) ) . '</title>' .
					'<link>' . aiosp_common::make_xml_safe( 'link', get_blog_option( get_current_blog_id(), 'siteurl' ) ) . '</link>' .
					'<description>' . aiosp_common::make_xml_safe( 'description', get_blog_option( get_current_blog_id(), 'blogdescription' ) ) . '</description>';
			} else {
				// TODO Add esc_* function.
				echo
					'<title>' . aiosp_common::make_xml_safe( 'title', get_option( 'blogname' ) ) . '</title>' .
					'<link>' . aiosp_common::make_xml_safe( 'link', get_option( 'siteurl' ) ) . '</link>' .
					'<description>' . aiosp_common::make_xml_safe( 'description', get_option( 'blogdescription' ) ) . '</description>';
			}

			// remove urls that do not have the rss element.
			$urls = array_filter( $urls, array( $this, 'include_in_rss' ) );

			if ( false !== strpos( $sitemap_type, 'latest' ) ) {
				// let's sort the array in descending order of date.
				uasort( $urls, array( $this, 'sort_modifed_date_descending' ) );
				$urls = array_slice( $urls, 0, apply_filters( $this->prefix . 'rss_latest_limit', 20 ) );
			}

			foreach ( $urls as $url ) {
				// TODO Add esc_* function.
				echo
				'<item>' .
					'<guid>' . aiosp_common::make_xml_safe( 'guid', $url['loc'] ) . '</guid>' .
					'<title>' . aiosp_common::make_xml_safe( 'title', $url['rss']['title'] ) . '</title>' .
					'<link>' . aiosp_common::make_xml_safe( 'link', $url['loc'] ) . '</link>' .
					// TODO Add esc_* or wp_kses function.
					'<description><![CDATA[' . $url['rss']['description'] . ']]></description>' .
					'<pubDate>' . aiosp_common::make_xml_safe( 'pubDate', $url['rss']['pubDate'] ) . '</pubDate>' .
				'</item>';
			}
			echo '</channel></rss>';
		}

		/**
		 * Include in RSS
		 *
		 * Remove elements not containing the rss element.
		 *
		 * @since 2.9
		 *
		 * @param $array
		 * @return bool
		 */
		public function include_in_rss( $array ) {
			return isset( $array['rss'] );
		}

		/**
		 * Sort Modified Date Descending
		 *
		 * Sort on the basis of modified date.
		 *
		 * @since 2.9
		 *
		 * @param $array1
		 * @param $array2
		 * @return bool|int
		 */
		public function sort_modifed_date_descending( $array1, $array2 ) {
			if ( ! isset( $array1['rss'] ) || ! isset( $array2['rss'] ) ) {
				return 0;
			}
			return $array1['rss']['timestamp'] < $array2['rss']['timestamp'];
		}

		/**
		 * Output Sitemap
		 *
		 * Output the XML for a sitemap.
		 *
		 * @since ?
		 *
		 * @param        $urls
		 * @param string $sitemap_type The type of sitemap viz. root, rss, rss_latest etc.. For static sitemaps, this would be empty.
		 * @param string $comment
		 * @return null
		 */
		private function output_sitemap( $urls, $sitemap_type, $comment = '' ) {
			if ( 0 === strpos( $sitemap_type, 'rss' ) ) {
				// starts with rss.
				$this->output_rss( $urls, $sitemap_type, $comment );
				return;
			}

			$max_items = 50000;
			if ( ! is_array( $urls ) ) {
				return null;
			}
			echo '<?xml version="1.0" encoding="UTF-8"?>' . "\r\n\r\n";
			// TODO Add esc_* function.
			echo '<!-- ' . sprintf( $this->comment_string, $comment, AIOSEOP_VERSION, date( 'D, d M Y H:i:s e' ) ) . " -->\r\n";
			$plugin_path  = $this->plugin_path['url'];
			$plugin_url   = wp_parse_url( $plugin_path );
			$current_host = $_SERVER['HTTP_HOST'];
			if ( empty( $current_host ) ) {
				$current_host = $_SERVER['SERVER_NAME'];
			}

			if ( ! empty( $current_host ) && ( $current_host !== $plugin_url['host'] ) ) {
				$plugin_url['host'] = $current_host;
			}

			// Code `unset( $plugin_url['scheme'] )`.
			$plugin_path = $this->unparse_url( $plugin_url );

			// Using the filter you need the full path to the custom xsl file.
			$xsl_url = $this->get_sitemap_xsl();

			$xml_header = '<?xml-stylesheet type="text/xsl" href="' . $xsl_url . '"?>' . "\r\n" . '<urlset ';
			$namespaces = apply_filters(
				$this->prefix . 'xml_namespace',
				array(
					'xmlns'       => 'http://www.sitemaps.org/schemas/sitemap/0.9',
					'xmlns:image' => 'http://www.google.com/schemas/sitemap-image/1.1',
				)
			);
			if ( ! empty( $namespaces ) ) {
				$ns = array();
				foreach ( $namespaces as $k => $v ) {
					$ns[] = esc_attr( $k ) . '="' . esc_url( $v, array( 'http', 'https' ) ) . '"';
				}
				$xml_header .= join( "\r\n\t", $ns );
			}
			$xml_header .= '>' . "\r\n";
			// TODO Add esc_* function.
			echo $xml_header;
			$count = 0;
			foreach ( $urls as $url ) {
				echo "\t<url>\r\n";
				if ( is_array( $url ) ) {
					if ( isset( $url['rss'] ) ) {
						unset( $url['rss'] );
					}
					foreach ( $url as $k => $v ) {
						if ( ! empty( $v ) ) {
							$v = aiosp_common::make_xml_safe( $k, $v );
							if ( is_array( $v ) ) {
								$buf = "\t\t\t<$k>\r\n";
								foreach ( $v as $ext => $attr ) {
									if ( is_array( $attr ) ) {
										$buf = '';
										// TODO Add esc_* function.
										echo "\t\t<$k>\r\n";
										foreach ( $attr as $a => $nested ) {
											if ( is_array( $nested ) ) {
												// TODO Add esc_* function.
												echo "\t\t\t<$a>\r\n";
												foreach ( $nested as $next => $nattr ) {
													$value = aiosp_common::make_xml_safe( $next, $nattr );
													// TODO Add esc_* function.
													echo "\t\t\t\t<$next>$value</$next>\r\n";
												}
												// TODO Add esc_* function.
												echo "\t\t\t</$a>\r\n";
											} else {
												$value = aiosp_common::make_xml_safe( $a, $nested );
												// TODO Add esc_* function.
												echo "\t\t\t<$a>$value</$a>\r\n";
											}
										}
										// TODO Add esc_* function.
										echo "\t\t</$k>\r\n";
									} else {
										$value = aiosp_common::make_xml_safe( $ext, $attr );
										$buf  .= "\t\t\t<$ext>$value</$ext>\r\n";
									}
								}
								if ( ! empty( $buf ) ) {
									// TODO Add esc_* function.
									echo $buf . "\t\t</$k>\r\n";
								}
							} else {
								$value = aiosp_common::make_xml_safe( $k, $v );
								// TODO Add esc_* function.
								echo "\t\t<$k>$value</$k>\r\n";
							}
						}
					}
				} else {
					$value = aiosp_common::make_xml_safe( 'loc', $url );
					// TODO Add esc_* function.
					echo "\t\t<loc>$value</loc>\r\n";
				}
				echo "\t</url>\r\n";
				if ( $count >= $max_items ) {
					break;
				}
			}
			echo '</urlset>';
		}

		/**
		 * Output Sitemap Index
		 *
		 * Output the XML for a sitemap index.
		 *
		 * @since ?
		 *
		 * @param        $urls
		 * @param string $comment
		 * @return null
		 */
		public function output_sitemap_index( $urls, $comment = '' ) {
			$max_items = 50000;
			if ( ! is_array( $urls ) ) {
				return null;
			}
			echo '<?xml version="1.0" encoding="UTF-8"?>' . "\r\n\r\n";
			// TODO Add esc_* function.
			echo '<!-- ' . sprintf( $this->comment_string, $comment, AIOSEOP_VERSION, date( 'D, d M Y H:i:s e' ) ) . " -->\r\n";
			$xsl_url = $this->get_sitemap_xsl();
			// TODO Add esc_* function.
			echo '<?xml-stylesheet type="text/xsl" href="' . $xsl_url . '"?>' . "\r\n";
			echo '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\r\n";
			$count = 0;
			foreach ( $urls as $url ) {
				echo "\t<sitemap>\r\n";
				if ( is_array( $url ) ) {
					foreach ( $url as $k => $v ) {
						// TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison.
						if ( ! in_array( $k, array( 'loc', 'lastmod' ) ) ) {
							continue;
						}
						$v = aiosp_common::make_xml_safe( $k, $v );
						// TODO Add esc_* function.
						echo "\t\t<$k>$v</$k>\r\n";
					}
				} else {
					$value = aiosp_common::make_xml_safe( 'loc', $url );
					// TODO Add esc_* function.
					echo "\t\t<loc>$value</loc>\r\n";
				}
				echo "\t</sitemap>\r\n";
				$count ++;
				if ( $count >= $max_items ) {
					break;
				}
			}
			echo '</sitemapindex>';
		}

		/**
		 * Build Sitemap Index
		 *
		 * Return an XML sitemap index as a string.
		 *
		 * @since ?
		 *
		 * @param        $urls
		 * @param string $comment
		 * @return string
		 */
		public function build_sitemap_index( $urls, $comment = '' ) {
			ob_start();
			$this->output_sitemap_index( $urls, $comment );

			return ob_get_clean();
		}

		/**
		 * Build Sitemap
		 *
		 * Return an XML sitemap as a string.
		 *
		 * @since ?
		 *
		 * @param        $urls
		 * @param string $sitemap_type The type of sitemap viz. root, rss, rss_latest etc.. For static sitemaps, this would be empty.
		 * @param string $comment
		 * @return string
		 */
		public function build_sitemap( $urls, $sitemap_type, $comment = '' ) {
			ob_start();
			$this->output_sitemap( $urls, $sitemap_type, $comment );

			return ob_get_clean();
		}

		/**
		 * Get Term Priority Data
		 *
		 * Return sitemap data for an array of terms.
		 *
		 * @since ?
		 *
		 * @param $terms
		 * @return array
		 */
		public function get_term_priority_data( $terms ) {
			$prio = array();
			if ( is_array( $terms ) && ! empty( $terms ) ) {
				$def_prio = $this->get_default_priority( 'taxonomies' );
				$def_freq = $this->get_default_frequency( 'taxonomies' );
				foreach ( $terms as $term ) {
					$pr_info            = array();
					$pr_info['loc']     = $this->get_term_link( $term, $term->taxonomy );
					$pr_info['lastmod'] = $this->get_tax_term_timestamp( $term );
					if (
						( 'sel' === $this->options[ $this->prefix . 'freq_taxonomies' ] )
						&& isset( $this->options[ $this->prefix . 'freq_taxonomies_' . $term->taxonomy ] )
						&& ( 'no' !== $this->options[ $this->prefix . 'freq_taxonomies_' . $term->taxonomy ] )
					) {
						$pr_info['changefreq'] = $this->options[ $this->prefix . 'freq_taxonomies_' . $term->taxonomy ];
					} else {
						$pr_info['changefreq'] = $def_freq;
					}
					if (
						( 'sel' === $this->options[ $this->prefix . 'prio_taxonomies' ] )
						&& isset( $this->options[ $this->prefix . 'prio_taxonomies_' . $term->taxonomy ] )
						&& ( 'no' !== $this->options[ $this->prefix . 'prio_taxonomies_' . $term->taxonomy ] )
					) {
						$pr_info['priority'] = $this->options[ $this->prefix . 'prio_taxonomies_' . $term->taxonomy ];
					} else {
						$pr_info['priority'] = $def_prio;
					}

					$pr_info['image:image'] = $this->get_images_from_term( $term );

					$pr_info['rss'] = array(
						'title'       => $term->name,
						'description' => $term->description,
						'pubDate'     => $this->get_date_for_term( $term ),
					);

					$prio[] = $pr_info;
				}
			}

			return $prio;
		}

		/**
		 * The get_tax_term_timestamp() function.
		 *
		 * Gets the Last Change timestamp for a taxonomy term.
		 *
		 * @since 3.2.0
		 *
		 * @param object $term
		 * @return string $lastmod
		 */
		private function get_tax_term_timestamp( $term ) {
			$taxonomy_object = get_taxonomy( $term->taxonomy );

			$lastmod = '';

			// Loop through all attached post types and get timestamp of last modified assigned post.
			foreach ( $taxonomy_object->object_type as $object_type ) {
				$latest_modified_post = new WP_Query(
					array(
						'post_type'      => $object_type,
						'post_status'    => 'publish',
						'posts_per_page' => 1,
						'orderby'        => 'modified',
						'order'          => 'DESC',
						'taxonomy'       => $term->taxonomy,
						'term'           => $term->name,
					)
				);

				if ( $latest_modified_post->have_posts() ) {
					$temp_lastmod = $latest_modified_post->posts[0]->post_modified_gmt;
					if ( '' === $lastmod || ( $temp_lastmod > $lastmod ) ) {
						$lastmod = $temp_lastmod;
					}
				}
			}

			$lastmod = date( 'Y-m-d\TH:i:s\Z', mysql2date( 'U', $lastmod ) );
			return $lastmod;
		}

		/**
		 * Get Date for Term
		 *
		 * Return the date of the latest post in the given taxonomy term.
		 *
		 * @since 2.9
		 *
		 * @param WP_Term $term The taxonomy term.
		 * @return string
		 */
		private function get_date_for_term( $term ) {
			$date  = '';
			$query = new WP_Query(
				array(
					'orderby'     => 'post_date',
					'order'       => 'DESC',
					'numberposts' => 1,
					'post_type'   => 'any',
					'post_status' => 'publish',
					'tax_query'   => array(
						array(
							'taxonomy' => $term->taxonomy,
							'terms'    => $term->term_id,
						),
					),
				)
			);

			if ( $query->have_posts() ) {
				$timestamp = mysql2date( 'U', $query->post->post_modified_gmt );
				$date      = date( 'r', $timestamp );
			}

			return $date;
		}

		/**
		 * Get Term Permalinks
		 *
		 * Return a list of permalinks for an array of terms.
		 *
		 * @since ?
		 *
		 * @param $terms
		 * @return array
		 */
		public function get_term_permalinks( $terms ) {
			$links = array();
			if ( is_array( $terms ) ) {
				foreach ( $terms as $term ) {
					$url     = $this->get_term_link( $term );
					$links[] = $url;
				}
			}

			return $links;
		}

		/**
		 * Get Archive Permalinks
		 *
		 * Return permalinks for archives.
		 *
		 * @since ?
		 *
		 * @param $posts
		 * @return array
		 */
		public function get_archive_permalinks( $posts ) {
			$links    = array();
			$archives = array();
			if ( is_array( $posts ) ) {
				foreach ( $posts as $post ) {
					$date                             = mysql2date( 'U', $post->post_date );
					$year                             = date( 'Y', $date );
					$month                            = date( 'm', $date );
					$archives[ $year . '-' . $month ] = array( $year, $month );
				}
			}
			$archives = array_keys( $archives );
			foreach ( $archives as $d ) {
				$links[] = get_month_link( $d[0], $d[1] );
			}

			return $links;
		}

		/**
		 * Get Author Permalink
		 *
		 * Return permalinks for authors.
		 *
		 * @since ?
		 *
		 * @param $posts
		 * @return array
		 */
		public function get_author_permalinks( $posts ) {
			$links   = array();
			$authors = array();
			if ( is_array( $posts ) ) {
				foreach ( $posts as $post ) {
					$authors[ $post->author_id ] = 1;
				}
			}
			$authors = array_keys( $authors );
			foreach ( $authors as $auth_id ) {
				$links[] = get_author_posts_url( $auth_id );
			}

			return $links;
		}

		/**
		 * Get Post Permalink
		 *
		 * Return permalinks for posts.
		 *
		 * @since ?
		 *
		 * @param $posts
		 * @return array
		 */
		public function get_post_permalinks( $posts ) {
			$links = array();
			if ( is_array( $posts ) ) {
				foreach ( $posts as $post ) {
					$post->filter = 'sample';
					$url          = $this->get_permalink( $post );
					$links[]      = $url;
				}
			}

			return $links;
		}

		/**
		 * Unparse URL
		 *
		 * Convert back from parse_url.
		 * Props to thomas at gielfeldt dot com.
		 *
		 * @since ?
		 *
		 * @link http://www.php.net/manual/en/function.parse-url.php#106731
		 *
		 * @param $parsed_url
		 * @return string
		 */
		public function unparse_url( $parsed_url ) {
			$scheme = isset( $parsed_url['scheme'] ) ? $parsed_url['scheme'] . '://' : '';
			$host   = isset( $parsed_url['host'] ) ? $parsed_url['host'] : '';
			if ( ! empty( $host ) && empty( $scheme ) ) {
				$scheme = '//';
			}
			$port     = isset( $parsed_url['port'] ) ? ':' . $parsed_url['port'] : '';
			$user     = isset( $parsed_url['user'] ) ? $parsed_url['user'] : '';
			$pass     = isset( $parsed_url['pass'] ) ? ':' . $parsed_url['pass'] : '';
			$pass     = ( $user || $pass ) ? "$pass@" : '';
			$path     = isset( $parsed_url['path'] ) ? $parsed_url['path'] : '';
			$query    = isset( $parsed_url['query'] ) ? '?' . $parsed_url['query'] : '';
			$fragment = isset( $parsed_url['fragment'] ) ? '#' . $parsed_url['fragment'] : '';

			return "$scheme$user$pass$host$port$path$query$fragment";
		}

		/**
		 * Get Additional Page Only
		 *
		 * Return data for user entered additional pages.
		 *
		 * @since 2.3.6
		 * @since 2.3.12.3 Refactored to use aioseop_home_url() for compatibility purposes.
		 *
		 * @return array
		 */
		public function get_addl_pages_only() {
			$pages = array();
			if ( ! empty( $this->options[ $this->prefix . 'addl_pages' ] ) ) {
				$siteurl = wp_parse_url( aioseop_home_url() );
				foreach ( $this->options[ $this->prefix . 'addl_pages' ] as $k => $v ) {
					$k   = aiosp_common::make_url_valid_smartly( $k );
					$url = wp_parse_url( $k );
					if ( empty( $url['host'] ) ) {
						$url['host'] = $siteurl['host'];
					}
					if ( ! empty( $url['path'] ) && substr( $url['path'], 0, 1 ) !== '/' ) {
						$url['path'] = '/' . $url['path'];
					}
					$freq = '';
					$prio = '';
					$mod  = '';
					if ( ! empty( $v['mod'] ) ) {
						$mod = $v['mod'];
					}
					if ( ! empty( $v['freq'] ) ) {
						$freq = $v['freq'];
					}
					if ( ! empty( $v['prio'] ) ) {
						$prio = $v['prio'];
					}
					if ( 'no' === $freq ) {
						$freq = '';
					}
					if ( 'no' === $prio ) {
						$prio = '';
					}
					$mod     = date( 'Y-m-d\TH:i:s\Z', mysql2date( 'U', $mod ) );
					$pages[] = array(
						'loc'        => $this->unparse_url( $url ),
						'lastmod'    => $mod,
						'changefreq' => $freq,
						'priority'   => $prio,
					);
				}
			}
			$pages = apply_filters( $this->prefix . 'addl_pages_only', $pages );

			return $pages;
		}

		/**
		 * Get Additional Pages
		 *
		 * Return data for user entered additional pages and extra pages.
		 *
		 * @since 2.3.6
		 * @since 2.3.12.3 Refactored to use aioseop_home_url() for compatibility purposes.
		 * @since 3.2.0 Do not include static homepage/posts page - #2126.
		 *
		 * @return array
		 */
		public function get_addl_pages() {
			$home = array();
			$home = array(
				'loc'         => aioseop_home_url(),
				'changefreq'  => $this->get_default_frequency( 'homepage' ),
				'priority'    => $this->get_default_priority( 'homepage' ),
				'image:image' => $this->get_images_from_post( (int) get_option( 'page_on_front' ) ),
			);

			$posts = (int) get_option( 'page_for_posts' );
			if ( $posts ) {
				$posts = $this->get_permalink( $posts );
				if ( $posts === $home['loc'] ) {
					$posts = array();
				} else {
					$posts = array(
						'loc'        => $posts,
						'changefreq' => $this->get_default_frequency( 'blog' ),
						'priority'   => $this->get_default_priority( 'blog' ),
					);
				}
			} else {
				$posts = array();
			}
			$pages = $this->get_addl_pages_only();
			if ( ! empty( $home ) ) {
				$pages[] = $home;
			}
			if ( ! empty( $posts ) ) {
				$pages[] = $posts;
			}
			$pages = apply_filters( $this->prefix . 'addl_pages', $pages );

			$pages = $this->get_homepage_timestamp( $pages );
			$pages = $this->remove_addl_static_pages( $pages );

			return $pages;
		}

		/**
		 * The remove_addl_static_pages() function.
		 *
		 * Removes the homepage/posts page from the Additional Pages index if it is static - #2126.
		 *
		 * @since 3.2.0
		 *
		 * @param array $pages
		 * @return array $pages
		 */
		private function remove_addl_static_pages( $pages ) {
			$pages_to_remove = array();
			if ( 0 !== (int) get_option( 'page_on_front' ) ) {
				$homepage_url = get_site_url() . '/';
				array_push( $pages_to_remove, $homepage_url );
			}

			$static_posts_page_id = (int) get_option( 'page_for_posts' );
			if ( 0 !== $static_posts_page_id ) {
				array_push( $pages_to_remove, get_permalink( $static_posts_page_id ) );
			}

			if ( count( $pages_to_remove ) > 0 ) {
				return $this->remove_urls_from_sitemap_page( $pages, $pages_to_remove );
			}
			return $pages;
		}

		/**
		 * The remove_urls_from_sitemap_page() function.
		 *
		 * Removes URLs from a sitemap page. This is used both for indexes and pages within indexes.
		 *
		 * @since 3.2.0
		 *
		 * @param array $pages
		 * @param array $pages_to_remove
		 * @return array $pages
		 */
		private function remove_urls_from_sitemap_page( $pages, $pages_to_remove ) {
			$count = count( $pages );
			for ( $i = 0; $i < $count; $i++ ) {
				if ( in_array( $pages[ $i ]['loc'], $pages_to_remove, true ) ) {
					unset( $pages[ $i ] );
				}
			}
			return $pages;
		}

		/**
		 * The get_homepage_timestamp() function.
		 *
		 * Gets the Last Change timestamp for the homepage if it isn't static.
		 *
		 * @since 3.2.0
		 *
		 * @param array $urls
		 * @return array $urls
		 */
		private function get_homepage_timestamp( $urls ) {
			if ( 0 !== (int) get_option( 'page_on_front' ) ) {
				return $urls;
			}

			$homepage_url = get_site_url() . '/';
			$urls         = $this->update_static_page_timestamp( $urls, $homepage_url );

			return $urls;
		}

		/**
		 * The get_posts_page_timestamp() function.
		 *
		 * Gets the Last Change timestamp for the posts page.
		 *
		 * @since 3.2.0
		 *
		 * @param array $urls
		 * @return array $urls
		 */
		private function get_posts_page_timestamp( $urls ) {
			$posts_page_id = (int) get_option( 'page_for_posts' );
			if ( 0 === $posts_page_id ) {
				return $urls;
			}

			$posts_page_url = get_permalink( $posts_page_id );
			$urls           = $this->update_static_page_timestamp( $urls, $posts_page_url );

			return $urls;
		}

		/**
		 * The update_static_page_timestamp() function.
		 *
		 * Update the timestamp attribute for a static page.
		 *
		 * @since 3.2.0
		 *
		 * @param array $urls
		 * @param string $static_page_url
		 * @return array $urls
		 */
		private function update_static_page_timestamp( $urls, $static_page_url ) {
			$lastmod = $this->get_last_modified_post_timestamp( 'post' );
			if ( false === $lastmod ) {
				return $urls;
			}

			$url_locs = array_combine( array_keys( $urls ), wp_list_pluck( $urls, 'loc' ) );
			$index    = array_search( $static_page_url, $url_locs );
			if ( false === $index ) {
				return $urls;
			}

			$urls[ $index ] = $this->insert_timestamp_as_second_attribute( $urls[ $index ], $lastmod );
			return $urls;
		}

		/**
		 * The get_last_modified_post_timestamp() function.
		 *
		 * Gets the last modified post.
		 *
		 * @since 3.2.0
		 *
		 * @param string $post_type
		 * @return mixed Timestamp of the last modified post or false if there is none.
		 */
		private function get_last_modified_post_timestamp( $post_type ) {
			$last_modified_post = new WP_Query(
				array(
					'post_type'      => $post_type,
					'post_status'    => 'publish',
					'posts_per_page' => 1,
					'orderby'        => 'modified',
					'order'          => 'DESC',
				)
			);

			if ( $last_modified_post->have_posts() ) {
				return $this->format_timestamp_as_lastmod_attribute( $last_modified_post );
			}
			return false;
		}

		/**
		 * The format_timestamp_as_lastmod_attribute() function.
		 *
		 * Formats the timestamp for a sitemap record in order to have valid sitemap schema.
		 *
		 * @since 3.2.0
		 *
		 * @param object $last_modified_post WP_Query for the last modified post.
		 * @return string $lastmod
		 */
		private function format_timestamp_as_lastmod_attribute( $last_modified_post ) {
			$lastmod = $last_modified_post->posts[0]->post_modified_gmt;
			return date( 'Y-m-d\TH:i:s\Z', mysql2date( 'U', $lastmod ) );
		}

		/**
		 * The insert_timestamp_as_second_attribute() function.
		 *
		 * Inserts the timestamp for a sitemap record as the second attribute.
		 * The lastmod subtag has to be inserted as second attribute in order to have valid schema.
		 *
		 * @since 3.2.0
		 *
		 * @param array $url
		 * @param string $lastmod
		 * @return array $url
		 */
		private function insert_timestamp_as_second_attribute( $url, $lastmod ) {
			return array_slice( $url, 0, 1, true ) + array( 'lastmod' => $lastmod ) + array_slice( $url, 1, null, true );
		}

		/**
		 * Get Additional Page Links
		 *
		 * Return links for user entered additional pages.
		 *
		 * @since ?
		 *
		 * @return array
		 */
		public function get_addl_page_links() {
			if ( ! empty( $this->options[ $this->prefix . 'addl_pages' ] ) ) {
				return array_keys( $this->options[ $this->prefix . 'addl_pages' ] );
			}

			return array();
		}

		/**
		 * Get Priority Calculation
		 *
		 * Scores posts based on date and relative comment count, if any.
		 *
		 * @since ?
		 *
		 * @param     $date
		 * @param mixed $stats
		 * @return array
		 */
		public function get_prio_calc( $date, $stats ) {
			static $cur_time = null;
			if ( null === $cur_time ) {
				$cur_time = time();
			}
			$time = $cur_time - mysql2date( 'U', $date );
			if ( ! empty( $stats ) && isset( $stats['max'] ) && $stats['max'] ) {
				$minadj = $time >> 3;
				$maxadj = $time >> 1;
				$avg    = $stats['count'] / $stats['total'];
				$calc   = ( $stats['comment_count'] - $stats['min'] ) / $stats['max'];
				$calc   = $maxadj * $calc;
				if ( $avg < $stats['comment_count'] ) {
					$minadj = $time >> 2;
				} else {
					$maxadj = $time >> 2;
				}
				if ( $calc > $maxadj ) {
					$calc = $maxadj;
				}
				if ( $calc < $minadj ) {
					$calc = $minadj;
				}
				$time -= $calc;
			}
			$days       = $time / ( 60 * 60 * 24 );
			$prio_table = array(
				'daily'   => 7,
				'weekly'  => 30,
				'monthly' => 210,
				'yearly'  => null,
			);
			$interval   = 1.0;
			$prev_days  = 0;
			foreach ( $prio_table as $change => $max_days ) {
				$interval -= 0.3;
				if ( null === $max_days ) {
					$changefreq = $change;
					$prio       = 0.1;
					break;
				}
				if ( $days < $max_days ) {
					$int_days_max = $max_days - $prev_days;
					$int_days     = $days - $prev_days;
					$prio         = $interval + ( (int) ( 3 * ( ( $max_days - $int_days ) / $int_days_max ) ) / 10.0 );
					$changefreq   = $change;
					break;
				}
				$prev_days = $max_days;
			}

			return array(
				'lastmod'    => $date,
				'changefreq' => $changefreq,
				'priority'   => $prio,
			);
		}

		/**
		 * Get Date Archive Priority from Posts
		 *
		 * Generate sitemap priority data for date archives from an array of posts.
		 *
		 * @since ?
		 *
		 * @param $posts
		 * @return array
		 */
		public function get_date_archive_prio_from_posts( $posts ) {
			$archives = array();
			if ( is_array( $posts ) ) {
				foreach ( $posts as $p ) {
					if ( 'post' !== $p->post_type ) {
						continue;
					}
					// add the post type to the date so as to support posts of different post types created on the same date.
					$date = date( 'Y-m', mysql2date( 'U', $p->post_date ) ) . $p->post_type;
					if ( empty( $archives[ $date ] ) ) {
						$archives[ $date ] = $p;
					} else {
						if ( $p->post_modified > $archives[ $date ]->post_modified ) {
							$archives[ $date ] = $p;
						}
					}
				}
			}

			if ( ! empty( $archives ) ) {
				return $this->get_prio_from_posts(
					$archives,
					$this->get_default_priority( 'archive', true ),
					$this->get_default_frequency( 'archive', true ),
					array(
						$this,
						'get_date_archive_link_from_post',
					),
					'archive'
				);
			}

			return $archives;
		}

		/**
		 * Get Archive Priority from Posts
		 *
		 * Generate sitemap priority data for archives from an array of posts.
		 *
		 * @since ?
		 * @since 3.2.0 Don't fetch WooCommerce shop page twice - #2126
		 *
		 * @param $posts
		 * @return array
		 */
		private function get_archive_prio_from_posts( $posts ) {
			$posttypes = array();
			if ( ! empty( $this->options[ "{$this->prefix}posttypes" ] ) ) {
				$posttypes = $this->options[ "{$this->prefix}posttypes" ];
			}

			if ( aioseop_is_woocommerce_active() ) {
				if ( in_array( 'product', $posttypes ) ) {
					$index = array_search( 'product', $posttypes );
					unset( $posttypes[ $index ] );
				}
			}

			$types_supporting_archives = get_post_types(
				array(
					'has_archive' => true,
					'_builtin'    => false,
				),
				'names'
			);
			$types                     = array();
			foreach ( $posts as $p ) {
				if ( array_key_exists( $p->post_type, $types ) ) {
					continue;
				}
				$types[ $p->post_type ] = $p;
			}

			$archive_pages = array();
			$types         = apply_filters( "{$this->prefix}include_post_types_archives", $types );
			if ( $types ) {
				foreach ( $types as $post_type => $p ) {
					// TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison.
					if ( ! ( in_array( $post_type, $posttypes ) && in_array( $post_type, $types_supporting_archives ) ) ) {
						continue;
					}
					$temp_archive_pages = $this->get_prio_from_posts(
						array( $p ),
						$this->get_default_priority( 'archive', true ),
						$this->get_default_frequency( 'archive', true ),
						array(
							$this,
							'get_archive_link_from_post',
						)
					);

					if ( ! empty( $temp_archive_pages ) ) {
						$temp_archive_pages = $this->get_archive_page_timestamp( $temp_archive_pages, $post_type );
					}

					$archive_pages = array_merge(
						$archive_pages,
						$temp_archive_pages
					);
				}
			}
			return $archive_pages;
		}

		/**
		 * The get_archive_page_timestamp() function.
		 *
		 * Get the Last Change timestamp for archive pages.
		 *
		 * @since 3.2.0
		 *
		 * @param array $urls
		 * @param string $post_type
		 * @return array $urls
		 */
		private function get_archive_page_timestamp( $urls, $post_type ) {
			$lastmod = $this->get_last_modified_post_timestamp( $post_type );
			if ( false === $lastmod ) {
				return $urls;
			}

			$count = count( $urls );
			for ( $i = 0; $i < $count; $i++ ) {
				$urls[ $i ] = $this->insert_timestamp_as_second_attribute( $urls[ $i ], $lastmod );
			}
			return $urls;
		}

		/**
		 * Return an archive link for a post.
		 *
		 * @param $post
		 *
		 * @return bool|string
		 */
		public function get_archive_link_from_post( $post ) {
			return get_post_type_archive_link( $post->post_type );
		}

		/**
		 * Get Date Archive Link from Post
		 *
		 * Return a date archive link for a post.
		 *
		 * @since ?
		 *
		 * @param $post
		 * @return bool|string
		 */
		public function get_date_archive_link_from_post( $post ) {
			$extra = array();
			if ( 'post' !== $post->post_type ) {
				return false;
			}
			$date = mysql2date( 'U', $post->post_date );

			return get_month_link( date( 'Y', $date ), date( 'm', $date ) );
		}

		/**
		 * Get Author Priority from Posts
		 *
		 * Generate sitemap priority data for authors from an array of posts.
		 *
		 * @since ?
		 *
		 * @param $posts
		 * @return array
		 */
		public function get_author_prio_from_posts( $posts ) {
			$authors = array();
			if ( is_array( $posts ) ) {
				foreach ( $posts as $p ) {
					if ( 'post' !== $p->post_type ) {
						continue;
					}
					if ( empty( $authors[ $p->post_author ] ) ) {
						$authors[ $p->post_author ] = $p;
					} else {
						if ( $p->post_modified > $authors[ $p->post_author ]->post_modified ) {
							$authors[ $p->post_author ] = $p;
						}
					}
				}
			}

			return $this->get_prio_from_posts(
				$authors,
				$this->get_default_priority( 'author', true ),
				$this->get_default_frequency( 'author', true ),
				array(
					$this,
					'get_author_link_from_post',
				),
				'author'
			);
		}

		/**
		 * Get Author Link from Post
		 *
		 * Return an author link from a post.
		 *
		 * @since ?
		 *
		 * @param $post
		 * @return string
		 */
		public function get_author_link_from_post( $post ) {
			return get_author_posts_url( $post->post_author );
		}

		/**
		 * Get Comment Count Stats
		 *
		 * Return comment statistics on an array of posts.
		 *
		 * @since ?
		 *
		 * @param $posts
		 * @return array|int
		 */
		public function get_comment_count_stats( $posts ) {
			$count = 0;
			$total = 0.0;
			$min   = null;
			$max   = 0;
			if ( is_array( $posts ) ) {
				foreach ( $posts as $post ) {
					if ( ! empty( $post->comment_count ) ) {
						$cnt = $post->comment_count;
						$count ++;
						$total += $cnt;
						if ( null === $min ) {
							$min = $cnt;
						}
						if ( $max < $cnt ) {
							$max = $cnt;
						}
						if ( $min > $cnt ) {
							$min = $cnt;
						}
					}
				}
			}
			if ( $count ) {
				return array(
					'max'   => $max,
					'min'   => $min,
					'total' => $total,
					'count' => $cnt,
				);
			} else {
				return 0;
			}
		}

		/**
		 * Get Priority from Posts
		 *
		 * Generate sitemap priority data from an array of posts.
		 *
		 * @since ?
		 *
		 * @param        $posts
		 * @param bool   $prio_override
		 * @param bool   $freq_override
		 * @param string $linkfunc
		 * @param string $type Type of entity being fetched viz. author, post etc.
		 * @return array
		 */
		public function get_prio_from_posts( $posts, $prio_override = false, $freq_override = false, $linkfunc = 'get_permalink', $type = 'post' ) {
			$prio = array();
			$args = array(
				'prio_override' => $prio_override,
				'freq_override' => $freq_override,
				'linkfunc'      => $linkfunc,
			);
			if ( $prio_override && $freq_override ) {
				$stats = 0;
			} else {
				$stats = $this->get_comment_count_stats( $posts );
			}
			if ( is_array( $posts ) ) {
				foreach ( $posts as $key => $post ) {
					// Determine if we check the post for images.
					$is_single    = true;
					$post->filter = 'sample';
					$timestamp    = null;
					if ( 'get_permalink' === $linkfunc ) {
						$url = $this->get_permalink( $post );
					} else {
						$url       = call_user_func( $linkfunc, $post );
						$is_single = false;
					}

					if ( strpos( $url, '__trashed' ) !== false ) {
						// excluded trashed urls.
						continue;
					}

					$date = $post->post_modified_gmt;
					if ( '0000-00-00 00:00:00' === $date ) {
						$date = $post->post_date_gmt;
					}
					if ( '0000-00-00 00:00:00' !== $date ) {
						$timestamp = $date;
						$date      = date( 'Y-m-d\TH:i:s\Z', mysql2date( 'U', $date ) );
					} else {
						$date = 0;
					}
					if ( $prio_override && $freq_override ) {
						$pr_info = array(
							'lastmod'    => $date,
							'changefreq' => null,
							'priority'   => null,
						);
					} else {
						if ( empty( $post->comment_count ) ) {
							$stat = 0;
						} else {
							$stat = $stats;
						}
						if ( ! empty( $stat ) ) {
							$stat['comment_count'] = $post->comment_count;
						}
						$pr_info = $this->get_prio_calc( $date, $stat );
					}
					if ( $freq_override ) {
						$pr_info['changefreq'] = $freq_override;
					}
					if ( $prio_override ) {
						$pr_info['priority'] = $prio_override;
					}
					if ( ( 'sel' === $this->options[ $this->prefix . 'prio_post' ] ) && isset( $this->options[ $this->prefix . 'prio_post_' . $post->post_type ] ) ) {
						if (
							'no' !== $this->options[ $this->prefix . 'prio_post_' . $post->post_type ]
							&& 'sel' !== $this->options[ $this->prefix . 'prio_post_' . $post->post_type ]
						) {
							$pr_info['priority'] = $this->options[ $this->prefix . 'prio_post_' . $post->post_type ];
						}
					}
					if ( ( 'sel' === $this->options[ $this->prefix . 'freq_post' ] ) && isset( $this->options[ $this->prefix . 'freq_post_' . $post->post_type ] ) ) {
						if (
							'no' !== $this->options[ $this->prefix . 'freq_post_' . $post->post_type ]
							&& 'sel' !== $this->options[ $this->prefix . 'freq_post_' . $post->post_type ]
						) {
							$pr_info['changefreq'] = $this->options[ $this->prefix . 'freq_post_' . $post->post_type ];
						}
					}
					$pr_info = array(
						'loc' => $url,
					) + $pr_info; // Prepend loc to	the	array.
					if ( is_float( $pr_info['priority'] ) ) {
						$pr_info['priority'] = sprintf( '%0.1F', $pr_info['priority'] );
					}

					// add the rss specific data.
					if ( $timestamp ) {
						$title = null;
						switch ( $type ) {
							case 'author':
								$title = get_the_author_meta( 'display_name', $key );
								break;
							default:
								$title = get_the_title( $post );
								break;
						}

						// RSS expects the GMT date.
						$timestamp      = mysql2date( 'U', $post->post_modified_gmt );
						$pr_info['rss'] = array(
							'title'       => $title,
							'description' => get_post_field( 'post_excerpt', $post->ID ),
							'pubDate'     => date( 'r', $timestamp ),
							'timestamp  ' => $timestamp,
							'post_type'   => $post->post_type,
						);
					}

					$pr_info['image:image'] = $is_single ? $this->get_images_from_post( $post ) : null;

					$pr_info = apply_filters( $this->prefix . 'prio_item_filter', $pr_info, $post, $args );
					if ( ! empty( $pr_info ) ) {
						$prio[] = $pr_info;
					}
				}
			}

			return $prio;
		}

		/**
		 * Get Images from Term
		 *
		 * Return the images attached to the term.
		 *
		 * @since 2.4
		 * @since 3.0 remove check for WP 4.4
		 *
		 * @param WP_Term $term the term object.
		 * @return array
		 */
		private function get_images_from_term( $term ) {
			if ( ! aiosp_include_images() ) {
				return array();
			}

			$images = array();

			$thumbnail_id = get_term_meta( $term->term_id, 'thumbnail_id', true );
			if ( $thumbnail_id ) {
				$image = wp_get_attachment_url( $thumbnail_id );
				if ( $image ) {
					$images['image:image'] = array(
						'image:loc'     => $image,
						'image:caption' => wp_get_attachment_caption( $thumbnail_id ),
						'image:title'   => get_the_title( $thumbnail_id ),
					);
				}
			}

			return $images;
		}

		/**
		 * Get Images from Post
		 *
		 * Return the images from the post.
		 *
		 * @todo Add ~`get_attachment_postid_to_url()` function.
		 * @todo Benchmark `wp_get_attachment_image_src()` & `wp_get_attachment_url()`.
		 * @todo Look into using 'wp_get_attachment_image_url()'.
		 *
		 * @since 2.4
		 * @since 2.11 Optimization #2008 - Reduce the need to convert url to id.
		 *
		 * @param WP_Post|int $post the post object.
		 * @return array
		 */
		private function get_images_from_post( $post ) {
			if ( ! aiosp_include_images() ) {
				return array();
			}

			$rtn_image_attributes = array();
			$post_image_ids       = array();
			$post_image_urls      = array();
			$transient_update     = false;

			if ( is_numeric( $post ) ) {
				if ( 0 === $post ) {
					return null;
				}
				$post = get_post( $post );
			}

			if ( 'attachment' === $post->post_type ) {
				if ( false === strpos( $post->post_mime_type, 'image/' ) ) {
					// Ignore all attachments except images.
					return null;
				}
				$attributes = wp_get_attachment_image_src( $post->ID );
				if ( $attributes ) {
					$rtn_image_attributes[] = array(
						'image:loc'     => $this->aioseop_clean_url( $attributes[0] ),
						'image:caption' => wp_get_attachment_caption( $post->ID ),
						'image:title'   => get_the_title( $post->ID ),
					);
				}

				return $rtn_image_attributes;
			}

			// Set Image IDs w/ URLs.
			if ( is_null( $this->image_ids_urls ) ) {
				// Get Transient/Cache data.
				if ( is_multisite() ) {
					$this->image_ids_urls = get_site_transient( 'aioseop_multisite_attachment_ids_urls' );
				} else {
					$this->image_ids_urls = get_transient( 'aioseop_attachment_ids_urls' );
				}

				// Set default if no data exists.
				if ( false === $this->image_ids_urls ) {
					$this->image_ids_urls = array();
				}
			}

			/**
			 * Static attachment cache, 1 query vs. n posts.
			 *
			 * Concepts like this should be followed; although this could possibly be improved (maybe as a wrapped function) but is still good code.
			 */
			static $post_thumbnails;

			if ( is_null( $post_thumbnails ) || defined( 'AIOSEOP_UNIT_TESTING' ) ) {
				global $wpdb;

				$post_thumbnails = $wpdb->get_results( "SELECT post_ID, meta_value FROM $wpdb->postmeta WHERE meta_key = '_thumbnail_id'", ARRAY_A );

				if ( $post_thumbnails ) {
					$post_thumbnails = array_combine(
						wp_list_pluck( $post_thumbnails, 'post_ID' ),
						wp_list_pluck( $post_thumbnails, 'meta_value' )
					);
				}
			}

			if ( isset( $post_thumbnails[ $post->ID ] ) ) {
				$post_image_ids[] = intval( $post_thumbnails[ $post->ID ] );
			}

			$post_image_ids = array_merge( $post_image_ids, $this->get_gallery_image_ids( $post ) );

			$this->get_gallery_images( $post, $post_image_urls );

			// Get image URLs from content.
			$content  = $post->post_content;
			$content .= $this->get_content_from_galleries( $content );
			$this->parse_content_for_images( $content, $post_image_urls );

			if ( ! empty( $post_image_urls ) ) {
				// Remove any invalid/empty images.
				$post_image_urls = array_filter( $post_image_urls, array( $this, 'is_image_url_valid' ) );

				// If possible, get ID from URL, and store the post's attachment ID => URL value.
				// This is to base the attachment query on the ID instead of the URL; which is less SQL intense.
				foreach ( $post_image_urls as $k1_index => &$v1_image_url ) {
					$v1_image_url  = aiosp_common::absolutize_url( $v1_image_url );
					$attachment_id = aiosp_common::attachment_url_to_postid( $v1_image_url );

					if ( $attachment_id ) {
						if ( ! isset( $this->image_ids_urls[ $attachment_id ] ) ) {
							// Use transient/cache data.
							$this->image_ids_urls[ $attachment_id ] = array( $v1_image_url );

							$transient_update = true;
						} else {
							// If transient/cache data is already set, and URL is not already stored.
							if ( ! in_array( $v1_image_url, $this->image_ids_urls[ $attachment_id ], true ) ) {
								$this->image_ids_urls[ $attachment_id ][] = $v1_image_url;

								$transient_update = true;
							}
						}

						// Store and use ID instead.
						array_push( $post_image_ids, $attachment_id );
						unset( $post_image_urls[ $k1_index ] );
					}
				}
			}

			// Site's Images.
			if ( $post_image_ids ) {
				// Filter out duplicates.
				$post_image_ids = array_unique( $post_image_ids );

				foreach ( $post_image_ids as $v1_image_id ) {
					// Set base URL to display later in this instance, or later (transient/cache) instances.
					// Converting ID from URL can also be heavy on memory & time.
					if ( ! isset( $this->image_ids_urls[ $v1_image_id ] ) ) {
						// Sets any remaining post image IDs that weren't converted from URL.
						$this->image_ids_urls[ $v1_image_id ] = array(
							'base_url' => $this->aioseop_clean_url( wp_get_attachment_url( $v1_image_id ) ),
						);

						$transient_update = true;
					} else {
						if ( empty( $this->image_ids_urls[ $v1_image_id ]['base_url'] ) ) {
							$this->image_ids_urls[ $v1_image_id ]['base_url'] = $this->aioseop_clean_url( wp_get_attachment_url( $v1_image_id ) );

							$transient_update = true;
						}
					}

					// Set return variable for image data/attributes.
					$rtn_image_attributes[] = array(
						'image:loc'     => $this->image_ids_urls[ $v1_image_id ]['base_url'],
						'image:caption' => wp_get_attachment_caption( $v1_image_id ),
						'image:title'   => get_the_title( $v1_image_id ),
					);
				}
			}

			// External/Custom images remaining.
			if ( ! empty( $post_image_urls ) ) {
				foreach ( $post_image_urls as $v1_image_url ) {
					$rtn_image_attributes[] = array(
						'image:loc' => $v1_image_url,
					);
				}
			}

			if ( $transient_update ) {
				add_action( 'shutdown', array( $this, 'set_transient_attachment_ids_urls' ) );
			}

			return $rtn_image_attributes;
		}

		/**
		 * Set Transient Attachment IDs => URLS
		 *
		 * Set Transient for Image IDs => URLs
		 *
		 * @since 2.11
		 */
		public function set_transient_attachment_ids_urls() {
			if ( is_multisite() ) {
				set_site_transient( 'aioseop_multisite_attachment_ids_urls', $this->image_ids_urls, DAY_IN_SECONDS );
			} else {
				set_transient( 'aioseop_attachment_ids_urls', $this->image_ids_urls, DAY_IN_SECONDS );
			}
		}

		/**
		 * Get Gallery Images
		 *
		 * Fetch images from WP, Jetpack and WooCommerce galleries.
		 *
		 * @since 2.4.2
		 * @since 2.11 Optimization #2008 - Reduce the need to convert url to id.
		 *
		 * @param WP_Post $post The post.
		 * @param array   $images the array of images.
		 */
		private function get_gallery_images( $post, &$images ) {
			if ( false === apply_filters( 'aioseo_include_images_in_wp_gallery', true ) ) {
				return;
			}

			// Check images galleries in the content. DO NOT run the_content filter here as it might cause issues with other shortcodes.
			if ( has_shortcode( $post->post_content, 'gallery' ) ) {
				/*
				 * TODO Investigate other alternatives to retrieve ID instead. Specifically Jetpack data.
				 *
				 * Is this even necessary? Jetpack uses many of the WP functions, some of which may already be in use.
				 * This is also limited to 1 source, and doesn't check other sources once a value is obtained.
				 *
				 * @link https://hayashikejinan.com/wp-content/uploads/jetpack_api/classes/Jetpack_PostImages.html
				 */
				if ( class_exists( 'Jetpack_PostImages' ) ) {
					// Get the jetpack gallery images.
					$jetpack = Jetpack_PostImages::get_images( $post->ID );
					if ( $jetpack ) {
						foreach ( $jetpack as $jetpack_image ) {
							$images[] = $jetpack_image['src'];
						}
					}
				}
			}

			$images = array_unique( $images );
		}

		/**
		 * Get Gallery Image IDs
		 *
		 * @uses get_post_galleries()
		 * @link https://developer.wordpress.org/reference/functions/get_post_galleries/
		 *
		 * @since 2.11
		 *
		 * @param WP_Post $post
		 * @return array
		 */
		private function get_gallery_image_ids( $post ) {
			$rtn_image_ids = array();
			if ( false === apply_filters( 'aioseo_include_images_in_wp_gallery', true ) ) {
				return $rtn_image_ids;
			}

			// Check images galleries in the content. DO NOT run the_content filter here as it might cause issues with other shortcodes.
			if ( has_shortcode( $post->post_content, 'gallery' ) ) {
				// Get the default WP gallery images.
				$galleries = get_post_galleries( $post, false );
				if ( ! empty( $galleries ) ) {
					foreach ( $galleries as $gallery ) {
						$gallery_ids = explode( ',', $gallery['ids'] );

						if ( ! empty( $gallery_ids ) ) {
							foreach ( $gallery_ids as $image_id ) {
								// Skip if invalid id.
								if ( ! is_numeric( $image_id ) ) {
									continue;
								}
								$image_id = intval( $image_id );

								array_push( $rtn_image_ids, $image_id );
							}
						}
					}
				}
			}

			// Check WooCommerce product gallery.
			if ( class_exists( 'WooCommerce' ) ) {
				$wc_image_ids = get_post_meta( $post->ID, '_product_image_gallery', true );
				if ( ! empty( $woo_images ) ) {
					$wc_image_ids = array_filter( explode( ',', $wc_image_ids ) );
					foreach ( $wc_image_ids as $image_id ) {
						if ( is_numeric( $image_id ) ) {
							$image_id = intval( $image_id );

							array_push( $rtn_image_ids, $image_id );
						}
					}
				}
			}

			return array_unique( $rtn_image_ids );
		}

		/**
		 * Get Content from Galleries
		 *
		 * Parses the content to find out if specified images galleries exist and if they do, parse them for images.
		 * Supports NextGen.
		 *
		 * @since 2.4.2
		 *
		 * @param string $content The post content.
		 * @return string
		 */
		private function get_content_from_galleries( $content ) {
			// Support for NextGen Gallery.
			static $gallery_types;

			$gallery_types   = array( 'ngg_images' );
			$types           = apply_filters( 'aioseop_gallery_shortcodes', $gallery_types );
			$gallery_content = '';

			if ( ! $types ) {
				return $gallery_content;
			}

			$found = array();
			if ( $types ) {
				foreach ( $types as $type ) {
					if ( has_shortcode( $content, $type ) ) {
						$found[] = $type;
					}
				}
			}

			// If none of the shortcodes-of-interest are found, bail.
			if ( empty( $found ) ) {
				return $gallery_content;
			}

			$galleries = array();

			if ( ! preg_match_all( '/' . get_shortcode_regex() . '/s', $content, $matches, PREG_SET_ORDER ) ) {
				return $gallery_content;
			}

			// Collect the shortcodes and their attributes.
			foreach ( $found as $type ) {
				foreach ( $matches as $shortcode ) {
					if ( $type === $shortcode[2] ) {

						$attributes = shortcode_parse_atts( $shortcode[3] );

						if ( '' === $attributes ) { // Valid shortcode without any attributes.
							$attributes = array();
						}

						$galleries[ $shortcode[2] ] = $attributes;
					}
				}
			}

			// Recreate the shortcodes and then render them to get the HTML content.
			if ( $galleries ) {
				foreach ( $galleries as $shortcode => $attributes ) {
					$code = '[' . $shortcode;

					foreach ( $attributes as $key => $value ) {
						$code .= " $key=$value";
					}
					$code .= ']';

					$gallery_content .= aioseop_do_shortcodes( $code );
				}
			}

			return $gallery_content;
		}

		/**
		 * AIOSEOP Clean URL
		 *
		 * Cleans the URL so that its acceptable in the sitemap.
		 *
		 * @since 2.4.1
		 *
		 * @param string $url The image url.
		 * @return string
		 */
		public function aioseop_clean_url( $url ) {
			// remove the query string.
			$url = strtok( $url, '?' );
			// make the url XML-safe.
			$url = htmlspecialchars( $url, ENT_COMPAT, 'UTF-8' );
			// Make the url absolute, if its relative.
			$url = aiosp_common::absolutize_url( $url );
			return apply_filters( 'aioseop_clean_url', $url );
		}

		/**
		 * The is_image_url_valid() function.
		 *
		 * Checks whether the image URL is valid.
		 *
		 * @since 2.4.1
		 * @since 2.4.3 Compatibility with Pre v4.7 wp_parse_url().
		 * @since 2.11.0 Sitemap Optimization #2008 - Changed to a more appropriate name.
		 * @since 3.0.0 Remove checks for old WP versions.
		 * @since 3.2.0 Remove redundant code.
		 *
		 * @param string $image The image src.
		 * @return bool
		 */
		public function is_image_url_valid( $image ) {
			// Bail if empty image.
			if ( empty( $image ) ) {
				return false;
			}

			$image   = aiosp_common::absolutize_url( $image );
			$extn    = pathinfo( $image, PATHINFO_EXTENSION );
			$allowed = apply_filters( 'aioseop_allowed_image_extensions', self::$image_extensions );
			// Bail if image does not refer to an image file otherwise Google Search Console might reject the sitemap.
			if ( ! in_array( $extn, $allowed, true ) ) {
				return false;
			}

			$image_host = wp_parse_url( $image, PHP_URL_HOST );
			$host       = wp_parse_url( home_url(), PHP_URL_HOST );

			if ( $image_host !== $host ) {
				// Allowed hosts will be provided in a wildcard format i.e. img.yahoo.* or *.akamai.*.
				// And we will convert that into a regular expression for matching.
				$whitelist = apply_filters( 'aioseop_images_allowed_from_hosts', array() );
				$allowed   = false;
				if ( $whitelist ) {
					foreach ( $whitelist as $pattern ) {
						if ( preg_match( '/' . str_replace( '*', '.*', $pattern ) . '/', $image_host ) === 1 ) {
							$allowed = true;
							break;
						}
					}
				}
				return $allowed;

			}
			return true;
		}

		/**
		 * Parse Content for Images
		 *
		 * Parse the post for images.
		 *
		 * @since 2.9.1
		 *
		 * @param string $content the post content.
		 * @param array  $images the array of images.
		 */
		public function parse_content_for_images( $content, &$images ) {
			// These tags should be WITHOUT trailing space because some plugins such as the nextgen gallery put newlines immediately after <img.
			$total = substr_count( $content, '<img' ) + substr_count( $content, '<IMG' );
			// no images found.
			if ( 0 === $total ) {
				return;
			}

			if ( class_exists( 'DOMDocument' ) ) {
				$dom = new domDocument();

				// Non-compliant HTML might give errors, so ignore them.
				libxml_use_internal_errors( true );
				$dom->loadHTML( $content );
				libxml_clear_errors();

				// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
				$dom->preserveWhiteSpace = false;

				$matches = $dom->getElementsByTagName( 'img' );
				foreach ( $matches as $match ) {
					$images[] = $match->getAttribute( 'src' );
				}
			} else {
				// Fall back to regex, but also report an error.
				global $img_err_msg;
				if ( ! isset( $img_err_msg ) ) {
					// we will log this error message only once, not per post.
					$img_err_msg = true;
					$this->debug_message( 'DOMDocument not found; using REGEX' );
				}
				preg_match_all( '/<img.*src=([\'"])?(.*?)\\1/', $content, $matches );
				if ( $matches && isset( $matches[2] ) ) {
					$images = array_merge( $images, $matches[2] );
				}
			}
		}

		/**
		 * Set Taxonomy Args
		 *
		 * Return excluded categories for taxonomy queries.
		 *
		 * @since ?
		 * @since 3.0.0 Added $taxonomies parameter.
		 * @since 3.0 Change 'exclude' to support excluding custom taxonomy terms. (Pro #240)
		 *
		 * @param array $taxonomies The array of taxonomy slugs.
		 * @param int   $page       The page number.
		 * @return array
		 */
		public function get_tax_args( $taxonomies, $page = 0 ) {
			$args = array();

			if ( ! empty( $this->options[ "{$this->prefix}indexes" ] ) ) {
				$args['number'] = $this->max_posts;
				$args['offset'] = $page * $this->max_posts;
			}

			$args['taxonomy'] = $this->show_or_hide_taxonomy( $taxonomies );

			$args['exclude'] = array();
			if ( $this->option_isset( 'excl_terms' ) ) {
				foreach ( $taxonomies as $v1_taxonomy ) {
					if ( isset( $this->options[ $this->prefix . 'excl_terms' ][ $v1_taxonomy ] ) ) {
						$args['exclude'] = array_merge( $args['exclude'], $this->options[ $this->prefix . 'excl_terms' ][ $v1_taxonomy ]['terms'] );
					}
				}
			}

			/**
			 * The aioseop_sitemap_exclude_tax_terms filter hook.
			 *
			 * Allows users to exclude (or include) taxonomy terms from the sitemap.
			 *
			 * @since 2.9
			 * @since 3.2.0 Rename filter hook & remove redundant params.
			 *
			 * @param array $args {
			 *     @type array $taxonomy Name of the taxonomy that is being included in the sitemap.
			 *     @type array $exclude IDs of taxonomy terms of the relevant taxonomy that need to be excluded.
			 * }
			 */
			$args = apply_filters( 'aioseop_sitemap_exclude_tax_terms', $args );

			return $args;
		}

		/**
		 * Set Post Args
		 *
		 * Return excluded categories and pages for post queries.
		 *
		 * @since ?
		 * @since 3.0 Change 'excl_terms' to tax_query format. (Pro #240)
		 *
		 * @param $args
		 * @return mixed
		 */
		public function set_post_args( $args ) {
			if ( $this->option_isset( 'excl_terms' ) ) {
				foreach ( $this->options[ $this->prefix . 'excl_terms' ] as $k1_taxonomy => $v1_tax_terms ) {
					if ( ! isset( $args['tax_query'] ) ) {
						$args['tax_query'] = array(
							'relation' => 'AND',
						);
					}
					$args['tax_query'][] = array(
						'taxonomy' => $k1_taxonomy,
						'terms'    => $v1_tax_terms['terms'],
						'operator' => 'NOT IN',
					);
				}
			}
			if ( $this->option_isset( 'excl_pages' ) ) {
				$args['exclude'] = $this->options[ $this->prefix . 'excl_pages' ];
			}

			return $args;
		}

		/**
		 * Get Data Archive Priority Data
		 *
		 * Return sitemap data for date archives.
		 *
		 * @since ?
		 *
		 * @return array
		 */
		public function get_date_archive_prio_data() {
			$args  = array(
				'numberposts' => 50000,
				'post_type'   => 'post',
			);
			$args  = $this->set_post_args( $args );
			$posts = $this->get_all_post_type_data( $args );

			return $this->get_date_archive_prio_from_posts( $posts );
		}

		/**
		 * Get Author Priority Data
		 *
		 * Return sitemap data for authors.
		 *
		 * @since ?
		 *
		 * @return array
		 */
		public function get_author_prio_data() {
			$args  = array(
				'numberposts' => 50000,
				'post_type'   => 'post',
			);
			$args  = $this->set_post_args( $args );
			$posts = $this->get_all_post_type_data( $args );

			return $this->get_author_prio_from_posts( $posts );
		}

		/**
		 * Get All Post Priority Data
		 *
		 * Return sitemap data for posts.
		 *
		 * @since ?
		 * @since 3.2.0 Update Last Change timestamp for WooCommerce shop page.
		 *
		 * @param string $include
		 * @param string $status
		 * @param int    $page
		 * @return array
		 */
		public function get_all_post_priority_data( $include = 'any', $status = 'publish', $page = 0 ) {
			$posts      = array();
			$page_query = array();
			if ( ! empty( $this->options[ "{$this->prefix}indexes" ] ) ) {
				$page_query = array( 'offset' => $page * $this->max_posts );
			}
			if ( ( 'publish' === $status ) && ( 'attachment' === $include ) ) {
				$status = 'inherit';
			}
			// TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison.
			if ( is_array( $include ) ) {
				$pos = array_search( 'attachment', $include );
				if ( false !== $pos ) {
					unset( $include[ $pos ] );
					$att_args = array(
						'post_type'   => 'attachment',
						'post_status' => 'inherit',
					);
					$att_args = array_merge( $att_args, $page_query );
					$posts    = $this->get_all_post_type_data( $att_args );
				}
			}
			$args  = array(
				'post_type'   => $include,
				'post_status' => $status,
			);
			$args  = array_merge( $args, $page_query );
			$args  = $this->set_post_args( $args );
			$posts = array_merge( $this->get_all_post_type_data( $args ), $posts );

			$links = $this->get_prio_from_posts( $posts, $this->get_default_priority( 'post', true ), $this->get_default_frequency( 'post', true ) );
			$links = array_merge( $links, $this->get_archive_prio_from_posts( $posts ) );

			$is_sitemap_indexes_disabled = empty( $this->options['aiosp_sitemap_indexes'] );
			if ( $is_sitemap_indexes_disabled || ( ! $is_sitemap_indexes_disabled && 'page' === $include ) ) {
				$links = $this->get_posts_page_timestamp( $links );
				$links = $this->get_prio_freq_static_homepage( $links );
				$links = $this->update_woocommerce_shop_timestamp( $links );
			}

			return $links;
		}

		/**
		 * The get_prio_freq_static_homepage() function.
		 *
		 * Sets the priority and frequency for the homepage if it is static.
		 *
		 * @since 3.2.0
		 *
		 * @param array $links
		 * @return array $links
		 */
		private function get_prio_freq_static_homepage( $links ) {
			if ( 0 === (int) get_option( 'page_on_front' ) ) {
				return $links;
			}

			$prio = 'no';
			$freq = 'no';
			if ( isset( $this->options['aiosp_sitemap_prio_homepage'] ) ) {
				$prio = $this->options['aiosp_sitemap_prio_homepage'];
			}
			if ( isset( $this->options['aiosp_sitemap_freq_homepage'] ) ) {
				$freq = $this->options['aiosp_sitemap_freq_homepage'];
			}

			$homepage_url   = get_site_url() . '/';
			$homepage_index = array_search( $homepage_url, array_column( $links, 'loc' ) ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.array_columnFound

			if ( ! $homepage_url ) {
				return $links;
			}

			if ( 'no' !== $prio ) {
				$links[ $homepage_index ]['priority'] = $prio;
			}
			if ( 'no' !== $freq ) {
				$links[ $homepage_index ]['changefreq'] = $freq;
			}

			return $links;
		}

		/**
		 * The update_woocommerce_shop_timestamp() function.
		 *
		 * Updates the Last Change timestamp for the WooCommerce shop page based on the last modified product - #2126.
		 *
		 * @since 3.2.0
		 *
		 * @param array $links
		 * @return array $links
		 */
		private function update_woocommerce_shop_timestamp( $links ) {
			if ( ! aioseop_is_woocommerce_active() ) {
				return $links;
			}

			$shop_page_url   = get_permalink( wc_get_page_id( 'shop' ) );
			$shop_page_index = array_search( $shop_page_url, array_column( $links, 'loc' ) ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.array_columnFound

			if ( ! $shop_page_index ) {
				return $links;
			}

			// TODO Use get_last_modified_post_timestamp() instead when #2721 is merged.
			$latest_modified_product = new WP_Query(
				array(
					'post_type'      => 'product',
					'post_status'    => 'publish',
					'posts_per_page' => 1,
					'orderby'        => 'modified',
					'order'          => 'DESC',
				)
			);

			if ( $latest_modified_product->have_posts() ) {
				$timestamp = $latest_modified_product->posts[0]->post_modified_gmt;
				$lastmod   = date( 'Y-m-d\TH:i:s\Z', mysql2date( 'U', $timestamp ) );
				// Last Change timestamp needs to be inserted as second attribute in order to have valid sitemap schema.
				// TODO Use insert_timestamp_as_second_attribute() instead when #2721 is merged.
				$links[ $shop_page_index ] = array_slice( $links[ $shop_page_index ], 0, 1, true ) + array( 'lastmod' => $lastmod ) + array_slice( $links[ $shop_page_index ], 1, null, true );
			}
			return $links;
		}

		/**
		 * Get All Permalinks
		 *
		 * Return a list of all permalinks.
		 *
		 * @since ?
		 *
		 * @param string $include
		 * @param string $status
		 * @return array
		 */
		public function get_all_permalinks( $include = 'any', $status = 'publish' ) {
			$args  = array(
				'post_type'   => $include,
				'post_status' => $status,
			);
			$args  = $this->set_post_args( $args );
			$posts = $this->get_all_post_type_data( $args );
			$links = $this->get_post_permalinks( $posts );
			if ( $this->option_isset( 'archive' ) ) {
				$links = array_merge( $links, $this->get_archive_permalinks( $posts ) );
			}
			if ( $this->option_isset( 'author' ) ) {
				$links = array_merge( $links, $this->get_author_permalinks( $posts ) );
			}

			return $links;
		}

		/**
		 * Cache Structure
		 *
		 * Static memory cache for permalink_structure option.
		 *
		 * @since ?
		 *
		 * @param $pre
		 * @return null
		 */
		public function cache_structure( $pre ) {
			return $this->cache_struct;
		}

		/**
		 * Cache Home
		 *
		 * Static memory cache for home option.
		 *
		 * @since ?
		 *
		 * @param $pre
		 * @return null
		 */
		public function cache_home( $pre ) {
			return $this->cache_home;
		}

		/**
		 * Cache Options
		 *
		 * Cache permalink_structure and home for repeated sitemap queries.
		 *
		 * @since ?
		 */
		public function cache_options() {
			static $start = true;
			if ( $start ) {
				$this->cache_struct = get_option( 'permalink_structure' );
				if ( ! empty( $this->cache_struct ) ) {
					add_filter( 'pre_option_permalink_structure', array( $this, 'cache_structure' ) );
				}
				$this->cache_home = get_option( 'home' );
				if ( ! empty( $this->cache_home ) ) {
					add_filter( 'pre_option_home', array( $this, 'cache_home' ) );
				}
				$start = false;
			}
		}

		/**
		 * Get Term Link
		 *
		 * Call get_term_link with caching in place.
		 *
		 * @since ?
		 *
		 * @param        $term
		 * @param string $taxonomy
		 * @return string|WP_Error
		 */
		public function get_term_link( $term, $taxonomy = '' ) {
			static $start = true;
			if ( $start ) {
				$this->cache_options();
				$start = false;
			}

			return get_term_link( $term, $taxonomy );
		}

		/**
		 * Get Permalink
		 *
		 * Call get_permalink with caching in place.
		 *
		 * @since ?
		 *
		 * @param $post
		 * @return false|string
		 */
		public function get_permalink( $post ) {
			static $start = true;
			if ( $start ) {
				$this->cache_options();
				$start = false;
			}

			return aioseop_get_permalink( $post );
		}

		/**
		 * Show or hide the taxonomy/taxonomies.
		 *
		 * @since 3.0.0
		 *
		 * @param array $taxonomy The array of taxonomy slugs.
		 *
		 * @return array The array of taxonomy slugs that need to be shown.
		 */
		private function show_or_hide_taxonomy( $taxonomy ) {
			/**
			 * Determines whether to show or hide the taxonomy/taxonomies.
			 *
			 * @since 3.0.0
			 *
			 * @param array $taxonomy The array of taxonomy slugs.
			 */
			return apply_filters( "{$this->prefix}show_taxonomy", $taxonomy );
		}

		/**
		 * Get All Terms Counts
		 *
		 * Return term counts using wp_count_terms().
		 *
		 * @since ?
		 *
		 * @param $args
		 * @return array|int|mixed|null|WP_Error
		 */
		public function get_all_term_counts( $args ) {
			$term_counts = null;
			if ( ! empty( $args ) && ! empty( $args['taxonomy'] ) ) {
				// TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison.
				if ( ! is_array( $args['taxonomy'] ) || ( count( $args['taxonomy'] ) == 1 ) ) {
					if ( is_array( $args['taxonomy'] ) ) {
						$args['taxonomy'] = array_shift( $args['taxonomy'] );
					}
					$term_counts = wp_count_terms( $this->show_or_hide_taxonomy( $args['taxonomy'] ), array( 'hide_empty' => true ) );
				} else {
					foreach ( $args['taxonomy'] as $taxonomy ) {
						if ( 'all' === $taxonomy ) {
							continue;
						}
						$term_counts[ $taxonomy ] = wp_count_terms( $this->show_or_hide_taxonomy( $taxonomy ), array( 'hide_empty' => true ) );
					}
				}
			}
			$term_counts = apply_filters( $this->prefix . 'term_counts', $term_counts, $args );

			return $term_counts;
		}

		/**
		 * Get All Post Counts
		 *
		 * Return post counts.
		 *
		 * @since ?
		 * @since 2.4.3 Refactored to use get_post_count() instead of wp_count_posts().
		 *
		 * @param $args
		 * @return array
		 */
		public function get_all_post_counts( $args ) {
			$post_counts = array();
			$status      = 'inherit';
			if ( ! empty( $args['post_status'] ) ) {
				$status = $args['post_status'];
			}
			if ( ! empty( $args ) && ! empty( $args['post_type'] ) ) {
				// #884: removed hard-to-understand code here which suspected $args['post_type'] to NOT be an array. Do not see any case in which this is likely to happen.
				foreach ( $args['post_type'] as $post_type ) {
					$count_args = $args;
					if ( 'all' === $post_type ) {
						continue;
					}
					if ( 'attachment' === $post_type ) {
						$count_args['post_status'] = 'inherit';
					}

					$count_args['post_type']   = $post_type;
					$post_counts[ $post_type ] = $this->get_post_count( $count_args );
				}
			}
			$post_counts = apply_filters( $this->prefix . 'post_counts', $post_counts, $args );

			return $post_counts;
		}

		/**
		 * Modify Post Params for External Plugins
		 *
		 * Modify the post arguments in case third-party plugins are being used e.g. WPML.
		 *
		 * @since 2.4.5
		 *
		 * @param $args
		 */
		public function modify_post_params_for_external_plugins( &$args ) {
			// if WPML is being used, do not suppress filters.
			if ( defined( 'ICL_SITEPRESS_VERSION' ) ) {
				$args['suppress_filters'] = false;
			}

			$args = apply_filters( $this->prefix . 'modify_post_params', $args );
		}

		/**
		 * Get Post Count
		 *
		 * Return post counts for the specified arguments.
		 *
		 * @since ?
		 *
		 * @param $args
		 * @return int
		 */
		public function get_post_count( $args ) {
			$this->modify_post_params_for_external_plugins( $args );

			// we will use WP_Query instead of get_posts here as that is more efficient.
			// BEWARE: since we are using WP_Query, suppress_filters is false.
			$args['posts_per_page']         = -1;
			$args['fields']                 = 'ids';
			$args['update_post_meta_cache'] = false;
			$args['update_post_term_cache'] = false;
			$query                          = new WP_Query( $args );
			if ( $query->have_posts() ) {
				return $query->post_count;
			}
			return 0;
		}

		/**
		 * Get total post count.
		 *
		 * @param $args
		 *
		 * @return int
		 */
		public function get_total_post_count( $args ) {
			$total  = 0;
			$counts = $this->get_all_post_counts( $args );
			if ( ! empty( $counts ) ) {
				foreach ( $counts as $count ) {
					$total += $count;
				}
			}

			return $total;
		}

		/**
		 * Get All Post Type Data
		 *
		 * Return post data using get_posts().
		 *
		 * @since ?
		 * @since 3.0 Changed to exclude noindex post types & posts. #1382
		 *
		 * @global array $aioseop_options
		 *
		 * @param array $args Query Arguments.
		 * @return array|mixed
		 */
		public function get_all_post_type_data( $args ) {
			global $aioseop_options;
			$defaults = array(
				'numberposts'   => $this->max_posts,
				'offset'        => 0,
				'category'      => 0,
				'orderby'       => 'post_date',
				'order'         => 'ASC',
				'include'       => array(),
				'exclude'       => array(),
				'post_type'     => 'any',
				'meta_query'    => array(),
				'cache_results' => false,
				'no_found_rows' => true,
			);

			$this->modify_post_params_for_external_plugins( $defaults );

			/*
			 * Filter to exclude password protected posts.
			 * TODO: move to its own function and call it from here, returning whatever is appropriate.
			 * @since 2.3.12
			 */
			if ( apply_filters( 'aioseop_sitemap_include_password_posts', true ) === false ) {
				$defaults['has_password'] = false;
			}

			$args = wp_parse_args( $args, $defaults );
			if ( empty( $args['post_type'] ) ) {
				return apply_filters( $this->prefix . 'post_filter', array(), $args );
			}
			$exclude_slugs = array();
			if ( ! empty( $args['exclude'] ) ) {
				$exclude = preg_split( '/[\s,]+/', trim( $args['exclude'] ) );
				if ( ! empty( $exclude ) ) {
					foreach ( $exclude as $k => $v ) {
						if ( ! is_numeric( $v ) ) {
							$exclude_slugs[] = $v;
							unset( $exclude[ $k ] );
						}
					}
					if ( ! empty( $exclude_slugs ) ) {
						$args['exclude'] = implode( ',', $exclude );
					}
				}
			}
			if ( ! is_array( $args['exclude'] ) ) {
				$args['exclude'] = explode( ',', $args['exclude'] );
			}

			// Exclude (method) query args.
			$ex_args               = $args;
			$ex_args['meta_query'] = array(
				'relation' => 'OR',

				array(
					'key'     => '_aioseop_sitemap_exclude',
					'value'   => 'on',
					'compare' => '=',
				),
				array(
					'key'     => '_aioseop_noindex',
					'value'   => 'on',
					'compare' => '=',
				),
			);
			// This needs to be -1 so that excluding posts isn't restricted to affect posts to not be excluded properly.
			$ex_args['posts_per_page'] = -1;
			$ex_args['fields']         = 'ids';

			// Exclude (method) query.
			$q_exclude = new WP_Query( $ex_args );
			if ( ! empty( $q_exclude->posts ) ) {
				$args['exclude'] = array_merge( $args['exclude'], $q_exclude->posts );
			}
			$this->excludes = array_merge( $args['exclude'], $exclude_slugs ); // Add excluded slugs and IDs to class var.

			// Avoid if possible.
			// Include (method) query args for including posts that may have been excluded;
			// for example, exclude post type, but include certain posts.
			// NOTE: Do NOT use this for basic including. It's best to avoid an additional query.
			$args_include = array(
				'post_type'      => array(),
				'meta_query'     => array(
					'relation' => 'OR',
					array(
						'key'     => '_aioseop_noindex',
						'value'   => 'off',
						'compare' => '=',
					),

				),
				'posts_per_page' => $this->max_posts,
			);

			// Exclude from main query, and check if a Query Include is needed.
			// Check for NoIndex Post Types, BUT also check for Index on NoIdex Post Type.
			if ( is_array( $aioseop_options['aiosp_cpostnoindex'] ) ) {
				// Check if wp_query_args post_type is an array or string.
				if ( is_array( $args['post_type'] ) ) {
					foreach ( $args['post_type'] as $index => $post_type ) {
						if ( in_array( $post_type, $aioseop_options['aiosp_cpostnoindex'], true ) ) {
							$args_include['post_type'][] = $post_type;
							unset( $args['post_type'][ $index ] );
						}
					}
				} else {
					if ( in_array( $args['post_type'], $aioseop_options['aiosp_cpostnoindex'], true ) ) {
						$args_include['post_type'][] = $args['post_type'];

						$q_include = new WP_Query( $args_include );
						// Return posts on single post type query, since no additional query is needed.
						return $q_include->posts;
					}
				}
			}

			// Avoid if possible.
			// Include (method) query.
			// NOTE: Do NOT use this for basic including. It's best to avoid an additional query.
			$posts_include = array();
			if ( ! empty( $args_include['post_type'] ) ) {
				$q_include = new WP_Query( $args_include );
				// When posts exists from the include method, add to $posts_include to add to final query.
				if ( ! empty( $q_include->posts ) ) {
					$posts_include = $q_include->posts;
				}
			}

			// TODO: consider using WP_Query instead of get_posts to improve efficiency.
			/**
			 * {$module_prefix}post_query
			 *
			 * Arguments to use on get_posts().
			 *
			 * @since ?
			 *
			 * @param array $args {
			 *     Arguments/params for get_posts.
			 *     @see get_posts()
			 *     @link https://developer.wordpress.org/reference/functions/get_posts/
			 * }
			 */
			$posts = get_posts( apply_filters( $this->prefix . 'post_query', $args ) );

			// TODO Possibly change to exclude with post__not_in.
			// Hardcoded exclude concept.
			if ( ! empty( $exclude_slugs ) ) {
				foreach ( $posts as $k => $v ) {
					// TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison.
					if ( in_array( $v->post_name, $exclude_slugs ) ) {
						unset( $posts[ $k ] );
					}
				}
			}
			// Hardcoded include concept.
			foreach ( $posts_include as $k1_post_id => $v1_post ) {
				$posts[ $k1_post_id ] = $v1_post;
			}

			/**
			 * {$module_prefix}post_filter
			 *
			 * Posts from finalized query for sitemap data.
			 *
			 * @since ?
			 *
			 * @param array $posts {
			 *     @type WP_Post ${$post_id} {
			 *         @see WP_Post object for more information.
			 *         @link https://codex.wordpress.org/Class_Reference/WP_Post
			 *     }
			 * }
			 * @param array $args {
			 *     Arguments/params for get_posts.
			 *     @see get_posts()
			 *     @link https://developer.wordpress.org/reference/functions/get_posts/
			 * }
			 */
			$posts = apply_filters( $this->prefix . 'post_filter', $posts, $args );

			return $posts;
		}
	}
}
