<?php
/*
 * License: GPLv3
 * License URI: https://www.gnu.org/licenses/gpl.txt
 * Copyright 2012-2026 Jean-Sebastien Morisset (https://wpsso.com/)
 */

if ( ! defined( 'ABSPATH' ) ) {

	die( 'These aren\'t the droids you\'re looking for.' );
}

if ( ! defined( 'WPSSO_PLUGINDIR' ) ) {

	die( 'Do. Or do not. There is no try.' );
}

/*
 * Extended by the WpssoPost, WpssoTerm, and WpssoUser classes.
 */
if ( ! class_exists( 'WpssoAbstractWpMeta' ) ) {

	abstract class WpssoAbstractWpMeta {

		protected $p;		// Wpsso class object.
		protected $a;		// Wpsso add-on class object.
		protected $upgrade;	// WpssoUpgradeMeta class object.
		protected $form;	// SucomForm class object.

		protected $md_cache_disabled = false;	// Disable local caches.

		protected static $head_tags = false;	// Must be false by default.
		protected static $head_info = array();

		private static $mod_defaults = array(
			'id'                      => 0,			// Post, term, or user ID.
			'name'                    => false,		// Module name ('post', 'term', or 'user').
			'name_transl'             => false,		// Module name translated.
			'obj'                     => false,		// Module object.
			'wp_obj'                  => false,		// WP object (WP_Post, WP_Term, etc.).
			'query_vars'              => array(),		// Defined by WpssoPage->get_mod().
			'posts_args'              => array(),
			'paged'                   => false,		// False or numeric (aka $wp_query->query_vars[ 'paged' ]).
			'paged_total'             => false,		// False or numberic (aka $wp_query->max_num_pages).
			'is_404'                  => false,
			'is_archive'              => false,
			'is_attachment'           => false,		// Post type is 'attachment'.
			'is_comment'              => false,
			'is_date'                 => false,
			'is_day'                  => false,
			'is_feed'                 => false,		// RSS feed.
			'is_home'                 => false,		// Home page (static or blog archive).
			'is_home_page'            => false,		// Static front page (singular post).
			'is_home_posts'           => false,		// Static posts page or blog archive page.
			'is_month'                => false,
			'is_post'                 => false,
			'is_post_type_archive'    => false,		// Post is an archive page.
			'is_public'               => true,
			'is_search'               => false,
			'is_term'                 => false,
			'is_user'                 => false,
			'is_year'                 => false,
			'comment_author'          => false,		// Comment author user ID.
			'comment_author_name'     => false,		// Comment author name.
			'comment_author_url'      => false,		// Comment author URL.
			'comment_paged'           => false,		// False or numeric (aka $wp_query->query_vars[ 'cpage' ]).
			'comment_parent'          => false,
			'comment_rating'          => false,
			'comment_time'            => false,
			'use_post'                => false,
			'post_slug'               => false,		// Post name (aka slug).
			'post_type'               => false,		// Post type name.
			'post_type_label_plural'  => false,		// Post type plural name.
			'post_type_label_single'  => false,		// Post type singular name.
			'post_mime_type'          => false,		// Post mime type (ie. image/jpg).
			'post_mime_group'         => false,		// Post mime group (ie. image).
			'post_mime_subgroup'      => false,		// Post mime subgroup (ie. jpg).
			'post_status'             => false,		// Post status name.
			'post_author'             => false,		// Post author id.
			'post_coauthors'          => array(),
			'post_time'               => false,		// Post published time (ISO 8601 date or false).
			'post_timestamp'          => false,		// Post published time (Unit timestamp or false).
			'post_modified_time'      => false,		// Post modified time (ISO 8601 date or false).
			'post_modified_timestamp' => false,		// Post modified time (Unit timestamp or false).
			'post_parent'             => false,		// Post parent id.
			'post_primary_tax_slug'   => 'category',	// Primary taxonomy, filtered by 'wpsso_primary_tax_slug' in WpssoPost->get_mod().
			'post_taxonomies'         => array(),		// Post taxonomy slugs.
			'tax_slug'                => '',
			'tax_label_plural'        => false,		// Taxonomy plural name.
			'tax_label_single'        => false,		// Taxonomy singular name.
			'term_tax_id'             => false,
			'user_name'               => '',		// User display name.
			'wpml_code'               => '',		// WPML language code.
		);

		public function __construct( &$plugin ) {

			return self::must_be_extended();
		}

		/*
		 * Disable local cache for get_mod(), get_options(), and get_defaults().
		 */
		public function md_cache_disable() {

			$this->md_cache_disabled = true;
		}

		public function md_cache_enable() {

			$this->md_cache_disabled = false;
		}

		/*
		 * Add WordPress action and filters callbacks.
		 */
		public function add_wp_callbacks() {

			return self::must_be_extended();
		}

		/*
		 * Since WPSSO Core v17.0.0.
		 *
		 * Register our comment, post, term, and user meta.
		 *
		 * Do not register a sanitation callback as the sanitation filter is executed much too frequently by WordPress, and
		 * only provides $meta_value, $meta_key, and $object_type arguments.
		 *
		 * See sanitize_meta() in wordpress/wp-includes/meta.php.
		 * See WpssoComment->add_wp_callbacks().
		 * See WpssoPost->add_wp_callbacks().
		 * See WpssoTerm->add_wp_callbacks().
		 * See WpssoUser->add_wp_callbacks().
		 */
		protected function register_meta( $object_type, $meta_key ) {

			if ( $this->p->debug->enabled ) {

				$this->p->debug->mark();
			}

			$meta_title = _x( $this->p->cf[ 'meta' ][ 'title' ], 'metabox title', 'wpsso' );

			register_meta( $object_type, $meta_key, $args = array(
				'type'              => 'array',
				'description'       => sprintf( _x( '%s Metadata', 'metadata title', 'wpsso' ), $meta_title ),
				'default'           => array(),
				'single'            => true,
				'sanitize_callback' => null,	// Executed much too frequently by WordPress.
				'auth_callback'     => null,
				'show_in_rest'      => false,
				'revisions_enabled' => 'post' === $object_type? true : false,	// Can only be used when the object type is 'post'.
			) );

			/*
			 * Although announced as a feature, WordPress v6.4 cannot display revisions with post meta arrays.
			 *
			 * Hooking the '_wp_post_revision_fields' filter to add the WPSSO_META_NAME array to the revision page will
			 * cause a PHP fatal error in PHP v8.2.
			 *
			 * See https://core.trac.wordpress.org/ticket/59827
			 *
			 * Enable this code and update the version check when WordPress is able to display revisions with post meta arrays.
			 *
			 * global $wp_version;
			 *
			 * if ( version_compare( $wp_version, '6.4.2', '<' ) ) {
			 *
			 * 	if ( $this->p->debug->enabled ) {
			 *
			 * 		$this->p->debug->log( 'exiting early: no revisions for post meta arrays in WordPress v' .  $wp_version );
			 * 	}
			 *
			 * 	return;	// Stop here.
			 * }
			 *
			 * add_filter( '_wp_post_revision_fields', array( $this, 'revision_fields_meta_title' ), 10, 2 );
			 *
			 * add_filter( '_wp_post_revision_field_' . WPSSO_META_NAME, array( $this, 'get_revision_fields_meta_options' ), 10, 3 );
			 */
		}

		/*
		 * Since WPSSO Core v17.3.0.
		 *
		 * Add WPSSO_META_NAME to the revision fields shown.
		 */
		public function revision_fields_meta_title( $fields, $wp_obj ) {

			$meta_title = _x( $this->p->cf[ 'meta' ][ 'title' ], 'metabox title', 'wpsso' );

			$fields[ WPSSO_META_NAME ] = sprintf( _x( '%s Metadata', 'metadata title', 'wpsso' ), $meta_title );

			return $fields;
		}

		/*
		 * Since WPSSO Core v17.3.0.
		 *
		 * Cleanup and convert the options array to a text string.
		 */
		public function get_revision_fields_meta_options( $md_opts, $meta_key, $wp_obj ) {

			if ( is_string( $md_opts ) ) {	// Nothing to do.

				return $md_opts;

			} elseif ( is_array( $md_opts ) ) {	// Convert array to string.

				$this->p->opt->remove_versions_checksum( $md_opts );

				return var_export( $md_opts, true );
			}

			return '';
		}

		/*
		 * Get the $mod object for a post, term, or user ID.
		 */
		public function get_mod( $obj_id ) {

			return self::must_be_extended( self::$mod_defaults );
		}

		public function get_mod_wp_object( array $mod ) {

			return self::must_be_extended( $ret_val = false );
		}

		public static function get_mod_defaults() {

			return self::$mod_defaults;
		}

		/*
		 * Get the $mod object for the home page.
		 */
		public static function get_mod_home() {

			$wpsso =& Wpsso::get_instance();

			if ( $wpsso->debug->enabled ) {

				$wpsso->debug->mark();
			}

			$mod = self::$mod_defaults;

			$post_id = 0;

			if ( 'page' === get_option( 'show_on_front' ) ) {

				if ( ! $post_id = (int) get_option( 'page_on_front', $default = 0 ) ) {

					$post_id = (int) get_option( 'page_for_posts', $default = 0 );
				}

				$mod = $wpsso->post->get_mod( $post_id );
			}

			/*
			 * Post elements.
			 */
			$mod[ 'is_home' ] = true;	// Home page (static or blog archive).

			return $mod;
		}

		/*
		 * Option handling methods:
		 *
		 *	get_defaults()
		 *	get_options()
		 *	save_options()
		 *	delete_options()
		 */
		public function get_defaults( $obj_id, $md_key = false ) {

			if ( $this->p->debug->enabled ) {

				$this->p->debug->log_args( array(
					'obj_id' => $obj_id,
					'md_key' => $md_key,
				) );
			}

			static $local_fifo = array();

			/*
			 * Maybe retrieve the array from the local cache.
			 *
			 * Note that the local cache is unique to each child class, so we can simply index by the object ID.
			 */
			$md_defs = array();

			if ( WpssoOptions::is_cache_allowed() ) {

				if ( ! $this->md_cache_disabled ) {

					if ( isset( $local_fifo[ $obj_id ] ) ) {

						if ( $this->p->debug->enabled ) {

							$this->p->debug->log( 'using defaults from local cache for id ' . $obj_id );
						}

					} else {

						/*
						 * Maybe limit the number of array elements.
						 */
						$local_fifo = SucomUtil::array_slice_fifo( $local_fifo, WPSSO_CACHE_ARRAY_FIFO_MAX );

						if ( $this->p->debug->enabled ) {

							$this->p->debug->mark_diff( 'adding local cache element for id ' . $obj_id );
						}

						$local_fifo[ $obj_id ] = array();
					}

					$md_defs =& $local_fifo[ $obj_id ];

				} elseif ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'metadata caching is disabled for id ' . $obj_id );
				}

			} elseif ( $this->p->debug->enabled ) {

				$this->p->debug->log( 'options caching is not allowed for id ' . $obj_id );
			}

			if ( empty( $md_defs[ 'opt_filtered' ] ) ) {

				/*
				 * Set before calling filters to prevent recursion.
				 */
				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'setting opt_filtered to 1' );
				}

				$md_defs[ 'opt_filtered' ] = 1;

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'required call to ' . get_class( $this ) . '->get_mod() for id ' . $obj_id );
				}

				$mod = $this->get_mod( $obj_id );

				$opts =& $this->p->options;		// Shortcut variable name.

				$def_lang            = SucomUtilWP::get_locale( $mod, $read_cache = false );	// Get locale for post, term, or user object.
				$def_dimension_units = WpssoUtilUnits::get_dimension_text();
				$def_fluid_vol_units = WpssoUtilUnits::get_fluid_volume_text();
				$def_weight_units    = WpssoUtilUnits::get_weight_text();
				$def_og_type         = $this->p->og->get_mod_og_type_id( $mod, $use_md_opts = false );
				$def_schema_type     = $this->p->schema->get_mod_schema_type_id( $mod, $use_md_opts = false );
				$def_primary_term_id = $this->p->post->get_default_term_id( $mod, $mod[ 'post_primary_tax_slug' ] );	// Returns term id or false.
				$def_reading_mins    = $this->p->page->get_reading_mins( $mod );
				$def_currency        = $this->p->get_options( 'og_def_currency', 'USD' );
				$def_country         = $this->p->get_options( 'og_def_country', 'none' );
				$def_timezone        = $this->p->get_options( 'og_def_timezone', 'UTC' );
				$def_art_section     = $this->p->get_options( 'schema_def_article_section', 'none' );
				$def_adult_type      = $this->p->get_options( 'schema_def_product_adult_type', 'none' );
				$def_age_group       = $this->p->get_options( 'schema_def_product_age_group', 'none' );
				$def_product_cat     = $this->p->get_options( 'schema_def_product_category', 'none' );		// Default Product Google Category.
				$def_product_cond    = $this->p->get_options( 'schema_def_product_condition', 'none' );		// Default Product Condition.
				$def_product_mrp     = $this->p->get_options( 'schema_def_product_mrp', 'none' );		// Default Product Return Policy.
				$def_ener_eff_min    = $this->p->get_options( 'schema_def_product_energy_efficiency_min', 'https://schema.org/EUEnergyEfficiencyCategoryD' );
				$def_ener_eff_max    = $this->p->get_options( 'schema_def_product_energy_efficiency_max', 'https://schema.org/EUEnergyEfficiencyCategoryA3Plus' );
				$def_size_group_0    = $this->p->get_options( 'schema_def_product_size_group_0', 'none' );
				$def_size_group_1    = $this->p->get_options( 'schema_def_product_size_group_1', 'none' );
				$def_size_system     = $this->p->get_options( 'schema_def_product_size_system', 'none' );
				$def_target_gender   = $this->p->get_options( 'schema_def_product_target_gender', 'none' );
				$def_size_name       = 'wpsso-opengraph';
				$def_media_request   = array( 'og_vid_upload' );
				$def_media_info      = $this->p->media->get_media_info( $def_size_name, $def_media_request, $mod, $md_pre = 'none' );

				$md_defs = array(
					'checksum'          => '',	// Checksum of plugin versions.
					'opt_checksum'      => '',	// Checksum of option versions.
					'opt_filtered'      => 1,	// Set before calling filters to prevent recursion.
					'opt_versions'      => array(),
					'attach_img_crop_x' => 'none',
					'attach_img_crop_y' => 'none',

					/*
					 * Gravity View (Side Metabox).
					 */
					'gv_id_title' => 0,	// Title Field ID
					'gv_id_desc'  => 0,	// Description Field ID
					'gv_id_img'   => 0,	// Post Image Field ID

					/*
					 * Edit General.
					 */
					'primary_term_id' => $def_primary_term_id,	// Primary Category.
					'seo_title'       => '',			// SEO Title Tag.
					'seo_desc'        => '',			// SEO Meta Description.
					'og_type'         => $def_og_type,		// Open Graph Type.
					'og_title'        => '',			// Social Title.
					'og_desc'         => '',			// Social Description.
					'pin_img_desc'    => '',			// Pinterest Description.
					'tc_title'        => '',			// X (Twitter) Card Title.
					'tc_desc'         => '',			// X (Twitter) Card Description.

					/*
					 * Edit Media tab.
					 */
					'og_img_max'             => isset( $opts[ 'og_img_max' ] ) ? (int) $opts[ 'og_img_max' ] : 1,
					'og_vid_max'             => isset( $opts[ 'og_vid_max' ] ) ? (int) $opts[ 'og_vid_max' ] : 1,
					'og_vid_prev_img'        => empty( $opts[ 'og_vid_prev_img' ] ) ? 0 : 1,
					'og_vid_autoplay'        => empty( $opts[ 'og_vid_autoplay' ] ) ? 0 : 1,
					'og_img_id'              => '',
					'og_img_id_lib'          => 'wp',
					'og_img_url'             => '',
					'og_vid_embed'           => '',
					'og_vid_url'             => '',
					'og_vid_title'           => '',
					'og_vid_desc'            => '',
					'og_vid_stream_url'      => '',
					'og_vid_width'           => '',
					'og_vid_height'          => '',
					'og_vid_upload_date'     => $def_media_info[ 'og_vid_upload_date' ] ? $def_media_info[ 'og_vid_upload_date' ] : '',
					'og_vid_upload_time'     => $def_media_info[ 'og_vid_upload_time' ] ? $def_media_info[ 'og_vid_upload_time' ] : 'none',
					'og_vid_upload_timezone' => $def_media_info[ 'og_vid_upload_timezone' ] ? $def_media_info[ 'og_vid_upload_timezone' ] : $def_timezone,
					'pin_img_id'             => '',
					'pin_img_id_lib'         => 'wp',
					'pin_img_url'            => '',
					'tc_lrg_img_id'          => '',
					'tc_lrg_img_id_lib'      => 'wp',
					'tc_lrg_img_url'         => '',
					'tc_sum_img_id'          => '',
					'tc_sum_img_id_lib'      => 'wp',
					'tc_sum_img_url'         => '',
					'schema_img_id'          => '',
					'schema_img_id_lib'      => 'wp',
					'schema_img_url'         => '',

					/*
					 * Edit Visibility tab.
					 */
					'canonical_url' => '',		// Canonical URL.
					'redirect_url'  => '',		// 301 Redirect URL.

					/*
					 * Open Graph and Schema Product type.
					 *
					 * See WpssoOpengraph->add_data_og_type_md().
					 */
					'product_category'              => $def_product_cat,		// Product Google Category.
					'product_brand'                 => '',
					'product_price'                 => '0.00',			// Product Price.
					'product_currency'              => $def_currency,
					'product_min_advert_price'      => '0.00',			// Product Min Advert Price.
					'product_avail'                 => 'none',			// Product Availability.
					'product_condition'             => $def_product_cond,		// Product Condition.
					'product_energy_efficiency'     => 'none',			// Product Energy Rating.
					'product_energy_efficiency_min' => $def_ener_eff_min,
					'product_energy_efficiency_max' => $def_ener_eff_max,
					'product_material'              => '',
					'product_mrp'                   => $def_product_mrp,		// Product Return Policy.
					'product_pattern'               => '',
					'product_color'                 => '',
					'product_target_gender'         => $def_target_gender,
					'product_size'                  => '',
					'product_size_group_0'          => $def_size_group_0,
					'product_size_group_1'          => $def_size_group_1,
					'product_size_system'           => $def_size_system,
					'product_age_group'             => $def_age_group,
					'product_adult_type'            => $def_adult_type,
					'product_length_value'          => '',				// Product Net Len. / Depth.
					'product_length_units'          => $def_dimension_units,
					'product_width_value'           => '',				// Product Net Width.
					'product_width_units'           => $def_dimension_units,
					'product_height_value'          => '',				// Product Net Height.
					'product_height_units'          => $def_dimension_units,
					'product_weight_value'          => '',				// Product Net Weight.
					'product_weight_units'          => $def_weight_units,
					'product_fluid_volume_value'    => '',				// Product Fluid Volume.
					'product_fluid_volume_units'    => $def_fluid_vol_units,
					'product_shipping_length_value' => '',				// Product Shipping Length.
					'product_shipping_length_units' => $def_dimension_units,
					'product_shipping_width_value'  => '',				// Product Shipping Width.
					'product_shipping_width_units'  => $def_dimension_units,
					'product_shipping_height_value' => '',				// Product Shipping Height.
					'product_shipping_height_units' => $def_dimension_units,
					'product_shipping_weight_value' => '',				// Product Shipping Weight.
					'product_shipping_weight_units' => $def_weight_units,
					'product_retailer_part_no'      => '',				// Product SKU.
					'product_mfr_part_no'           => '',				// Product MPN.
					'product_gtin14'                => '',				// Product GTIN-14.
					'product_gtin13'                => '',				// Product GTIN-13 (EAN).
					'product_gtin12'                => '',				// Product GTIN-12 (UPC).
					'product_gtin8'                 => '',				// Product GTIN-8.
					'product_gtin'                  => '',				// Product GTIN.
					'product_isbn'                  => '',				// Product ISBN.
					'product_award_0'               => '',				// Product Award.
					'product_award_1'               => '',				// Product Award.
					'product_award_2'               => '',				// Product Award.
					'product_award_3'               => '',				// Product Award.
					'product_award_4'               => '',				// Product Award.

					/*
					 * All Schema Types.
					 */
					'schema_type'            => $def_schema_type,	// Schema Type.
					'schema_title'           => '',			// Schema Name.
					'schema_title_alt'       => '',			// Schema Alternate Name.
					'schema_title_bc'        => '',			// Schema Breadcrumb Name.
					'schema_desc'            => '',			// Schema Description.
					'schema_addl_type_url_0' => '',
					'schema_addl_type_url_1' => '',
					'schema_addl_type_url_2' => '',
					'schema_addl_type_url_3' => '',
					'schema_addl_type_url_4' => '',
					'schema_sameas_url_0'    => '',
					'schema_sameas_url_1'    => '',
					'schema_sameas_url_2'    => '',
					'schema_sameas_url_3'    => '',
					'schema_sameas_url_4'    => '',

					/*
					 * Schema Creative Work.
					 */
					'schema_headline'        => '',						// Headline.
					'schema_text'            => '',						// Full Text.
					'schema_keywords_csv'    => '',						// Keywords.
					'schema_lang'            => $def_lang,					// Language.
					'schema_family_friendly' => $opts[ 'schema_def_family_friendly' ],	// Family Friendly.
					'schema_copyright_year'  => '',						// Copyright Year.
					'schema_license_url'     => '',						// License URL.
					'schema_pub_org_id'      => $opts[ 'schema_def_pub_org_id' ],		// Publisher Organization.
					'schema_pub_person_id'   => $opts[ 'schema_def_pub_person_id' ],	// Publisher Person.
					'schema_prov_org_id'     => $opts[ 'schema_def_prov_org_id' ],		// Provider Organization.
					'schema_prov_person_id'  => $opts[ 'schema_def_prov_person_id' ],	// Provider Person.
					'schema_fund_org_id'     => $opts[ 'schema_def_fund_org_id' ],		// Funder Organization.
					'schema_fund_person_id'  => $opts[ 'schema_def_fund_person_id' ],	// Funder Person.
					'schema_ispartof_url_0'  => '',						// Is Part of URLs.
					'schema_ispartof_url_1'  => '',						// Is Part of URLs.
					'schema_ispartof_url_2'  => '',						// Is Part of URLs.
					'schema_award_0'         => '',						// Creative Work Awards.
					'schema_award_1'         => '',						// Creative Work Awards.
					'schema_award_2'         => '',						// Creative Work Awards.
					'schema_award_3'         => '',						// Creative Work Awards.
					'schema_award_4'         => '',						// Creative Work Awards.

					/*
					 * See https://schema.org/citation.
					 *
					 * There is very little information available from Google about the expected JSON markup structure
					 * for citations - the only information available is from the the Google's Dataset type
					 * documentation.
					 *
					 * See https://developers.google.com/search/docs/appearance/structured-data/dataset.
					 */
					'schema_citation_0' => '',	// Reference Citations.
					'schema_citation_1' => '',	// Reference Citations.
					'schema_citation_2' => '',	// Reference Citations.
					'schema_citation_3' => '',	// Reference Citations.
					'schema_citation_4' => '',	// Reference Citations.

					/*
					 * Schema Article.
					 */
					'schema_article_section' => $def_art_section,	// Article Section.
					'schema_reading_mins'    => $def_reading_mins,	// Est. Reading Time.

					/*
					 * Schema Book.
					 */
					'schema_book_author_type'      => $opts[ 'schema_def_book_author_type' ],	// Book Author Type.
					'schema_book_author_name'      => '',						// Book Author Name.
					'schema_book_author_url'       => '',						// Book Author URL.
					'schema_book_pub_date'         => '',						// Book Published Date (Y-m-d).
					'schema_book_pub_time'         => 'none',					// Book Published Time (H:i).
					'schema_book_pub_timezone'     => $def_timezone,				// Book Published Timezone.
					'schema_book_created_date'     => '',						// Book Created Date (Y-m-d).
					'schema_book_created_time'     => 'none',					// Book Created Time (H:i).
					'schema_book_created_timezone' => $def_timezone,				// Book Created Timezone.
					'schema_book_edition'          => '',						// Book Edition.
					'schema_book_format'           => $opts[ 'schema_def_book_format' ],		// Book Format.
					'schema_book_pages'            => '',						// Book Pages.
					'schema_book_isbn'             => '',						// Book ISBN.
					'schema_book_offer_name_0'     => '',
					'schema_book_offer_name_1'     => '',
					'schema_book_offer_name_2'     => '',
					'schema_book_offer_name_3'     => '',
					'schema_book_offer_name_4'     => '',
					'schema_book_offer_price_0'    => '',
					'schema_book_offer_price_1'    => '',
					'schema_book_offer_price_2'    => '',
					'schema_book_offer_price_3'    => '',
					'schema_book_offer_price_4'    => '',
					'schema_book_offer_currency_0' => $def_currency,
					'schema_book_offer_currency_1' => $def_currency,
					'schema_book_offer_currency_2' => $def_currency,
					'schema_book_offer_currency_3' => $def_currency,
					'schema_book_offer_currency_4' => $def_currency,
					'schema_book_offer_avail_0'    => 'https://schema.org/InStock',
					'schema_book_offer_avail_1'    => 'https://schema.org/InStock',
					'schema_book_offer_avail_2'    => 'https://schema.org/InStock',
					'schema_book_offer_avail_3'    => 'https://schema.org/InStock',
					'schema_book_offer_avail_4'    => 'https://schema.org/InStock',

					/*
					 * Schema Book > Audiobook.
					 */
					'schema_book_audio_duration_days'  => 0,	// Audiobook Duration (Days).
					'schema_book_audio_duration_hours' => 0,	// Audiobook Duration (Hours).
					'schema_book_audio_duration_mins'  => 0,	// Audiobook Duration (Mins).
					'schema_book_audio_duration_secs'  => 0,	// Audiobook Duration (Secs).

					/*
					 * Schema Event.
					 */
					'schema_event_lang'                  => $def_lang,						// Event Language.
					'schema_event_attendance'            => $opts[ 'schema_def_event_attendance' ],			// Event Attendance.
					'schema_event_online_url'            => '',							// Event Online URL.
					'schema_event_location_id'           => $opts[ 'schema_def_event_location_id' ],		// Event Venue.
					'schema_event_performer_org_id'      => $opts[ 'schema_def_event_performer_org_id' ],		// Performer Organization.
					'schema_event_performer_person_id'   => $opts[ 'schema_def_event_performer_person_id' ],	// Performer Person.
					'schema_event_organizer_org_id'      => $opts[ 'schema_def_event_organizer_org_id' ],		// Organizer Organization.
					'schema_event_organizer_person_id'   => $opts[ 'schema_def_event_organizer_person_id' ],	// Organizer Person.
					'schema_event_fund_org_id'           => $opts[ 'schema_def_event_fund_org_id' ],		// Funder Organization.
					'schema_event_fund_person_id'        => $opts[ 'schema_def_event_fund_person_id' ],		// Funder Person.
					'schema_event_status'                => 'https://schema.org/EventScheduled',			// Event Status.
					'schema_event_start_date'            => '',							// Event Start Date (Y-m-d).
					'schema_event_start_time'            => 'none',							// Event Start Time (H:i).
					'schema_event_start_timezone'        => $def_timezone,						// Event Start Timezone.
					'schema_event_end_date'              => '',							// Event End Date (Y-m-d).
					'schema_event_end_time'              => 'none',							// Event End Time (H:i).
					'schema_event_end_timezone'          => $def_timezone,						// Event End Timezone.
					'schema_event_previous_date'         => '',							// Event Previous Start Date (Y-m-d).
					'schema_event_previous_time'         => 'none',							// Event Previous Start Time (H:i).
					'schema_event_previous_timezone'     => $def_timezone,						// Event Previous Start Timezone.
					'schema_event_offer_name_0'          => '',
					'schema_event_offer_name_1'          => '',
					'schema_event_offer_name_2'          => '',
					'schema_event_offer_name_3'          => '',
					'schema_event_offer_name_4'          => '',
					'schema_event_offer_price_0'         => '',
					'schema_event_offer_price_1'         => '',
					'schema_event_offer_price_2'         => '',
					'schema_event_offer_price_3'         => '',
					'schema_event_offer_price_4'         => '',
					'schema_event_offer_currency_0'      => $def_currency,
					'schema_event_offer_currency_1'      => $def_currency,
					'schema_event_offer_currency_2'      => $def_currency,
					'schema_event_offer_currency_3'      => $def_currency,
					'schema_event_offer_currency_4'      => $def_currency,
					'schema_event_offer_avail_0'         => 'https://schema.org/InStock',
					'schema_event_offer_avail_1'         => 'https://schema.org/InStock',
					'schema_event_offer_avail_2'         => 'https://schema.org/InStock',
					'schema_event_offer_avail_3'         => 'https://schema.org/InStock',
					'schema_event_offer_avail_4'         => 'https://schema.org/InStock',
					'schema_event_offers_start_date'     => '',							// Event Offers Start Date (Y-m-d).
					'schema_event_offers_start_time'     => 'none',							// Event Offers Start Time (H:i).
					'schema_event_offers_start_timezone' => $def_timezone,						// Event Offers Start Timezone.
					'schema_event_offers_end_date'       => '',							// Event Offers End Date (Y-m-d).
					'schema_event_offers_end_time'       => 'none',							// Event Offers End Time (H:i).
					'schema_event_offers_end_timezone'   => $def_timezone,						// Event Offers End Timezone.

					/*
					 * Schema How-To.
					 */
					'schema_howto_yield'           => '',	// How-To Yield.
					'schema_howto_prep_days'       => 0,	// How-To Preparation Time (Days).
					'schema_howto_prep_hours'      => 0,	// How-To Preparation Time (Hours).
					'schema_howto_prep_mins'       => 0,	// How-To Preparation Time (Mins).
					'schema_howto_prep_secs'       => 0,	// How-To Preparation Time (Secs).
					'schema_howto_total_days'      => 0,	// How-To Total Time (Days).
					'schema_howto_total_hours'     => 0,	// How-To Total Time (Hours).
					'schema_howto_total_mins'      => 0,	// How-To Total Time (Mins).
					'schema_howto_total_secs'      => 0,	// How-To Total Time (Secs).
					'schema_howto_supply_0'        => '',
					'schema_howto_supply_1'        => '',
					'schema_howto_supply_2'        => '',
					'schema_howto_step_section_0'  => 0,	// How-To Step or Section (0 or 1).
					'schema_howto_step_section_1'  => 0,	// How-To Step or Section (0 or 1).
					'schema_howto_step_section_2'  => 0,	// How-To Step or Section (0 or 1).
					'schema_howto_step_0'          => '',
					'schema_howto_step_1'          => '',
					'schema_howto_step_2'          => '',
					'schema_howto_step_text_0'     => '',
					'schema_howto_step_text_1'     => '',
					'schema_howto_step_text_2'     => '',
					'schema_howto_step_text_3'     => '',
					'schema_howto_step_text_4'     => '',
					'schema_howto_step_image_id_0' => '',
					'schema_howto_step_image_id_1' => '',
					'schema_howto_step_image_id_2' => '',
					'schema_howto_step_image_id_3' => '',
					'schema_howto_step_image_id_4' => '',

					/*
					 * Schema JobPosting.
					 */
					'schema_job_title'                => '',					// Job Title.
					'schema_job_hiring_org_id'        => $opts[ 'schema_def_job_hiring_org_id' ],	// Job Hiring Organization.
					'schema_job_location_id'          => $opts[ 'schema_def_job_location_id' ],	// Job Location.
					'schema_job_location_type'        => $opts[ 'schema_def_job_location_type' ],	// Job Location Type.
					'schema_job_salary'               => '',					// Job Base Salary.
					'schema_job_salary_currency'      => $def_currency,				// Job Base Salary Currency.
					'schema_job_salary_period'        => 'year',					// Job Base Salary per Year, Month, Week, Hour.
					'schema_job_empl_type_FULL_TIME'  => 0,						// Job Employment Type.
					'schema_job_empl_type_PART_TIME'  => 0,
					'schema_job_empl_type_CONTRACTOR' => 0,
					'schema_job_empl_type_TEMPORARY'  => 0,
					'schema_job_empl_type_INTERN'     => 0,
					'schema_job_empl_type_VOLUNTEER'  => 0,
					'schema_job_empl_type_PER_DIEM'   => 0,
					'schema_job_empl_type_OTHER'      => 0,
					'schema_job_expire_date'          => '',					// Job Posting Expires Date (Y-m-d).
					'schema_job_expire_time'          => 'none',					// Job Posting Expires Time (H:i).
					'schema_job_expire_timezone'      => $def_timezone,				// Job Posting Expires Timezone.

					/*
					 * Schema CreativeWork > LearningResource.
					 */
					'schema_learnres_educational_level' => 'none',	// Educational Level.
					'schema_learnres_resource_type'     => '',	// Resource Type.

					/*
					 * Schema CreativeWork > Movie.
					 */
					'schema_movie_actor_person_name_0'    => '',
					'schema_movie_actor_person_name_1'    => '',
					'schema_movie_actor_person_name_2'    => '',
					'schema_movie_actor_person_name_3'    => '',
					'schema_movie_actor_person_name_4'    => '',
					'schema_movie_director_person_name_0' => '',
					'schema_movie_director_person_name_1' => '',
					'schema_movie_director_person_name_2' => '',
					'schema_movie_director_person_name_3' => '',
					'schema_movie_director_person_name_4' => '',
					'schema_movie_prodco_org_id'          => 'none',	// Movie Production Company.
					'schema_movie_released_date'          => '',		// Movie Release Date (Y-m-d).
					'schema_movie_released_time'          => 'none',	// Movie Release Time (H:i).
					'schema_movie_released_timezone'      => $def_timezone,	// Movie Release Timezone.
					'schema_movie_duration_days'          => 0,		// Movie Runtime (Days).
					'schema_movie_duration_hours'         => 0,		// Movie Runtime (Hours).
					'schema_movie_duration_mins'          => 0,		// Movie Runtime (Mins).
					'schema_movie_duration_secs'          => 0,		// Movie Runtime (Secs).

					/*
					 * Schema Organization.
					 */
					'schema_organization_id' => 'none',	// Organization.

					/*
					 * Schema Person.
					 */
					'schema_person_id' => 'none',	// Person.

					/*
					 * Schema Place.
					 */
					'schema_place_id' => 'none',	// Place.

					/*
					 * Schema Profile Page.
					 */
					'schema_profile_person_id' => 'none',	// Person.

					/*
					 * Schema QA Page.
					 */
					'schema_qa_desc' => '',		// QA Heading.

					/*
					 * Schema Recipe.
					 */
					'schema_recipe_cuisine'               => '',	// Recipe Cuisine.
					'schema_recipe_course'                => '',	// Recipe Course.
					'schema_recipe_yield'                 => '',	// Recipe Yield.
					'schema_recipe_cook_method'           => '',	// Recipe Cooking Method.
					'schema_recipe_prep_days'             => 0,	// Recipe Preparation Time (Days).
					'schema_recipe_prep_hours'            => 0,	// Recipe Preparation Time (Hours).
					'schema_recipe_prep_mins'             => 0,	// Recipe Preparation Time (Mins).
					'schema_recipe_prep_secs'             => 0,	// Recipe Preparation Time (Secs).
					'schema_recipe_cook_days'             => 0,	// Recipe Cooking Time (Days).
					'schema_recipe_cook_hours'            => 0,	// Recipe Cooking Time (Hours).
					'schema_recipe_cook_mins'             => 0,	// Recipe Cooking Time (Mins).
					'schema_recipe_cook_secs'             => 0,	// Recipe Cooking Time (Secs).
					'schema_recipe_total_days'            => 0,	// Recipe Total Time (Days).
					'schema_recipe_total_hours'           => 0,	// Recipe Total Time (Hours).
					'schema_recipe_total_mins'            => 0,	// Recipe Total Time (Mins).
					'schema_recipe_total_secs'            => 0,	// Recipe Total Time (Secs).
					'schema_recipe_ingredient_0'          => '',
					'schema_recipe_ingredient_1'          => '',
					'schema_recipe_ingredient_2'          => '',
					'schema_recipe_ingredient_3'          => '',
					'schema_recipe_ingredient_4'          => '',
					'schema_recipe_instruction_section_0' => 0,	// Recipe Instruction or Section (0 or 1).
					'schema_recipe_instruction_section_1' => 0,	// Recipe Instruction or Section (0 or 1).
					'schema_recipe_instruction_section_2' => 0,	// Recipe Instruction or Section (0 or 1).
					'schema_recipe_instruction_section_3' => 0,	// Recipe Instruction or Section (0 or 1).
					'schema_recipe_instruction_section_4' => 0,	// Recipe Instruction or Section (0 or 1).
					'schema_recipe_instruction_0'         => '',
					'schema_recipe_instruction_1'         => '',
					'schema_recipe_instruction_2'         => '',
					'schema_recipe_instruction_3'         => '',
					'schema_recipe_instruction_4'         => '',
					'schema_recipe_instruction_text_0'    => '',
					'schema_recipe_instruction_text_1'    => '',
					'schema_recipe_instruction_text_2'    => '',
					'schema_recipe_instruction_text_3'    => '',
					'schema_recipe_instruction_text_4'    => '',
					'schema_recipe_instruction_img_id_0'  => '',
					'schema_recipe_instruction_img_id_1'  => '',
					'schema_recipe_instruction_img_id_2'  => '',
					'schema_recipe_instruction_img_id_3'  => '',
					'schema_recipe_instruction_img_id_4'  => '',
					'schema_recipe_instruction_img_lib_0' => '',
					'schema_recipe_instruction_img_lib_1' => '',
					'schema_recipe_instruction_img_lib_2' => '',
					'schema_recipe_instruction_img_lib_3' => '',
					'schema_recipe_instruction_img_lib_4' => '',

					/*
					 * Schema Recipe - Nutrition Information.
					 */
					'schema_recipe_nutri_serv'      => '',	// Serving Size.
					'schema_recipe_nutri_cal'       => '',	// Calories.
					'schema_recipe_nutri_prot'      => '',	// Protein.
					'schema_recipe_nutri_fib'       => '',	// Fiber.
					'schema_recipe_nutri_carb'      => '',	// Carbohydrates.
					'schema_recipe_nutri_sugar'     => '',	// Sugar.
					'schema_recipe_nutri_sod'       => '',	// Sodium.
					'schema_recipe_nutri_fat'       => '',	// Fat.
					'schema_recipe_nutri_trans_fat' => '',	// Trans Fat.
					'schema_recipe_nutri_sat_fat'   => '',	// Saturated Fat.
					'schema_recipe_nutri_unsat_fat' => '',	// Unsaturated Fat.
					'schema_recipe_nutri_chol'      => '',	// Cholesterol.

					/*
					 * Schema Review.
					 */
					'schema_review_rating'          => '0.0',					// Review Rating.
					'schema_review_rating_min'      => $opts[ 'schema_def_review_rating_min' ],	// Review Rating From.
					'schema_review_rating_max'      => $opts[ 'schema_def_review_rating_max' ],	// Review Rating To.
					'schema_review_rating_alt_name' => '',						// Rating Alt Name.

					/*
					 * Schema Review Subject (aka Item Reviewed).
					 */
					'schema_review_item_name'         => '',	// Subject Name.
					'schema_review_item_desc'         => '',	// Subject Description.
					'schema_review_item_img_id'       => '',
					'schema_review_item_img_url'      => '',
					'schema_review_item_url'          => '',					// Subject Webpage URL.
					'schema_review_item_type'         => $opts[ 'schema_def_review_item_type' ],	// Subject Schema Type.
					'schema_review_item_sameas_url_0' => '',
					'schema_review_item_sameas_url_1' => '',
					'schema_review_item_sameas_url_2' => '',
					'schema_review_item_sameas_url_3' => '',
					'schema_review_item_sameas_url_4' => '',

					/*
					 * Schema Review Subject: Creative Work.
					 */
					'schema_review_item_cw_author_type'      => 'none',		// Subject Author Type.
					'schema_review_item_cw_author_name'      => '',			// Subject Author Name.
					'schema_review_item_cw_author_url'       => '',			// Subject Author URL.
					'schema_review_item_cw_pub_date'         => '',			// Subject Published Date (Y-m-d).
					'schema_review_item_cw_pub_time'         => 'none',		// Subject Published Time (H:i).
					'schema_review_item_cw_pub_timezone'     => $def_timezone,	// Subject Published Timezone.
					'schema_review_item_cw_created_date'     => '',			// Subject Created Date (Y-m-d).
					'schema_review_item_cw_created_time'     => 'none',		// Subject Created Time (H:i).
					'schema_review_item_cw_created_timezone' => $def_timezone,	// Subject Created Timezone.

					/*
					 * Schema Review Subject: Creative Work > Book.
					 */
					'schema_review_item_cw_book_isbn' => '',	// Subject Book ISBN.

					/*
					 * Schema Review Subject: Creative Work > Movie.
					 */
					'schema_review_item_cw_movie_actor_person_name_0'    => '',
					'schema_review_item_cw_movie_actor_person_name_1'    => '',
					'schema_review_item_cw_movie_actor_person_name_2'    => '',
					'schema_review_item_cw_movie_actor_person_name_3'    => '',
					'schema_review_item_cw_movie_actor_person_name_4'    => '',
					'schema_review_item_cw_movie_director_person_name_0' => '',
					'schema_review_item_cw_movie_director_person_name_1' => '',
					'schema_review_item_cw_movie_director_person_name_2' => '',
					'schema_review_item_cw_movie_director_person_name_3' => '',
					'schema_review_item_cw_movie_director_person_name_4' => '',

					/*
					 * Schema Review Subject: Place.
					 */
					'schema_review_item_place_street_address' => '',
					'schema_review_item_place_po_box_number'  => '',
					'schema_review_item_place_city'           => '',
					'schema_review_item_place_region'         => '',
					'schema_review_item_place_postal_code'    => '',
					'schema_review_item_place_country'        => $def_country,

					/*
					 * Schema Review Subject: Place > Local Business.
					 */
					'schema_review_item_place_price_range' => '',

					/*
					 * Schema Review Subject: Place > Food Establishment.
					 */
					'schema_review_item_place_cuisine' => '',

					/*
					 * Schema Review Subject: Product.
					 */
					'schema_review_item_product_brand'            => '',	// Product Brand.
					'schema_review_item_product_offer_name_0'     => '',
					'schema_review_item_product_offer_name_1'     => '',
					'schema_review_item_product_offer_name_2'     => '',
					'schema_review_item_product_offer_name_3'     => '',
					'schema_review_item_product_offer_name_4'     => '',
					'schema_review_item_product_offer_price_0'    => '',
					'schema_review_item_product_offer_price_1'    => '',
					'schema_review_item_product_offer_price_2'    => '',
					'schema_review_item_product_offer_price_3'    => '',
					'schema_review_item_product_offer_price_4'    => '',
					'schema_review_item_product_offer_currency_0' => $def_currency,
					'schema_review_item_product_offer_currency_1' => $def_currency,
					'schema_review_item_product_offer_currency_2' => $def_currency,
					'schema_review_item_product_offer_currency_3' => $def_currency,
					'schema_review_item_product_offer_currency_4' => $def_currency,
					'schema_review_item_product_offer_avail_0'    => 'https://schema.org/InStock',
					'schema_review_item_product_offer_avail_1'    => 'https://schema.org/InStock',
					'schema_review_item_product_offer_avail_2'    => 'https://schema.org/InStock',
					'schema_review_item_product_offer_avail_3'    => 'https://schema.org/InStock',
					'schema_review_item_product_offer_avail_4'    => 'https://schema.org/InStock',
					'schema_review_item_product_retailer_part_no' => '',	// Product SKU.
					'schema_review_item_product_mfr_part_no'      => '',	// Product MPN.

					/*
					 * Schema Review Subject: Software Application.
					 */
					'schema_review_item_software_app_cat'              => '',	// Application Category.
					'schema_review_item_software_app_os'               => '',	// Operating System.
					'schema_review_item_software_app_dl_url'           => '',	// Download URL.
					'schema_review_item_software_app_offer_name_0'     => '',
					'schema_review_item_software_app_offer_name_1'     => '',
					'schema_review_item_software_app_offer_name_2'     => '',
					'schema_review_item_software_app_offer_name_3'     => '',
					'schema_review_item_software_app_offer_name_4'     => '',
					'schema_review_item_software_app_offer_price_0'    => '',
					'schema_review_item_software_app_offer_price_1'    => '',
					'schema_review_item_software_app_offer_price_2'    => '',
					'schema_review_item_software_app_offer_price_3'    => '',
					'schema_review_item_software_app_offer_price_4'    => '',
					'schema_review_item_software_app_offer_currency_0' => $def_currency,
					'schema_review_item_software_app_offer_currency_1' => $def_currency,
					'schema_review_item_software_app_offer_currency_2' => $def_currency,
					'schema_review_item_software_app_offer_currency_3' => $def_currency,
					'schema_review_item_software_app_offer_currency_4' => $def_currency,
					'schema_review_item_software_app_offer_avail_0'    => 'https://schema.org/InStock',
					'schema_review_item_software_app_offer_avail_1'    => 'https://schema.org/InStock',
					'schema_review_item_software_app_offer_avail_2'    => 'https://schema.org/InStock',
					'schema_review_item_software_app_offer_avail_3'    => 'https://schema.org/InStock',
					'schema_review_item_software_app_offer_avail_4'    => 'https://schema.org/InStock',

					/*
					 * Schema Claim Review.
					 */
					'schema_review_claim_reviewed'  => '',	// Short Summary of Claim.
					'schema_review_claim_first_url' => '',	// First Appearance URL.

					/*
					 * Schema Service.
					 */
					'schema_service_prov_org_id'          => $opts[ 'schema_def_service_prov_org_id' ],	// Provider Organization.
					'schema_service_prov_person_id'       => $opts[ 'schema_def_service_prov_person_id' ],	// Provider Person.
					'schema_service_award_0'              => '',						// Service Award.
					'schema_service_award_1'              => '',						// Service Award.
					'schema_service_award_2'              => '',						// Service Award.
					'schema_service_award_3'              => '',						// Service Award.
					'schema_service_award_4'              => '',						// Service Award.
					'schema_service_offer_name_0'         => '',
					'schema_service_offer_name_1'         => '',
					'schema_service_offer_name_2'         => '',
					'schema_service_offer_name_3'         => '',
					'schema_service_offer_name_4'         => '',
					'schema_service_offer_price_0'        => '',
					'schema_service_offer_price_1'        => '',
					'schema_service_offer_price_2'        => '',
					'schema_service_offer_price_3'        => '',
					'schema_service_offer_price_4'        => '',
					'schema_service_offer_currency_0'     => $def_currency,
					'schema_service_offer_currency_1'     => $def_currency,
					'schema_service_offer_currency_2'     => $def_currency,
					'schema_service_offer_currency_3'     => $def_currency,
					'schema_service_offer_currency_4'     => $def_currency,
					'schema_service_offer_avail_0'        => 'https://schema.org/InStock',
					'schema_service_offer_avail_1'        => 'https://schema.org/InStock',
					'schema_service_offer_avail_2'        => 'https://schema.org/InStock',
					'schema_service_offer_avail_3'        => 'https://schema.org/InStock',
					'schema_service_offer_avail_4'        => 'https://schema.org/InStock',
					'schema_service_offer_catalog_0'      => '',						// Offer Catalog Name.
					'schema_service_offer_catalog_1'      => '',						// Offer Catalog Name.
					'schema_service_offer_catalog_2'      => '',						// Offer Catalog Name.
					'schema_service_offer_catalog_3'      => '',						// Offer Catalog Name.
					'schema_service_offer_catalog_4'      => '',						// Offer Catalog Name.
					'schema_service_offer_catalog_text_0' => '',						// Offer Catalog Description.
					'schema_service_offer_catalog_text_1' => '',						// Offer Catalog Description.
					'schema_service_offer_catalog_text_2' => '',						// Offer Catalog Description.
					'schema_service_offer_catalog_text_3' => '',						// Offer Catalog Description.
					'schema_service_offer_catalog_text_4' => '',						// Offer Catalog Description.
					'schema_service_offer_catalog_url_0'  => '',						// Offer Catalog URL.
					'schema_service_offer_catalog_url_1'  => '',						// Offer Catalog URL.
					'schema_service_offer_catalog_url_2'  => '',						// Offer Catalog URL.
					'schema_service_offer_catalog_url_3'  => '',						// Offer Catalog URL.
					'schema_service_offer_catalog_url_4'  => '',						// Offer Catalog URL.
					'schema_service_latitude'             => '',						// Service Latitude.
					'schema_service_longitude'            => '',						// Service Longitude.
					'schema_service_radius'               => '',						// Service Radius.
					'schema_service_area_id_0'            => 'none',					// Service Areas.
					'schema_service_area_id_1'            => 'none',					// Service Areas.
					'schema_service_area_id_2'            => 'none',					// Service Areas.
					'schema_service_area_id_3'            => 'none',					// Service Areas.
					'schema_service_area_id_4'            => 'none',					// Service Areas.

					/*
					 * Schema Software Application.
					 */
					'schema_software_app_cat'    => '',	// Application Category.
					'schema_software_app_os'     => '',	// Operating System.
					'schema_software_app_dl_url' => '',	// Download URL.

					/*
					 * Schema WebPage.
					 */
					'schema_webpage_reviewed_by_org_id_0'    => '',
					'schema_webpage_reviewed_by_org_id_1'    => '',
					'schema_webpage_reviewed_by_org_id_2'    => '',
					'schema_webpage_reviewed_by_org_id_3'    => '',
					'schema_webpage_reviewed_by_org_id_4'    => '',
					'schema_webpage_reviewed_by_person_id_0' => '',
					'schema_webpage_reviewed_by_person_id_1' => '',
					'schema_webpage_reviewed_by_person_id_2' => '',
					'schema_webpage_reviewed_by_person_id_3' => '',
					'schema_webpage_reviewed_by_person_id_4' => '',
					'schema_webpage_reviewed_last_date'      => '',			// Reviewed Last Date (Y-m-d).
					'schema_webpage_reviewed_last_time'      => 'none',		// Reviewed Last Time (H:i).
					'schema_webpage_reviewed_last_timezone'  => $def_timezone,	// Reviewed Last Timezone.
				);

				/*
				 * Set the reviewed by options.
				 */
				$reviewed_by_max = SucomUtil::get_const( 'WPSSO_SCHEMA_WEBPAGE_REVIEWED_BY_MAX' );

				foreach ( range( 0, $reviewed_by_max - 1 ) as $num ) {

					$md_defs[ 'schema_webpage_reviewed_by_org_id_' . $num ]    = 'none';
					$md_defs[ 'schema_webpage_reviewed_by_person_id_' . $num ] = 'none';
				}

				/*
				 * See https://developers.google.com/search/reference/robots_meta_tag.
				 */
				$directives = SucomUtilRobots::get_default_directives();

				foreach ( $directives as $directive_key => $directive_value ) {

					$opt_key = str_replace( '-', '_', 'robots_' . $directive_key );	// Convert dashes to underscores.

					/*
					 * Use a default value from the plugin settings, if one exists.
					 */
					if ( isset( $this->p->options[ $opt_key ] ) ) {

						$md_defs[ $opt_key ] = $this->p->options[ $opt_key ];

					/*
					 * Save as a checkbox:
					 *
					 *	'robots_noarchive'
					 *	'robots_nofollow'
					 *	'robots_noimageindex'
					 *	'robots_noindex'
					 *	'robots_nosnippet'
					 *	'robots_notranslate'
					 */
					} elseif ( 0 === strpos( $directive_key, 'no' ) && is_bool( $directive_value ) ) {

						$md_defs[ $opt_key ] = $directive_value ? 1 : 0;

					/*
					 * Save the directive value:
					 *
					 *	'robots_max_snippet'
					 *	'robots_max_image_preview'
					 *	'robots_max_video_preview'
					 */
					} elseif ( 0 === strpos( $directive_key, 'max' ) ) {

						$md_defs[ $opt_key ] = $directive_value;
					}
				}

				/*
				 * Overwrite the default options with any custom options from the parent.
				 */
				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'inheriting parent metadata options for ' . $mod[ 'name' ] . ' id ' . $mod[ 'id' ] );
				}

				$parent_opts = $this->get_inherited_md_opts( $mod );

				if ( ! empty( $parent_opts ) ) {

					$md_defs = SucomUtil::array_merge_recursive_distinct( $md_defs, $parent_opts );
				}

				/*
				 * The 'import_custom_fields' filter is executed before the 'wpsso_get_md_options' and
				 * 'wpsso_get_post_options' filters, custom field values may get overwritten by these filters.
				 *
				 * The 'import_custom_fields' filter is also executed before the 'wpsso_get_md_defaults' and
				 * 'wpsso_get_post_defaults' filters, so submitted form values that are identical to their defaults
				 * can be removed before saving the options array.
				 *
				 * See WpssoPost->get_options().
				 * See WpssoAbstractWpMeta->get_defaults().
				 * See WpssoUtilCustomFields->filter_import_custom_fields().
				 * See WpssoIntegEcomWooCommerce->add_mt_product().
				 * See WpssoIntegEcomWooAddGtin->filter_wc_variation_alt_options().
				 */
				if ( 'post' === $mod[ 'name' ] ) {

					if ( $this->p->debug->enabled ) {

						$this->p->debug->log( 'applying filters "wpsso_import_custom_fields" for ' . $mod[ 'name' ] . ' id ' . $mod[ 'id' ] );
					}

					$md_defs = apply_filters( 'wpsso_import_custom_fields', $md_defs, $mod, get_metadata( $mod[ 'name' ], $mod[ 'id' ] ) );

					/*
					 * Since WPSSO Core v14.2.0.
					 *
					 * See WpssoIntegEcomWooCommerce->add_mt_product().
					 */
					if ( $this->p->debug->enabled ) {

						$this->p->debug->log( 'applying filters "wpsso_import_product_attributes" for ' . $mod[ 'name' ] . ' id ' . $mod[ 'id' ] );
					}

					$md_defs = apply_filters( 'wpsso_import_product_attributes', $md_defs, $mod, $mod[ 'wp_obj' ] );
				}

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'applying filters "wpsso_get_md_defaults" for ' . $mod[ 'name' ] . ' id ' . $mod[ 'id' ] );
				}

				$md_defs = apply_filters( 'wpsso_get_md_defaults', $md_defs, $mod );

				if ( ! empty( $mod[ 'name' ] ) ) {

					/*
					 * Example $filter_name = 'wpsso_get_post_defaults'.
					 */
					$filter_name = SucomUtil::sanitize_hookname( 'wpsso_get_' . $mod[ 'name' ] . '_defaults' );

					if ( $this->p->debug->enabled ) {

						$this->p->debug->log( 'applying filters "' . $filter_name . '" for ' . $mod[ 'name' ] . ' id ' . $mod[ 'id' ] );
					}

					$md_defs = apply_filters( $filter_name, $md_defs, $mod[ 'id' ], $mod );
				}

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'applying filters "wpsso_sanitize_md_defaults" for ' . $mod[ 'name' ] . ' id ' . $mod[ 'id' ] );
				}

				$md_defs = apply_filters( 'wpsso_sanitize_md_defaults', $md_defs, $mod );

			} elseif ( $this->p->debug->enabled ) {

				$this->p->debug->log( 'skipped filters: opt_filtered is true' );
			}

			if ( false !== $md_key ) {

				if ( isset( $md_defs[ $md_key ] ) ) {

					return $md_defs[ $md_key ];
				}

				return null;
			}

			return $md_defs;
		}

		public function get_options( $obj_id, $md_key = false, $filter_opts = true, $merge_defs = false ) {

			$ret_val = false === $md_key ? array() : null;	// Allow for $md_key = 0.

			return self::must_be_extended( $ret_val );
		}

		/*
		 * See WpssoComment->get_options().
		 * See WpssoPost->get_options().
		 * See WpssoTerm->get_options().
		 * See WpssoUser->get_options().
		 */
		protected function upgrade_options( $md_opts, $obj_id ) {

			if ( $this->p->debug->enabled ) {

				$this->p->debug->log( 'upgrading metadata options' );
			}

			if ( ! is_object( $this->upgrade ) ) {

				require_once WPSSO_PLUGINDIR . 'lib/upgrade-meta.php';

				$this->upgrade = new WpssoUpgradeMeta( $this->p );
			}

			$mod = $this->get_mod( $obj_id );

			$md_opts = $this->upgrade->md_options( $md_opts, $mod );

			return $md_opts;
		}

		/*
		 * Do not pass $md_opts by reference as the options array may get merged with default values.
		 */
		protected function return_options( $obj_id, array $md_opts, $md_key = false, $merge_defs = false ) {

			/*
			 * If a multilingual plugin is available, trust the plugin and ignore previous / inherited custom language values.
			 */
			if ( $this->p->avail[ 'lang' ][ 'any' ] ) {

				unset( $md_opts[ 'schema_lang' ] );
			}

			if ( $merge_defs ) {

				$md_opts = $this->merge_defaults( $obj_id, $md_opts );
			}

			if ( false !== $md_key ) {

				if ( ! isset( $md_opts[ $md_key ] ) || '' === $md_opts[ $md_key ] ) {

					if ( $this->p->debug->enabled ) {

						$this->p->debug->log( 'returning null value: ' . $md_key . ' not set or empty string' );
					}

					return null;
				}

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'returning meta value: ' . $md_key . ' = ' . $md_opts[ $md_key ] );
				}

				return $md_opts[ $md_key ];
			}

			return $md_opts;
		}

		protected function merge_defaults( $obj_id, array $md_opts ) {

			if ( empty( $md_opts[ 'options_merged' ] ) ) {

				$md_defs = $this->get_defaults( $obj_id );	// Uses a local cache.

				if ( is_array( $md_defs ) ) {	// Just in case.

					foreach ( $md_defs as $md_defs_key => $md_defs_val ) {

						if ( ! isset( $md_opts[ $md_defs_key ] ) && '' !== $md_defs_val ) {

							$md_opts[ $md_defs_key ] = $md_defs[ $md_defs_key ];
						}
					}
				}

				$md_opts[ 'options_merged' ] = true;
			}

			return $md_opts;
		}

		/*
		 * Extended by WpssoPost->save_options( $post_id, $rel = false );
		 * Extended by WpssoTerm->save_options( $term_id, $term_tax_id = false );
		 * Extended by WpssoUser->save_options( $user_id, $rel = false );
		 */
		public function save_options( $obj_id, $rel = false ) {

			return self::must_be_extended( $ret_val = false );
		}

		/*
		 * Extended by WpssoPost->delete_options( $post_id, $rel = false );
		 * Extended by WpssoTerm->delete_options( $term_id, $term_tax_id = false );
		 * Extended by WpssoUser->delete_options( $user_id, $rel = false );
		 */
		public function delete_options( $obj_id, $rel = false ) {

			return self::must_be_extended( $ret_val = false );
		}

		/*
		 * Get all publicly accessible post, term, or user IDs.
		 */
		public static function get_public_ids( array $args = array() ) {

			return self::must_be_extended( $ret_val = array() );
		}

		/*
		 * Return an array of post mods for a given $mod object.
		 *
		 * If 'posts_per_page' is not set for an archive page, it will be set to the WordPress settings value.
		 *
		 * See WpssoPage->get_posts_mods().
		 */
		public function get_posts_mods( array $mod ) {

			$posts_mods = array();

			if ( $mod[ 'is_archive' ] ) {

				if ( ! isset( $mod[ 'posts_args' ][ 'posts_per_page' ] ) ) {

					$mod[ 'posts_args' ][ 'posts_per_page' ] = get_option( 'posts_per_page' );
				}
			}

			if ( $this->p->debug->enabled ) {

				if ( isset( $mod[ 'posts_args' ][ 'posts_per_page' ] ) ) {

					$this->p->debug->log( 'using posts_per_page = ' . $mod[ 'posts_args' ][ 'posts_per_page' ] );

				} else $this->p->debug->log( 'posts_per_page not defined' );
			}

			$posts_ids = $this->get_posts_ids( $mod );	// Extended method.

			if ( is_array( $posts_ids ) ) {	// Just in case.

				$have_num = 0;

				foreach ( $posts_ids as $post_id ) {

					$have_num++;

					if ( $this->p->debug->enabled ) {

						$this->p->debug->log( '#' . $have_num . ': getting mod for post id ' . $post_id );
					}

					$posts_mods[] = $this->p->post->get_mod( $post_id );
				}

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'retrieved ' . $have_num . ' post mods' );
				}
			}

			return $posts_mods;
		}

		/*
		 * Returns an array of post IDs for a given $mod object.
		 *
		 * See WpssoAbstractWpMeta->get_posts_mods().
		 */
		public function get_posts_ids( array $mod ) {

			return self::must_be_extended( $ret_val = array() );	// Return an empty array.
		}

		public function ajax_get_metabox_sso() {

			self::must_be_extended();

			die( -1 );	// Nothing to do.
		}

		/*
		 * See WpssoPost->add_meta_boxes().
		 * See WpssoProfileYourSSO->show_metabox_sso().
		 * See WpssoTerm->add_meta_boxes().
		 * See WpssoUser->add_meta_boxes().
		 */
		public function show_metabox_sso( $wp_obj ) {

			echo $this->get_metabox_sso( $wp_obj );
		}

		public function get_metabox_sso( $wp_obj ) {

			$mod        = $this->p->page->get_mod( $use_post = false, $mod = false, $wp_obj );
			$metabox_id = $this->p->cf[ 'meta' ][ 'id' ];
			$tabs       = $this->get_document_sso_tabs( $metabox_id, $mod );
			$md_opts    = $this->get_options( $mod[ 'id' ] );
			$md_defs    = $this->get_defaults( $mod[ 'id' ] );

			$this->form = new SucomForm( $this->p, WPSSO_META_NAME, $md_opts, $md_defs, $this->p->id );

			wp_nonce_field( WpssoAdmin::get_nonce_action(), WPSSO_NONCE_NAME );

			$table_rows = array();

			foreach ( $tabs as $tab_key => $title ) {

				/*
				 * See WpssoAmFiltersEdit->filter_mb_sso_edit_appmeta_rows().
				 * See WpssoBcFiltersEdit->filter_mb_sso_edit_schema_rows().
				 * See WpssoEditGeneral->filter_mb_sso_edit_general_rows().
				 * See WpssoEditMedia->filter_mb_sso_edit_media_rows().
				 * See WpssoEditSchema->filter_mb_sso_edit_schema_rows().
				 * See WpssoEditVisibility->filter_mb_sso_edit_visibility_rows().
				 */
				$filter_name = 'wpsso_mb_' . $metabox_id . '_' . $tab_key . '_rows';

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'applying filters "' . $filter_name . '"' );
				}

				$table_rows[ $tab_key ] = apply_filters( $filter_name, array(), $this->form, self::$head_info, $mod );
			}

			$tabbed_args = array( 'layout' => 'vertical' );	// Force vertical layout.

			$container_id = 'wpsso_mb_' . $metabox_id . '_inside';

			$metabox_html = "\n" . '<div id="' . $container_id . '">';
			$metabox_html .= $this->p->util->metabox->get_tabbed( $metabox_id, $tabs, $table_rows, $tabbed_args );
			$metabox_html .= '<!-- ' . $container_id . '_footer begin -->' . "\n";

			if ( $this->p->debug->enabled ) {

				$this->p->debug->log( 'applying filters "' . $container_id . '"' );
			}

			$metabox_html .= apply_filters( $container_id . '_footer', '', $mod );
			$metabox_html .= '<!-- ' . $container_id . '_footer end -->' . "\n";
			$metabox_html .= $this->get_metabox_javascript( $container_id );
			$metabox_html .= '</div><!-- #'. $container_id . ' -->' . "\n";

			return $metabox_html;
		}

		public function get_metabox_javascript( $container_id ) {

			$doing_ajax   = SucomUtilWP::doing_ajax();
			$container_id = empty( $container_id ) ? '' : '#' . $container_id;
			$metabox_html = '';

			if ( $doing_ajax ) {

				/*
				 * Provide a parent container_id value to initialize a single metabox (when loading a single
				 * metabox via ajax, for example).
				 */
				$metabox_html .= '<!-- init metabox javascript for ajax call -->' . "\n" .
					'<script>wpssoInitMetabox( \'' . $container_id . '\', true );</script>' . "\n";
			}

			return $metabox_html;
		}

		public function load_meta_page( $screen = false ) {

			return self::must_be_extended();
		}

		/*
		 * The post, term, or user has an ID, is public, and (in the case of a post) the post status is published.
		 *
		 * See WpssoPost->load_meta_page().
		 * See WpssoTerm->load_meta_page().
		 * See WpssoUser->load_meta_page().
		 */
		protected function check_head_info( $mod ) {

			if ( ! $mod[ 'id' ] || ! $mod[ 'is_public' ] ) {

				return;

			} elseif ( $mod[ 'is_post' ] && 'publish' !== $mod[ 'post_status' ] ) {

				return;
			}

			$canonical_url = $this->p->util->get_canonical_url( $mod );

			$ref_url = $this->p->util->maybe_set_ref( $canonical_url, $mod, __( 'checking meta tags', 'wpsso' ) );

			foreach ( array( 'image', 'description' ) as $mt_suffix ) {

				if ( empty( self::$head_info[ 'og:' . $mt_suffix ] ) ) {

					if ( $this->p->debug->enabled ) {

						$this->p->debug->log( 'og:' . $mt_suffix . ' meta tag is value empty and required' );
					}

					/*
					 * An is_admin() test is required to make sure the WpssoMessages class is available.
					 */
					if ( $this->p->notice->is_admin_pre_notices() ) {

						$notice_msg = $this->p->msgs->get( 'notice-missing-og-' . $mt_suffix );

						$notice_key = $mod[ 'name' ] . '-' . $mod[ 'id' ] . '-notice-missing-og-' . $mt_suffix;

						if ( ! empty( $notice_msg ) ) {	// Just in case.

							$this->p->notice->err( $notice_msg, null, $notice_key );
						}
					}
				}
			}

			$this->p->util->maybe_unset_ref( $ref_url );

			do_action( 'wpsso_check_head_info', self::$head_info, $mod, $canonical_url );
		}

		/*
		 * Extended by WpssoPost->add_meta_boxes( $post_type, $post_obj = false );
		 * Extended by WpssoTerm->add_meta_boxes( $term_obj, $tax_slug = false );
		 * Extended by WpssoUser->add_meta_boxes( $user_obj, $rel = false );
		 */
		public function add_meta_boxes( $mixed, $rel = false ) {

			return self::must_be_extended();
		}

		/*
		 * Does this page have a Document SSO metabox?
		 *
		 * If this is a post/term/user editing page, then the self::$head_tags variable will be an array.
		 */
		public static function is_meta_page() {

			if ( is_array( self::$head_tags ) ) {	// Array is allowed to be empty.

				return true;
			}

			return false;
		}

		public static function get_head_tags() {

			return self::$head_tags;
		}

		protected function get_document_sso_tabs( $metabox_id, array $mod ) {

			$tabs = array();

			switch ( $metabox_id ) {

				case $this->p->cf[ 'meta' ][ 'id' ]:	// 'sso' metabox ID.

					$tabs[ 'edit_general' ] = _x( 'Edit General', 'metabox tab', 'wpsso' );

					if ( ! $this->p->util->is_schema_disabled() ) {

						$tabs[ 'edit_schema' ] = _x( 'Edit Schema', 'metabox tab', 'wpsso' );
					}

					$tabs[ 'edit_media' ] = _x( 'Edit Media', 'metabox tab', 'wpsso' );

					if ( $mod[ 'is_public' ] ) {

						$tabs[ 'edit_visibility' ] = _x( 'Edit Visibility', 'metabox tab', 'wpsso' );
						$tabs[ 'prev_social' ]     = _x( 'Preview Social', 'metabox tab', 'wpsso' );
						$tabs[ 'prev_oembed' ]     = _x( 'Preview oEmbed', 'metabox tab', 'wpsso' );
						$tabs[ 'validators' ]      = _x( 'Validators', 'metabox tab', 'wpsso' );
					}

					break;
			}

			/*
			 * Exclude the 'Edit Media' tab from attachment editing pages.
			 */
			if ( $mod[ 'is_attachment' ] ) {

				unset( $tabs[ 'edit_media' ] );
			}

			/*
			 * Exclude the 'oEmbed' tab from non-post editing pages.
			 *
			 * get_oembed_response_data() is available since WP v4.4.
			 */
			if ( ! function_exists( 'get_oembed_response_data' ) ||	! $mod[ 'is_post' ] || ! $mod[ 'id' ] ) {

				unset( $tabs[ 'prev_oembed' ] );
			}

			/*
			 * See WpssoAmFiltersEdit->filter_mb_sso_tabs().
			 */
			$filter_name = SucomUtil::sanitize_hookname( 'wpsso_mb_'. $metabox_id . '_tabs', $tabs, $mod );

			if ( $this->p->debug->enabled ) {

				$this->p->debug->log( 'applying filters "' . $filter_name . '"' );
			}

			return apply_filters( $filter_name, $tabs, $mod );
		}

		/*
		 * See WpssoAbstractWpMeta->check_sortable_meta().
		 * See WpssoOembed->post_oembed_response_data().
		 * See WpssoOembed->post_oembed_response_data_rich.
		 * See WpssoOembed->the_embed_thumbnail_url().
		 * See WpssoOembed->the_embed_thumbnail_url_image_shape().
		 * See WpssoOembed->the_embed_thumbnail_id().
		 * See WpssoOembed->the_embed_excerpt().
		 */
		public function get_head_info( $mixed, $read_cache = true ) {

			static $local_fifo = array();

			$mod = is_array( $mixed ) ? $mixed : $this->get_mod( $mixed );	// Uses a local cache.

			unset( $mixed );

			/*
			 * Maybe return the array from the local cache.
			 */
			if ( $read_cache ) {

				if ( isset( $local_fifo[ $mod[ 'id' ] ] ) ) {

					return $local_fifo[ $mod[ 'id' ] ];
				}
			}

			/*
			 * Maybe limit the number of array elements.
			 */
			$local_fifo = SucomUtil::array_slice_fifo( $local_fifo, WPSSO_CACHE_ARRAY_FIFO_MAX );

			$use_post  = 'post' === $mod[ 'name' ] ? $mod[ 'id' ] : false;
			$head_tags = $this->p->head->get_head_array( $use_post, $mod, $read_cache );
			$head_info = $this->p->head->extract_head_info( $head_tags, $mod );

			return $local_fifo[ $mod[ 'id' ] ] = $head_info;
		}

		/*
		 * Called by WpssoHead->extract_head_info().
		 */
		public function get_head_info_thumb_bg_img( $head_info, $mod, $md_pre = 'og', $mt_pre = 'og' ) {

			if ( $this->p->debug->enabled ) {

				$this->p->debug->mark( 'getting thumbnail image' );	// Begin timer.
			}

			$media_html = '';

			if ( empty( $head_info[ $mt_pre . ':image:id' ] ) ) {

				$media_html .= '<!-- checking head info array for image URL -->';

			} else {

				$pid = $head_info[ $mt_pre . ':image:id' ];

				$size_name = 'thumbnail';

				/*
				 * get_mt_single_image_src() returns an og:image:url value, not an og:image:secure_url.
				 *
				 * Example:
				 *
				 * 	array(
				 *		$mt_pre . ':image:url'        => '',
				 *		$mt_pre . ':image:width'      => '',
				 *		$mt_pre . ':image:height'     => '',
				 *		$mt_pre . ':image:cropped'    => '',	// Internal meta tag.
				 *		$mt_pre . ':image:id'         => '',	// Internal meta tag.
				 *		$mt_pre . ':image:alt'        => '',
				 *		$mt_pre . ':image:size_name'  => '',	// Internal meta tag.
				 * 	);
				 */
				$mt_single_image = $this->p->media->get_mt_single_image_src( $pid, $size_name, $mt_pre );

				$media_html .= '<!-- get ' . $size_name . ' for image id ' . $pid . ' = ';

				if ( empty( $mt_single_image[ $mt_pre . ':image:url' ] ) ) {	// Just in case.

					$media_html .= 'failed';

				} else {

					$media_html .= 'success';

					$head_info =& $mt_single_image;	// Use the updated image information.
				}

				$media_html .= ' -->';
			}

			$image_url = SucomUtil::get_first_mt_media_url( $head_info );

			if ( empty( $image_url ) ) {

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'no thumbnail image URL' );
				}

			} else {

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'thumbnail image URL = ' . $image_url );
				}

				$media_html .= '<div class="wp-thumb-bg-img" style="background-image:url(' . $image_url . ');"></div><!-- .wp-thumb-bg-img -->';
			}

			if ( $this->p->debug->enabled ) {

				$this->p->debug->mark( 'getting thumbnail image' );	// End timer.
			}

			return $media_html;
		}

		/*
		 * Return a specific option from the custom social settings meta with fallback for multiple option keys. If $md_key
		 * is an array, then get the first non-empty option from the options array. This is an easy way to provide a
		 * fallback value for the first array key. Use 'none' as a key name to skip this fallback behavior.
		 *
		 * Example: get_options_multi( $obj_id, array( 'seo_desc', 'og_desc' ) );
		 */
		public function get_options_multi( $obj_id, $md_key = false, $filter_opts = true ) {

			if ( $this->p->debug->enabled ) {

				$this->p->debug->log_args( array(
					'obj_id'      => $obj_id,
					'md_key'      => $md_key,
					'filter_opts' => $filter_opts,
				) );
			}

			if ( empty( $obj_id ) ) {

				return null;
			}

			if ( false === $md_key ) {	// Return the whole options array.

				$md_val = $this->get_options( $obj_id, $md_key, $filter_opts );

			} elseif ( true === $md_key ) {	// True is invalid for a custom meta key.

				$md_val = null;

			} else {	// Return the first matching index value.

				if ( is_array( $md_key ) ) {

					$check_md_keys = array_unique( $md_key );	// Prevent duplicate key values.

				} else {

					$check_md_keys = array( $md_key );	// Convert a string to an array.
				}

				foreach ( $check_md_keys as $md_key ) {

					if ( 'none' === $md_key ) {	// Special index keyword - stop here.

						return null;

					} elseif ( empty( $md_key ) ) {	// Skip empty array keys.

						continue;

					} elseif ( is_array( $md_key ) ) {	// An array of arrays is not valid.

						continue;

					} else {

						if ( $this->p->debug->enabled ) {

							$this->p->debug->log( 'getting id ' . $obj_id . ' option ' . $md_key . ' value' );
						}

						if ( ( $md_val = $this->get_options( $obj_id, $md_key, $filter_opts ) ) !== null ) {

							if ( $this->p->debug->enabled ) {

								$this->p->debug->log( 'option ' . $md_key . ' value found (not null)' );
							}

							break;	// Stop after first match.
						}
					}
				}
			}

			if ( null !== $md_val ) {

				if ( $this->p->debug->enabled ) {

					$mod = $this->get_mod( $obj_id );

					$this->p->debug->log( 'custom ' . $mod[ 'name' ] . ' ' . ( false === $md_key ? 'options' :
						( is_array( $md_key ) ? implode( $glue = ', ', $md_key ) : $md_key ) ) . ' = ' .
						( is_array( $md_val ) ? print_r( $md_val, true ) : '"' . $md_val . '"' ) );
				}
			}

			return $md_val;
		}

		/*
		 * Extended by WpssoPost->clear_cache( $post_id, $rel = false );
		 * Extended by WpssoTerm->clear_cache( $term_id, $term_tax_id = false );
		 * Extended by WpssoUser->clear_cache( $user_id, $rel = false );
		 */
		public function clear_cache( $obj_id, $rel = false ) {

			return self::must_be_extended();
		}

		/*
		 * Extended by WpssoPost->refresh_cache( $post_id, $rel = false );
		 * Extended by WpssoTerm->refresh_cache( $term_id, $term_tax_id = false );
		 * Extended by WpssoUser->refresh_cache( $user_id, $rel = false );
		 */
		public function refresh_cache( $obj_id, $rel = false ) {

			return self::must_be_extended();
		}

		/*
		 * Called by WpssoPost->clear_cache().
		 * Called by WpssoTerm->clear_cache().
		 * Called by WpssoUser->clear_cache().
		 */
		protected function clear_mod_cache( array $mod ) {

			/*
			 * Clear the WpssoPage->get_the_content() cache.
			 */
			$this->p->page->clear_the_content( $mod );

			/*
			 * Clear the WpssoHead->get_head_array() cache.
			 */
			$this->p->head->clear_head_array( $mod );

			/*
			 * WordPress stores data using a post, term, or user ID, along with a group string.
			 */
			wp_cache_delete( $mod[ 'id' ], $mod[ 'name' ] . '_meta' );

			do_action( 'wpsso_clear_mod_cache', $mod );
		}

		/*
		 * Check user capability for the comment, post, term, or user id.
		 *
		 * Extended by WpssoComment->user_can_edit( $comment_id, $rel = false );
		 * Extended by WpssoPost->user_can_edit( $post_id, $rel = false );
		 * Extended by WpssoTerm->user_can_edit( $term_id, $term_tax_id = false );
		 * Extended by WpssoUser->user_can_edit( $user_id, $rel = false );
		 */
		public function user_can_edit( $obj_id, $rel = false ) {

			return self::must_be_extended( $ret_val = false );	// Return false by default.
		}

		/*
		 * Called by WpssoComment->user_can_edit();
		 * Called by WpssoPost->user_can_edit();
		 * Called by WpssoTerm->user_can_edit();
		 * Called by WpssoUser->user_can_edit();
		 */
		protected function verify_submit_nonce() {

			if ( empty( $_POST ) ) {	// Nothing to save - nonce not required.

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'submit POST is empty' );
				}

				return false;
			}

			if ( empty( $_POST[ WPSSO_NONCE_NAME ] ) ) {

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'submit POST nonce token missing' );
				}

				return false;
			}

			if ( ! wp_verify_nonce( $_POST[ WPSSO_NONCE_NAME ], WpssoAdmin::get_nonce_action() ) ) {

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'submit nonce token validation failed' );
				}

				if ( is_admin() ) {

					$this->p->notice->err( __( 'Nonce token validation failed for the submitted form (update ignored).', 'wpsso' ) );
				}

				return false;

			}

			return true;
		}

		/*
		 * Merge and check submitted post, term, and user metabox options.
		 *
		 * See WpssoComment->save_options().
		 * See WpssoPost->save_options().
		 * See WpssoTerm->save_options().
		 * See WpssoUser->save_options().
		 */
		protected function get_submit_opts( $mod ) {

			/*
			 * The $mod array argument is preferred but not required.
			 */
			if ( ! is_array( $mod ) ) {

				$mod = $this->get_mod( $mod );
			}

			$md_defs = $this->get_defaults( $mod[ 'id' ] );	// Uses a local cache.
			$md_prev = $this->get_options( $mod[ 'id' ] );	// Uses a local cache.

			/*
			 * Merge and sanitize the new options.
			 *
			 * If no options were changed, then merge and re-sanitize the previous options.
			 */
			if ( empty( $_POST[ WPSSO_META_NAME ] ) ) {

				$md_opts = $md_prev;

			} else {

				$md_opts = $_POST[ WPSSO_META_NAME ];
				$md_opts = SucomUtil::restore_checkboxes( $md_opts );
				$md_opts = SucomUtil::array_merge_recursive_distinct( $md_prev, $md_opts );	// Complete the array with previous options.
			}

			$md_opts = $this->p->opt->sanitize( $md_opts, $md_defs, $network = false, $mod );

			/*
			 * WpssoUtil->is_canonical_disabled() returns true if:
			 *
			 *	- An SEO plugin is active.
			 *	- The 'add_link_rel_canonical' option is unchecked.
			 *	- The 'wpsso_add_link_rel_canonical' filter returns false.
			 *	- The 'wpsso_canonical_disabled' filter returns true.
			 */
			if ( ! empty( $md_opts[ 'canonical_url' ] ) && $this->p->util->is_canonical_disabled() ) {

				unset( $md_opts[ 'canonical_url' ] );
			}

			/*
			 * WpssoUtil->is_redirect_disabled() returns true if:
			 *
			 *	- An SEO plugin is active.
			 *	- The 'wpsso_redirect_disabled' filter returns true.
			 */
			if ( ! empty( $md_opts[ 'redirect_url' ] ) && $this->p->util->is_redirect_disabled() ) {

				unset( $md_opts[ 'redirect_url' ] );
			}

			/*
			 * WpssoUtil->is_canonical_disabled() returns true if:
			 *
			 *	- An SEO plugin is active.
			 *	- The theme does not support the 'title-tag' feature.
			 *	- The WPSSO_TITLE_TAG_DISABLE constant is true.
			 *	- The 'plugin_title_tag' option is not 'seo_title'.
			 */
			if ( ! empty( $md_opts[ 'seo_title' ] ) && $this->p->util->is_seo_title_disabled() ) {

				unset( $md_opts[ 'seo_title' ] );
			}

			/*
			 * WpssoUtil->is_seo_desc_disabled() returns true if:
			 *
			 *	- An SEO plugin is active.
			 *	- The 'add_meta_name_description' option is unchecked.
			 */
			if ( ! empty( $md_opts[ 'seo_desc' ] ) && $this->p->util->is_seo_desc_disabled() ) {

				unset( $md_opts[ 'seo_desc' ] );
			}

			/*
			 * Check image size options (id, prefix, width, height, crop, etc.).
			 */
			foreach ( array( 'og', 'pin', 'schema', 'tc_lrg', 'tc_sum' ) as $md_pre ) {

				/*
				 * If there's no image ID, then remove the image id library prefix.
				 *
				 * If an image id is being used, then remove the image URL (only one can be defined).
				 */
				if ( empty( $md_opts[ $md_pre . '_img_id' ] ) ) {

					unset( $md_opts[ $md_pre . '_img_id_lib' ] );

				} else {

					unset( $md_opts[ $md_pre . '_img_url' ] );
				}

				if ( empty( $md_opts[ $md_pre . '_img_url' ] ) ) {	// Just in case.

					unset( $md_opts[ $md_pre . '_img_url:width' ] );
					unset( $md_opts[ $md_pre . '_img_url:height' ] );
				}
			}

			/*
			 * Remove empty or default values.
			 *
			 * Use strict comparison to manage conversion (don't allow string to integer conversion, for example).
			 */
			foreach ( $md_opts as $md_key => $md_val ) {

				if ( '' === $md_val ) {

					unset( $md_opts[ $md_key ] );

				} elseif ( is_array( $md_val ) ) {	// ie. 'opt_versions' or other array.

					if ( isset( $md_defs[ $md_key ] ) && $md_val === $md_defs[ $md_key ] ) {

						unset( $md_opts[ $md_key ] );
					}

				} elseif ( $md_val === WPSSO_UNDEF || $md_val === (string) WPSSO_UNDEF ) {

					switch ( $md_key ) {

						case 'robots_max_snippet':
						case 'robots_max_video_preview':

							break;

						default:

							unset( $md_opts[ $md_key ] );
					}

				} elseif ( isset( $md_defs[ $md_key ] ) ) {

					if ( $md_val === $md_defs[ $md_key ] || $md_val === (string) $md_defs[ $md_key ] ) {

						unset( $md_opts[ $md_key ] );
					}
				}
			}

			/*
			 * Re-number multi options (example: schema type url, recipe ingredient, recipe instruction, etc.).
			 */
			$this->md_keys_multi_renum( $md_opts );

			/*
			 * Check for default recipe values.
			 */
			foreach ( SucomUtil::preg_grep_keys( '/^schema_recipe_(prep|cook|total)_(days|hours|mins|secs)$/', $md_opts ) as $md_key => $value ) {

				$md_opts[ $md_key ] = (int) $value;

				if ( $md_opts[ $md_key ] === $md_defs[ $md_key ] ) {

					unset( $md_opts[ $md_key ] );
				}
			}

			/*
			 * Check product energy efficiency rating and review rating.
			 */
			foreach ( array( 'product_energy_efficiency', 'schema_review_rating' ) as $md_rating_key ) {

				if ( ! empty( $md_opts[ $md_rating_key ] ) ) {

					/*
					 * Fallback to default values if the min/max is empty.
					 */
					foreach ( array( $md_rating_key . '_min', $md_rating_key . '_max' ) as $md_key ) {

						if ( empty( $md_opts[ $md_key ] ) && isset( $md_defs[ $md_key ] ) ) {

							$md_opts[ $md_key ] = $md_defs[ $md_key ];
						}
					}

				} else {

					foreach ( array( $md_rating_key, $md_rating_key . '_min', $md_rating_key . '_max' ) as $md_key ) {

						unset( $md_opts[ $md_key ] );
					}
				}
			}

			/*
			 * Check and maybe fix missing event date, time, and timezone values.
			 */
			foreach ( array( 'schema_event_start', 'schema_event_end', 'schema_event_previous' ) as $md_pre ) {

				/*
				 * Unset date / time if same as the default value.
				 */
				foreach ( array( 'date', 'time', 'timezone' ) as $md_ext ) {

					if ( isset( $md_opts[ $md_pre . '_' . $md_ext ] ) &&
						( $md_opts[ $md_pre . '_' . $md_ext ] === $md_defs[ $md_pre . '_' . $md_ext ] ||
							$md_opts[ $md_pre . '_' . $md_ext ] === 'none' ) ) {

						unset( $md_opts[ $md_pre . '_' . $md_ext ] );
					}
				}

				if ( empty( $md_opts[ $md_pre . '_date' ] ) && empty( $md_opts[ $md_pre . '_time' ] ) ) {	// No date or time.

					unset( $md_opts[ $md_pre . '_date' ] );
					unset( $md_opts[ $md_pre . '_time' ] );
					unset( $md_opts[ $md_pre . '_timezone' ] );

				} elseif ( ! empty( $md_opts[ $md_pre . '_date' ] ) && empty( $md_opts[ $md_pre . '_time' ] ) ) {	// Date with no time.

					$md_opts[ $md_pre . '_time' ] = '00:00';

				} elseif ( empty( $md_opts[ $md_pre . '_date' ] ) && ! empty( $md_opts[ $md_pre . '_time' ] ) ) {	// Time with no date.

					if ( 'schema_event_previous' === $md_pre ) {

						unset( $md_opts[ $md_pre . '_date' ] );
						unset( $md_opts[ $md_pre . '_time' ] );
						unset( $md_opts[ $md_pre . '_timezone' ] );

					} else $md_opts[ $md_pre . '_date' ] = gmdate( 'Y-m-d' );
				}
			}

			/*
			 * Events with a previous start date must have "rescheduled" as their status.
			 *
			 * A rescheduled event, without a previous start date, is also invalid.
			 */
			if ( ! empty( $md_opts[ 'schema_event_previous_date' ] ) ) {

				$md_opts[ 'schema_event_status' ]          = 'https://schema.org/EventRescheduled';
				$md_opts[ 'schema_event_status:disabled' ] = true;

			} elseif ( isset( $md_opts[ 'schema_event_status' ] ) && 'https://schema.org/EventRescheduled' === $md_opts[ 'schema_event_status' ] ) {

				$md_opts[ 'schema_event_status' ] = 'https://schema.org/EventScheduled';
			}

			/*
			 * Check offer options to make sure they have at least a name or a price.
			 */
			$metadata_offers_max = SucomUtil::get_const( 'WPSSO_SCHEMA_METADATA_OFFERS_MAX', 5 );

			foreach( array( 'schema_event', 'schema_review_item_product', 'schema_review_item_software_app' ) as $md_pre ) {

				foreach ( range( 0, $metadata_offers_max - 1, 1 ) as $key_num ) {

					$is_valid_offer = false;

					foreach ( array(
						$md_pre . '_offer_name',
						$md_pre . '_offer_price'
					) as $md_offer_pre ) {

						if ( isset( $md_opts[ $md_offer_pre . '_' . $key_num ] ) && '' !== $md_opts[ $md_offer_pre . '_' . $key_num ] ) {

							$is_valid_offer = true;
						}
					}

					if ( ! $is_valid_offer ) {

						unset( $md_opts[ $md_pre . '_offer_currency_' . $key_num ] );
						unset( $md_opts[ $md_pre . '_offer_avail_' . $key_num ] );
					}
				}
			}

			/*
			 * Check for invalid Schema type combinations (ie. a claim review of a claim review).
			 */
			if ( isset( $md_opts[ 'schema_type' ] ) ) {	// Just in case.

				if ( 'book.audio' === $md_opts[ 'schema_type' ] ) {

					$md_opts[ 'schema_book_format' ] = 'https://schema.org/AudiobookFormat';

				} elseif ( 'review' === $md_opts[ 'schema_type' ] ) {

					if ( isset( $md_opts[ 'schema_review_item_type' ] ) && 'review' === $md_opts[ 'schema_review_item_type' ] ) {

						$md_opts[ 'schema_review_item_type' ] = $this->p->options[ 'schema_def_review_item_type' ];

						$notice_msg = __( 'Another review cannot be the subject of a review.', 'wpsso' ) . ' ';

						$notice_msg .= __( 'Please select a Schema type for the review subject that describes the subject being reviewed.', 'wpsso' );

						$this->p->notice->err( $notice_msg );
					}

				} elseif ( 'review.claim' === $md_opts[ 'schema_type' ] ) {

					if ( isset( $md_opts[ 'schema_review_item_type' ] ) && 'review.claim' === $md_opts[ 'schema_review_item_type' ] ) {

						$md_opts[ 'schema_review_item_type' ] = $this->p->options[ 'schema_def_review_item_type' ];

						$notice_msg = __( 'Another claim review cannot be the subject of a claim review.', 'wpsso' ) . ' ';

						$notice_msg .= __( 'Please select a Schema type for the review subject that describes the subject being reviewed.', 'wpsso' );

						$this->p->notice->err( $notice_msg );
					}
				}
			}

			/*
			 * Add plugin and add-on versions (ie. 'checksum', 'opt_checksum', and 'opt_versions').
			 */
			$this->p->opt->add_versions_checksum( $md_opts );	// $md_opts must be an array.

			return $md_opts;
		}

		/*
		 * Re-number multi options (example: schema type url, recipe ingredient, recipe instruction, etc.).
		 */
		public function md_keys_multi_renum( array &$md_opts ) {

			$md_keys_multi = WpssoConfig::get_md_keys_multi();

			foreach ( $md_keys_multi as $multi_key => $is_multi ) {

				/*
				 * $is_multi = true | false | array.
				 */
				if ( empty( $is_multi ) ) {	// False.

					continue;
				}

				/*
				 * Get multi option values indexed by their number.
				 */
				$md_multi_opts = SucomUtil::preg_grep_keys( '/^' . $multi_key . '_([0-9]+)$/', $md_opts, $invert = false, $replace = '$1' );
				$md_renum_opts = array();	// Start with a fresh array.
				$renum         = 0;		// Start a new index at 0.

				foreach ( $md_multi_opts as $md_num => $md_val ) {

					if ( '' !== $md_val ) {	// Only save non-empty values.

						$md_renum_opts[ $multi_key . '_' . $renum ] = $md_val;
					}

					/*
					 * Check if there are linked options, and if so, re-number those options as well.
					 */
					if ( is_array( $is_multi ) ) {

						foreach ( $is_multi as $md_linked_key ) {

							if ( isset( $md_opts[ $md_linked_key . '_' . $md_num ] ) ) {	// Just in case.

								$md_renum_opts[ $md_linked_key . '_' . $renum ] = $md_opts[ $md_linked_key . '_' . $md_num ];
							}
						}
					}

					$renum++;	// Increment the new index number.
				}

				/*
				 * Remove any existing multi options, including any linked options.
				 */
				$md_opts = SucomUtil::preg_grep_keys( '/^' . $multi_key . '_([0-9]+)$/', $md_opts, $invert = true );

				if ( is_array( $is_multi ) ) {

					foreach ( $is_multi as $md_linked_key ) {

						$md_opts = SucomUtil::preg_grep_keys( '/^' . $md_linked_key . '_([0-9]+)$/', $md_opts, $invert = true );
					}
				}

				/*
				 * Save the re-numbered options.
				 */
				foreach ( $md_renum_opts as $md_key => $md_val ) {

					$md_opts[ $md_key ] = $md_val;
				}

				unset( $md_renum_opts );
			}
		}

		/*
		 * Return column keys and their column info.
		 */
		public static function get_sortable_columns( $col_key = false ) {

			static $local_cache = null;

			if ( null === $local_cache ) {

				$cf = WpssoConfig::get_config();

				$columns = $cf[ 'edit' ][ 'columns' ];

				$local_cache = apply_filters( 'wpsso_get_sortable_columns', $columns );
			}

			if ( false !== $col_key ) {

				if ( isset( $local_cache[ $col_key ] ) ) {

					return $local_cache[ $col_key ];
				}

				return null;	// Column key not found.
			}

			return $local_cache;	// Always an array.
		}

		public static function get_column_headers() {

			$headers = array();

			$sortable_cols = self::get_sortable_columns();	// Uses a local cache.

			foreach ( $sortable_cols as $col_key => $col_info ) {

				if ( ! empty( $col_info[ 'header' ] ) ) {

					$headers[ $col_key ] = _x( $col_info[ 'header' ], 'column header', 'wpsso' );
				}
			}

			return $headers;
		}

		/*
		 * Example Array (
		 *	[schema_type_name] => _wpsso_head_info_schema_type_name
		 *	[schema_type]      => _wpsso_head_info_schema_type
		 *	[og_type]          => _wpsso_head_info_og_type
		 *	[og_img]           => _wpsso_head_info_og_img_thumb
		 *	[og_desc]          => _wpsso_head_info_og_desc
		 *	[is_noindex]       => _wpsso_head_info_is_noindex
		 *	[is_redirect]      => _wpsso_head_info_is_redirect
		 * )
		*/
		public static function get_column_meta_keys( $col_key = false ) {

			static $local_cache = null;

			if ( null === $local_cache ) {

				$local_cache = array();

				$sortable_cols = self::get_sortable_columns();	// Uses a local cache.

				foreach ( $sortable_cols as $cache_key => $cache_info ) {	// Avoid variable name conflict with args.

					if ( ! empty( $cache_info[ 'meta_key' ] ) ) {

						$local_cache[ $cache_key ] = $cache_info[ 'meta_key' ];
					}
				}
			}

			if ( false !== $col_key ) {

				if ( isset( $local_cache[ $col_key ] ) ) {

					return $local_cache[ $col_key ];
				}

				return null;	// Column key not found.
			}

			return $local_cache;
		}

		/*
		 * See WpssoCmcfXml::get().
		 * See WpssoGmfXml::get().
		 * See https://developer.wordpress.org/reference/classes/wp_meta_query/.
		 */
		static public function get_column_meta_query_og_type( $og_type = 'product', $schema_lang = null ) {

			$wpsso =& Wpsso::get_instance();

			if ( $wpsso->debug->enabled ) {

				$wpsso->debug->mark();
			}

			static $local_cache = array();

			if ( null === $schema_lang ) {

				$schema_lang = SucomUtilWP::get_locale();
			}

			if ( ! isset( $local_cache[ $og_type ] ) ) {

				$local_cache[ $og_type ] = array();
			}

			if ( ! isset( $local_cache[ $og_type ][ $schema_lang ] ) ) {

				$og_type_key      = self::get_column_meta_keys( 'og_type' );		// Example: '_wpsso_head_info_og_type'.
				$noindex_key      = self::get_column_meta_keys( 'is_noindex' );		// Example: '_wpsso_head_info_is_noindex'.
				$redirect_key     = self::get_column_meta_keys( 'is_redirect' );	// Example: '_wpsso_head_info_is_redirect'.
				$schema_lang_key  = self::get_column_meta_keys( 'schema_lang' );	// Example: '_wpsso_head_info_schema_lang'.

				$local_cache[ $og_type ][ $schema_lang ] = array(
					'relation' => 'AND',
					array(
						'key'     => $og_type_key,
						'value'   => $og_type,
						'compare' => '=',
						'type'    => 'CHAR',
					),
					array(
						'key'     => $noindex_key,
						'value'   => '1',
						'compare' => '!=',
						'type'    => 'CHAR',
					),
					array(
						'key'     => $redirect_key,
						'value'   => '1',
						'compare' => '!=',
						'type'    => 'CHAR',
					),
				);

				/*
				 * The WPML integration module returns true so WPML can filter posts for the current language.
				 *
				 * See WpssoIntegLangWpml->__construct().
				 */
				if ( $wpsso->debug->enabled ) {

					$wpsso->debug->log( 'applying filters "wpsso_post_public_ids_suppress_filters"' );
				}

				$suppress_filters = apply_filters( 'wpsso_post_public_ids_suppress_filters', true );

				if ( $suppress_filters ) {

					$local_cache[ $og_type ][ $schema_lang ][] = array(
						'key'     => $schema_lang_key,
						'value'   => $schema_lang,
						'compare' => '=',
						'type'    => 'CHAR',
					);
				}

				if ( $wpsso->debug->enabled ) {

					$wpsso->debug->log( 'applying filters "wpsso_column_meta_query_og_type"' );
				}

				$local_cache[ $og_type ][ $schema_lang ] = apply_filters( 'wpsso_column_meta_query_og_type',
					$local_cache[ $og_type ][ $schema_lang ], $og_type, $schema_lang );
			}

			return $local_cache[ $og_type ][ $schema_lang ];
		}

		/*
		 * See https://developer.wordpress.org/reference/classes/wp_meta_query/.
		 */
		static public function get_column_meta_query_exclude() {

			$wpsso =& Wpsso::get_instance();

			if ( $wpsso->debug->enabled ) {

				$wpsso->debug->mark();
			}

			static $local_cache = null;

			if ( null === $local_cache ) {

				$noindex_key  = self::get_column_meta_keys( 'is_noindex' );	// Example: '_wpsso_head_info_is_noindex'.
				$redirect_key = self::get_column_meta_keys( 'is_redirect' );	// Example: '_wpsso_head_info_is_redirect'.

				$local_cache = array(
					'relation' => 'OR',
					array(
						'key'     => $noindex_key,
						'value'   => '1',
						'compare' => '=',
						'type'    => 'CHAR',
					),
					array(
						'key'     => $redirect_key,
						'value'   => '1',
						'compare' => '=',
						'type'    => 'CHAR',
					),
				);
			}

			return $local_cache;	// Return an empty string or array.
		}

		/*
		 * See WpssoAbstractWpMeta::check_sortable_meta().
		 */
		public static function get_column_info_by_meta_key( $meta_key ) {

			$sortable_cols = self::get_sortable_columns();	// Uses a local cache.

			foreach ( $sortable_cols as $col_key => $col_info ) {

				if ( ! empty( $col_info[ 'meta_key' ] ) ) {

					if ( $col_info[ 'meta_key' ] === $meta_key ) {

						return $col_info;
					}
				}
			}

			return array();
		}

		public function get_column_content( $value, $column_name, $obj_id ) {

			if ( empty( $obj_id ) || 0 !== strpos( $column_name, 'wpsso_' ) ) {	// Just in case.

				return $value;
			}

			$col_key = str_replace( 'wpsso_', '', $column_name );

			if ( $col_info = self::get_sortable_columns( $col_key ) ) {	// Uses a local cache.

				$mod = $this->get_mod( $obj_id );

				$value = $this->get_column_wp_cache( $mod, $col_info );	// Can return 'none' or an empty string.

				/*
				 * Callback added by WpssoRarAdmin->filter_get_sortable_columns().
				 */
				$callbacks_key = $mod[ 'name' ] . '_callbacks';

				if ( isset( $col_info[ $callbacks_key ] ) && is_array( $col_info[ $callbacks_key ] ) ) {

					foreach( $col_info[ $callbacks_key ] as $input_callback ) {

						$value = call_user_func( $input_callback, $value, $obj_id );
					}
				}

				if ( 'none' === $value ) {

					$value = '';
				}
			}

			return $value;
		}

		/*
		 * Can return 'none' or an empty string.
		 *
		 * Called by WpssoPost->get_column_content().
		 * Called by WpssoTerm->get_column_content().
		 * Called by WpssoUser->get_column_content().
		 */
		public function get_column_wp_cache( array $mod, array $col_info ) {

			if ( $this->p->debug->enabled ) {

				$this->p->debug->mark();
			}

			$value  = '';
			$locale = empty( $col_info[ 'localized' ] ) ? '' : SucomUtilWP::get_locale();

			if ( empty( $col_info[ 'meta_key' ] ) ) {	// Just in case.

				return $value;
			}

			/*
			 * Retrieves the cache contents from the cache by key and group.
			 */
			$metadata = $mod[ 'obj' ]->get_update_meta_cache( $mod[ 'id' ] );

			if ( ! empty( $metadata[ $col_info[ 'meta_key' ] ][ 0 ] ) ) {

				$value = maybe_unserialize( $metadata[ $col_info[ 'meta_key' ] ][ 0 ] );	// String or localized array.
			}

			/*
			 * $value is an empty string by default. If get_update_meta_cache() did not return a value for this meta
			 * key, then $value will still be an empty string. If this meta key is localized, and the locale key does
			 * not exist in the array, then fetch a new / updated array value.
			 */
			if ( '' === $value || ( $locale && ! isset( $value[ $locale ] ) ) ) {

				$value = static::get_meta( $mod[ 'id' ], $col_info[ 'meta_key' ], $single = true );	// Use static method from child.
			}

			if ( $locale ) {	// Values stored by locale.

				$value = isset( $value[ $locale ] ) ? $value[ $locale ] : '';
			}

			if ( is_array( $value ) ) {	// Just in case.

				$value = '';
			}

			return $value;
		}

		/*
		 * Filters all post, term, and user metadata.
		 */
		public function check_sortable_meta( $value, $obj_id, $meta_key, $single ) {

			if ( empty( $obj_id ) || 0 !== strpos( $meta_key, '_wpsso_head_info_' ) ) {

				return $value;
			}

			$mod = $this->get_mod( $obj_id );

			/*
			 * Note that the sort order, page number, locale, amp and embed checks are provided by
			 * WpssoHead->get_head_cache_index() and not SucomUtil::get_mod_salt().
			 *
			 * Example cache salts:
			 *
			 * 	'post:123_type:page'
			 *	'post:123_type:product_is_pta'	// WooCommerce shop page.
			 *	'term:123_tax:product_cat'	// WooCommerce category page.
			 */
			$cache_salt = SucomUtil::get_mod_salt( $mod );

			static $local_recursion = array();

			if ( ! empty( $local_recursion[ $cache_salt ][ $meta_key ] ) ) {

				return $value;	// Return null.
			}

			$col_info = self::get_column_info_by_meta_key( $meta_key );

			if ( ! empty( $col_info ) ) {

				$local_recursion[ $cache_salt ][ $meta_key ] = true;	// Prevent recursion.

				$metadata = static::get_meta( $obj_id, $meta_key, $single = true );	// Use static method from child.

				$do_head_info = false;

				if ( '' === $metadata ) {

					$do_head_info = true;

				} elseif ( ! empty( $col_info[ 'localized' ] ) ) {	// Value stored by locale.

					$locale = SucomUtilWP::get_locale();

					if ( empty( $metadata[ $locale ] ) ) {

						$do_head_info = true;
					}
				}

				if ( $do_head_info ) {

					$this->get_head_info( $mod, $read_cache = true );	// Uses a local cache.
				}

				unset( $local_recursion[ $cache_salt ][ $meta_key ] );
			}

			return $value;
		}

		public function update_sortable_meta( $obj_id, $col_key, $content ) {

			if ( empty( $obj_id ) ) {	// Just in case.

				return;
			}

			if ( $col_info = self::get_sortable_columns( $col_key ) ) {	// Uses a local cache.

				if ( ! empty( $col_info[ 'meta_key' ] ) ) {	// Just in case.

					if ( ! empty( $col_info[ 'localized' ] ) ) {	// Value stored by locale.

						$locale   = SucomUtilWP::get_locale();
						$content  = array( $locale => $content );
						$metadata = static::get_meta( $obj_id, $col_info[ 'meta_key' ], $single = true );

						if ( is_array( $metadata ) ) {

							$content = array_merge( $metadata, $content );
						}
					}

					static::update_meta( $obj_id, $col_info[ 'meta_key' ], $content );	// Use static method from child.
				}
			}
		}

		public function add_sortable_columns( $columns ) {

			$sortable_cols = self::get_sortable_columns();	// Uses a local cache.

			foreach ( $sortable_cols as $col_key => $col_info ) {

				if ( ! empty( $col_info[ 'orderby' ] ) ) {

					$columns[ 'wpsso_' . $col_key ] = 'wpsso_' . $col_key;
				}
			}

			return $columns;
		}

		public function set_column_orderby( $query ) {

			$col_name = $query->get( 'orderby' );

			if ( is_string( $col_name ) && strpos( $col_name, 'wpsso_' ) === 0 ) {

				$col_key = str_replace( 'wpsso_', '', $col_name );

				if ( $col_info = self::get_sortable_columns( $col_key ) ) {	// Uses a local cache.

					foreach ( array( 'meta_key', 'orderby' ) as $set_name ) {

						if ( ! empty( $col_info[ $set_name ] ) ) {

							$query->set( $set_name, $col_info[ $set_name ] );
						}
					}
				}
			}
		}

		/*
		 * Called by WpssoPost->add_post_column_headings().
		 * Called by WpssoPost->add_media_column_headings().
		 * Called by WpssoTerm->add_term_column_headings().
		 * Called by WpssoUser->add_user_column_headings().
		 */
		protected function add_column_headings( $columns, $opt_suffix ) {

			foreach ( self::get_column_headers() as $col_key => $col_header ) {

				/*
				 * Check if the column is enabled globally for the post, media, term, or user edit list.
				 */
				if ( ! empty( $this->p->options[ 'plugin_' . $col_key . '_col_' . $opt_suffix ] ) ) {

					if ( $this->p->debug->enabled ) {

						$this->p->debug->log( 'adding wpsso_' . $col_key . ' column' );
					}

					$columns[ 'wpsso_' . $col_key ] = $col_header;
				}
			}

			return $columns;
		}

		/*
		 * Retrieves or updates the metadata cache by key and group.
		 */
		public function get_update_meta_cache( $obj_id ) {

			return self::must_be_extended( $ret_val = array() );	// Return an empty array.
		}

		/*
		 * Return merged custom options from the post or term parents.
		 *
		 * Called by WpssoAbstractWpMeta->get_defaults(), WpssoPost->get_options(), and WpssoTerm->get_options().
		 */
		public function get_inherited_md_opts( array $mod ) {

			if ( $this->p->debug->enabled ) {

				$this->p->debug->mark();
			}

			$md_opts      = array();
			$ancestor_ids = array();

			if ( $mod[ 'is_attachment' ] ) {	// Attachments do not inherit metadata.

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'exiting early: attachments do not inherit metadata' );
				}

				return $md_opts;
			}

			/*
			 * See https://www.php.net/manual/en/function.array-intersect-key.php.
			 */
			$inherit_opts = $this->p->cf[ 'form' ][ 'inherit_md_opts' ];
			$inherit_opts = apply_filters( 'wpsso_inherit_md_opts', $inherit_opts, $mod );

			/*
			 * Since WPSSO Core v9.10.0.
			 *
			 * Note that only public children can inherit custom images from their parents.
			 *
			 * Use add_filter( 'wpsso_inherit_custom_images', '__return_true' ) to inherit images for private children.
			 */
			$inherit_images = empty( $this->p->options[ 'plugin_inherit_images' ] ) ? false : $mod[ 'is_public' ];
			$inherit_images = (bool) apply_filters( 'wpsso_inherit_custom_images', $inherit_images, $mod );

			if ( $inherit_images ) {

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'inherit custom images is enabled' );
				}

			} else {

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'inherit custom images is disabled' );
				}

				$inherit_opts = SucomUtil::preg_grep_keys( '/_img_/', $inherit_opts, $invert = true );
			}

			if ( $mod[ 'is_post' ] ) {

				if ( ! empty( $mod[ 'post_type' ] ) && is_string( $mod[ 'post_type' ] ) ) {	// Not false or empty string.

					if ( $this->p->debug->enabled ) {

						$this->p->debug->log( 'getting ancestors for post type = ' . $mod[ 'post_type' ] );
					}

					$ancestor_ids = get_ancestors( $mod[ 'id' ], $object_type = $mod[ 'post_type' ], $resource_type = 'post_type' );

				} elseif ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'skipped getting ancestors: invalid post type' );
				}

			} elseif ( $mod[ 'is_term' ] ) {

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'getting ancestors for taxonomy = ' . $mod[ 'tax_slug' ] );
				}

				$ancestor_ids = get_ancestors( $mod[ 'id' ], $object_type = $mod[ 'tax_slug' ], $resource_type = 'taxonomy' );
			}

			if ( empty( $ancestor_ids ) ) {

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'no ancestors for ' . $mod[ 'name' ] . ' id ' . $mod[ 'id' ] );
				}

			} elseif ( empty( $inherit_opts ) ) {

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'no inherit opts for ' . $mod[ 'name' ] . ' id ' . $mod[ 'id' ] );
				}

			} else {

				/*
				 * Merge the custom options array top-down, so we merge the closest parent last.
				 */
				$ancestor_ids = array_reverse( $ancestor_ids );

				foreach ( $ancestor_ids as $parent_id ) {

					if ( $this->p->debug->enabled ) {

						$this->p->debug->log( 'getting parent id ' . $parent_id . ' metadata for ' . $mod[ 'name' ] . ' id ' . $mod[ 'id' ] );
					}

					$metadata = $mod[ 'obj' ]->get_update_meta_cache( $parent_id );

					if ( empty( $metadata[ WPSSO_META_NAME ][ 0 ] ) ) {

						if ( $this->p->debug->enabled ) {

							$this->p->debug->log( 'no metadata for parent id ' . $parent_id );
						}

					} else {

						$parent_opts = maybe_unserialize( $metadata[ WPSSO_META_NAME ][ 0 ] );

						if ( $this->p->debug->enabled ) {

							$this->p->debug->log_arr( 'parent_opts', $parent_opts );
						}

						$parent_opts = array_intersect_key( $parent_opts, $inherit_opts );
						$md_opts     = SucomUtil::array_merge_recursive_distinct( $md_opts, $parent_opts );
					}
				}

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log_arr( 'inherit_opts', $inherit_opts );
					$this->p->debug->log_arr( 'md_opts', $md_opts );
				}
			}

			return $md_opts;
		}

		/*
		 * Returns an array of single image associative arrays.
		 *
		 * $size_names can be a keyword (ie. 'opengraph' or 'schema'), a registered size name, or an array of size names.
		 *
		 * $md_pre can be a text string or array of prefixes.
		 */
		public function get_md_images( $num, $size_names, array $mod, $md_pre = 'og', $mt_pre = 'og' ) {

			if ( $this->p->debug->enabled ) {

				$this->p->debug->log_args( array(
					'num'        => $num,
					'size_names' => $size_names,
					'mod'        => $mod,
					'md_pre'     => $md_pre,
					'mt_pre'     => $mt_pre,
				), get_class( $this ) );
			}

			if ( empty( $mod[ 'id' ] ) ) {	// Just in case.

				return array();

			} elseif ( $num < 1 ) {	// Just in case.

				return array();
			}

			$size_names = $this->p->util->get_image_size_names( $size_names );	// Always returns an array.
			$md_pre     = is_array( $md_pre ) ? array_merge( $md_pre, array( 'og' ) ) : array( $md_pre, 'og' );
			$mt_images  = array();

			foreach( array_unique( $md_pre ) as $opt_pre ) {

				if ( $opt_pre === 'none' ) {		// Special index keyword.

					break;

				} elseif ( empty( $opt_pre ) ) {	// Skip empty md_pre values.

					continue;
				}

				$pid = $this->get_options( $mod[ 'id' ], $opt_pre . '_img_id' );
				$lib = $this->get_options( $mod[ 'id' ], $opt_pre . '_img_id_lib' );
				$url = $this->get_options( $mod[ 'id' ], $opt_pre . '_img_url' );

				if ( $pid > 0 ) {

					if ( $this->p->debug->enabled ) {

						$this->p->debug->log( 'using custom ' . $opt_pre . ' image id = ' . $pid, get_class( $this ) );
					}

					$mt_images = $this->p->media->get_mt_pid_images( $pid, $size_names, $mt_pre );
				}

				if ( empty( $mt_images ) && $url ) {

					if ( $this->p->debug->enabled ) {

						$this->p->debug->log( 'using custom ' . $opt_pre . ' image url = ' . $url, get_class( $this ) );
					}

					$img_width  = $this->get_options( $mod[ 'id' ], $opt_pre . '_img_url:width' );
					$img_height = $this->get_options( $mod[ 'id' ], $opt_pre . '_img_url:height' );

					$mt_single_image = SucomUtil::get_mt_image_seed( $mt_pre );

					$mt_single_image[ $mt_pre . ':image:url' ]    = $url;
					$mt_single_image[ $mt_pre . ':image:width' ]  = $img_width > 0 ? $img_width : WPSSO_UNDEF;
					$mt_single_image[ $mt_pre . ':image:height' ] = $img_height > 0 ? $img_height : WPSSO_UNDEF;

					if ( $this->p->util->push_max( $mt_images, $mt_single_image, $num ) ) {

						return $mt_images;
					}
				}

				if ( $pid || $url ) {	// Stop after first $md_pre image found.

					break;
				}
			}

			if ( $this->p->util->is_maxed( $mt_images, $num ) ) {

				return $mt_images;
			}

			/*
			 * Example filter names:
			 *
			 *	'wpsso_post_image_ids'
			 *	'wpsso_term_image_ids'
			 *	'wpsso_user_image_ids'
			 */
			$filter_name = 'wpsso_' . $mod[ 'name' ] . '_image_ids';

			$image_ids = apply_filters( $filter_name, array(), $size_names, $mod[ 'id' ], $mod );

			foreach ( $image_ids as $pid ) {

				if ( $pid > 0 ) {	// Quick sanity check.

					if ( $this->p->debug->enabled ) {

						$this->p->debug->log( 'adding image pid: ' . $pid );
					}

					$mt_pid_images = $this->p->media->get_mt_pid_images( $pid, $size_names, $mt_pre );

					if ( $this->p->util->merge_max( $mt_images, $mt_pid_images, $num ) ) {

						return $mt_images;
					}
				}
			}

			/*
			 * Example filter names:
			 *
			 *	'wpsso_post_image_urls'
			 *	'wpsso_term_image_urls'
			 *	'wpsso_user_image_urls'
			 */
			$filter_name = 'wpsso_' . $mod[ 'name' ] . '_image_urls';

			if ( $this->p->debug->enabled ) {

				$this->p->debug->log( 'applying filters "' . $filter_name . '"' );
			}

			$image_urls = apply_filters( $filter_name, array(), $size_names, $mod[ 'id' ], $mod );

			foreach ( array_unique( $image_urls ) as $num => $url ) {

				if ( false === filter_var( $url, FILTER_VALIDATE_URL ) ) {	// Just in case.

					if ( $this->p->debug->enabled ) {

						$this->p->debug->log( 'skipping image #' . $num . ': url "' . $url . '" is invalid' );
					}

				} else {

					if ( $this->p->util->is_uniq_url( $url, 'image_urls', $mod ) ) {

						if ( $this->p->debug->enabled ) {

							$this->p->debug->log( 'adding image url: ' . $url );
						}

						$mt_single_image = SucomUtil::get_mt_image_seed( $mt_pre );

						$mt_single_image[ $mt_pre . ':image:url' ] = $url;

						$this->p->util->add_image_url_size( $mt_single_image, $mt_pre . ':image' );

						if ( $this->p->util->push_max( $mt_images, $mt_single_image, $num ) ) {

							return $mt_images;
						}
					}
				}
			}

			return $mt_images;
		}

		/*
		 * Extended by the WpssoUser class to support non-WordPress user images.
		 *
		 * Returns an array of single image associative arrays.
		 *
		 * $md_pre can be a text string or array of prefixes.
		 */
		public function get_og_images( $num, $size_names, $obj_id, $md_pre = 'og', $mt_pre = 'og' ) {

			if ( $this->p->debug->enabled ) {

				$this->p->debug->mark();
			}

			$mod = $this->get_mod( $obj_id );

			return $this->get_md_images( $num, $size_names, $mod, $md_pre, $mt_pre );
		}

		/*
		 * Returns an array of single video associative arrays.
		 *
		 * $md_pre can be a text string or array of prefixes.
		 */
		public function get_og_videos( $num, $obj_id, $md_pre = 'og', $mt_pre = 'og' ) {

			if ( $this->p->debug->enabled ) {

				$this->p->debug->log_args( array(
					'num'    => $num,
					'obj_id' => $obj_id,
					'md_pre' => $md_pre,
					'mt_pre' => $mt_pre,
				), get_class( $this ) );
			}

			if ( empty( $obj_id ) ) {	// Just in case.

				return array();

			} elseif ( $num < 1 ) {	// Just in case.

				return array();
			}

			$mod = $this->get_mod( $obj_id );	// Required for get_content_videos().

			$md_pre = is_array( $md_pre ) ? array_merge( $md_pre, array( 'og' ) ) : array( $md_pre, 'og' );

			$mt_videos = array();

			foreach( array_unique( $md_pre ) as $opt_pre ) {

				if ( $opt_pre === 'none' ) {		// Special index keyword.

					break;

				} elseif ( empty( $opt_pre ) ) {	// Skip empty md_pre values.

					continue;
				}

				$embed_html = $this->get_options( $obj_id, $opt_pre . '_vid_embed' );
				$embed_url  = $this->get_options( $obj_id, $opt_pre . '_vid_url' );

				if ( $embed_html ) {

					if ( $this->p->debug->enabled ) {

						$this->p->debug->log( 'fetching video(s) from custom ' . $opt_pre . ' embed code', get_class( $this ) );
					}

					/*
					 * Returns an array of single video associative arrays.
					 */
					$mt_videos = $this->p->media->get_content_videos( $num, $mod, $embed_html );
				}

				if ( empty( $mt_videos ) && $embed_url ) {

					if ( $this->p->util->is_uniq_url( $embed_url, $uniq_context = 'video', $mod ) ) {

						if ( $this->p->debug->enabled ) {

							$this->p->debug->log( 'fetching video from custom ' . $opt_pre . ' url ' . $embed_url, get_class( $this ) );
						}

						$args = array(
							'url'        => $embed_url,
							'stream_url' => '',
							'width'      => null,
							'height'     => null,
							'type'       => '',
							'prev_url'   => '',
							'api'        => '',
						);

						/*
						 * Returns a single video associative array.
						 */
						$mt_single_video = $this->p->media->get_video_details( $args, $mod, $fallback = true );

						if ( ! empty( $mt_single_video ) ) {

							if ( $this->p->util->push_max( $mt_videos, $mt_single_video, $num ) ) {

								if ( $this->p->debug->enabled ) {

									$this->p->debug->log( 'returning ' . count( $mt_videos ) . ' videos' );
								}

								return $mt_videos;
							}
						}
					}
				}

				if ( $embed_html || $embed_url ) {	// Stop after first $md_pre video found.

					break;
				}
			}

			return $mt_videos;
		}

		public function get_mt_reviews( $obj_id, $rating_meta = 'rating', $worst_rating = 1, $best_rating = 5 ) {

			return self::must_be_extended( $ret_val = array() );	// Return an empty array.
		}

		public function get_mt_comment_review( $comment_obj, $rating_meta = 'rating', $worst_rating = 1, $best_rating = 5 ) {

			$mt_ret = array();

			if ( empty( $comment_obj->comment_ID ) ) {	// Just in case.

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'exiting early: comment object id is empty' );
				}

				return $mt_ret;
			}

			$comment_mod = $this->p->comment->get_mod( $comment_obj->comment_ID );

			$mt_comment = $this->p->og->get_array( $comment_mod, $size_names = 'schema', $md_pre = array( 'schema', 'og' ) );

			$mt_ret[ 'review:id' ]           = $comment_mod[ 'id' ];
			$mt_ret[ 'review:url' ]          = isset( $mt_comment[ 'og:url' ] ) ? $mt_comment[ 'og:url' ] : '';
			$mt_ret[ 'review:title' ]        = isset( $mt_comment[ 'og:title' ] ) ? $mt_comment[ 'og:title' ] : '';
			$mt_ret[ 'review:description' ]  = isset( $mt_comment[ 'og:description' ] ) ? $mt_comment[ 'og:description' ] : '';
			$mt_ret[ 'review:text' ]         = $this->p->page->get_text( $comment_mod, $md_key = 'schema_text', $max_len = 'schema_text' );
			$mt_ret[ 'review:created_time' ] = $comment_mod[ 'comment_time' ];
			$mt_ret[ 'review:author:id' ]    = $comment_mod[ 'comment_author' ];
			$mt_ret[ 'review:author:name' ]  = $comment_mod[ 'comment_author_name' ];
			$mt_ret[ 'review:author:url' ]   = $comment_mod[ 'comment_author_url' ];
			$mt_ret[ 'review:image' ]        = isset( $mt_comment[ 'og:image' ] ) ? $mt_comment[ 'og:image' ] : array();
			$mt_ret[ 'review:video' ]        = isset( $mt_comment[ 'og:video' ] ) ? $mt_comment[ 'og:video' ] : array();

			/* foreach ( $mt_ret as $mt_key => $mt_val ) {

				$filter_name = SucomUtil::sanitize_hookname( 'wpsso_mt_comment_review_' . $mt_key );

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'applying filters "' . $filter_name . '"' );
				}

				$mt_ret[ $mt_key ] = apply_filters( $filter_name, $mt_val, $comment_mod );
			} */

			/*
			 * Review rating.
			 *
			 * Rating values must be larger than 0 to include rating info.
			 */
			$rating_value = (float) get_metadata( 'comment', $comment_mod[ 'id' ], $rating_meta, $single = true );

			if ( $rating_value > 0 ) {

				$mt_ret[ 'review:rating:value' ] = $rating_value;
				$mt_ret[ 'review:rating:worst' ] = $worst_rating;
				$mt_ret[ 'review:rating:best' ]  = $best_rating;
			}

			return $mt_ret;
		}

		/*
		 * WpssoPost class specific methods.
		 */
		public function get_canonical_shortlink( $shortlink = false, $obj_id = 0, $context = 'post', $allow_slugs = true ) {

			return self::must_be_extended( $ret_val = '' );
		}

		public function maybe_restore_shortlink( $shortlink = false, $obj_id = 0, $context = 'post', $allow_slugs = true ) {

			return self::must_be_extended( $ret_val = '' );
		}

		/*
		 * Since WPSSO Core v7.6.0.
		 */
		public static function get_attached_ids( $obj_id, $attach_type ) {

			$md_opts = static::get_meta( $obj_id, WPSSO_META_ATTACHED_NAME, $single = true );	// Use static method from child.

			if ( isset( $md_opts[ $attach_type ] ) ) {

				if ( is_array( $md_opts[ $attach_type ] ) ) {	// Just in case.

					return $md_opts[ $attach_type ];
				}
			}

			return array();	// No values.
		}

		/*
		 * Since WPSSO Core v7.6.0.
		 *
		 * Used by WpssoFaqShortcodeQuestion->do_shortcode().
		 */
		public static function add_attached( $obj_id, $attach_type, $attachment_id ) {

			$md_opts = static::get_meta( $obj_id, WPSSO_META_ATTACHED_NAME, $single = true );	// Use static method from child.

			if ( ! isset( $md_opts[ $attach_type ][ $attachment_id ] ) ) {

				if ( ! is_array( $md_opts ) ) {

					$md_opts = array();
				}

				$md_opts[ $attach_type ][ $attachment_id ] = true;

				return static::update_meta( $obj_id, WPSSO_META_ATTACHED_NAME, $md_opts );	// Use static method from child.
			}

			return false;	// No addition.
		}

		/*
		 * Since WPSSO Core v7.6.0.
		 */
		public static function delete_attached( $obj_id, $attach_type, $attachment_id ) {

			$md_opts = static::get_meta( $obj_id, WPSSO_META_ATTACHED_NAME, $single = true );	// Use static method from child.

			if ( isset( $md_opts[ $attach_type ][ $attachment_id ] ) ) {

				unset( $md_opts[ $attach_type ][ $attachment_id ] );

				if ( empty( $md_opts ) ) {	// Cleanup.

					return static::delete_meta( $obj_id, WPSSO_META_ATTACHED_NAME );	// Use static method from child.
				}

				return static::update_meta( $obj_id, WPSSO_META_ATTACHED_NAME, $md_opts );	// Use static method from child.
			}

			return false;	// No delete.
		}

		/*
		 * Returns a custom or default term ID, or false if a term for the $tax_slug is not found.
		 */
		public function get_primary_term_id( array $mod, $tax_slug = 'category' ) {

			return self::must_be_extended( $ret_val = false );	// Return false by default.
		}

		/*
		 * Returns the first taxonomy term ID, , or false if a term for the $tax_slug is not found.
		 */
		public function get_default_term_id( array $mod, $tax_slug = 'category' ) {

			return self::must_be_extended( $ret_val = false );	// Return false by default.
		}

		/*
		 * Returns an associative array of term IDs and their names or objects.
		 *
		 * The primary or default term id will be included as the first array element.
		 */
		public function get_primary_terms( array $mod, $tax_slug = 'category', $output = 'objects' ) {

			return self::must_be_extended( $ret_val = array() );
		}

		/*
		 * Since WPSSO Core v16.4.0.
		 *
		 * Add an element to the metadata options array, if the array key does not already exist.
		 *
		 * After changing the metadata options element, do not forget to refresh the cache for that object ID.
		 */
		public static function add_meta_opts_key( $obj_id, $meta_key, $opts_key, $value ) {

			return self::update_meta_opts_key( $obj_id, $meta_key, $opts_key, $value, $protect = true );
		}

		/*
		 * Always call this method as static::get_meta(), and not self::get_meta(), to execute the method via the child
		 * class instead of the parent class. This method can also be called via $mod[ 'obj' ]::get_meta().
		 *
		 * If $meta_key is en empty string, retrieves all metadata for the specified object ID.
		 *
		 * See https://developer.wordpress.org/reference/functions/get_metadata/.
		 */
		public static function get_meta( $obj_id, $meta_key = '', $single = false ) {

			$ret_val = $single ? '' : array();

			return self::must_be_extended( $ret_val );
		}

		/*
		 * Since WPSSO Core v16.4.0.
		 *
		 * Get an element from the metadata options array. Returns null if the array key does not exist.
		 */
		public static function get_meta_opts_key( $obj_key, $meta_key, $opts_key ) {

			$md_opts = static::get_meta( $obj_id, $meta_key, $single = true );	// Use static method from child.

			if ( array_key_exists( $opts_key, $md_opts ) ) {

				return $md_opts[ $opts_key ];
			}

			return null;	// No value.
		}

		/*
		 * Always call this method as static::update_meta(), and not self::update_meta(), to execute the method via the
		 * child class instead of the parent class. This method can also be called via $mod[ 'obj' ]::update_meta().
		 */
		public static function update_meta( $obj_id, $meta_key, $value ) {

			return self::must_be_extended( $ret_val = false );	// No update.
		}

		/*
		 * Update a metadata options array element, if the array key does not exit, or its value is different.
		 *
		 * This method reads the $meta_key options array from the WordPress metadata table, updates the array element value
		 * (if different), then saves the array back to the WordPress metadata table (if the array has changed). Because of
		 * the overhead required to read and save the array from/to the WordPress metadata table, this method should be
		 * used only for metadata maintenance purposes. Calling this method on every page load is not recommended.
		 *
		 * After changing the metadata options element, do not forget to refresh the cache for that object ID.
		 *
		 * Use $protect = true to prevent overwriting an existing value.
		 *
		 * Example: WpssoPost::update_meta_opts_key( 123, 'schema_review_rating', 1 );
		 */
		public static function update_meta_opts_key( $obj_id, $meta_key, $opts_key, $value, $protect = false ) {

			$md_opts = static::get_meta( $obj_id, $meta_key, $single = true );	// Use static method from child.

			if ( ! is_array( $md_opts ) ) {

				$md_opts = array();	// $meta_key not found.
			}

			if ( array_key_exists( $opts_key, $md_opts ) ) {

				if ( $protect ) {	// Prevent overwriting an existing value.

					return false;	// No update.

				} elseif ( $value === $md_opts[ $opts_key ] ) {	// Nothing to do.

					return false;	// No update.
				}
			}

			$md_opts[ $opts_key ] = $value;

			return static::update_meta( $obj_id, $meta_key, $md_opts );	// Use static method from child.
		}

		/*
		 * Always call this method as staticdelete_meta(), and not selfdelete_meta(), to execute the method via the child
		 * class instead of the parent class. This method can also be called via $mod[ 'obj' ]delete_meta().
		 */
		public static function delete_meta( $obj_id, $meta_key ) {

			return self::must_be_extended( $ret_val = false );	// No delete.
		}

		/*
		 * Since WPSSO Core v16.4.0.
		 *
		 * Delete an element from a metadata options array, if the array key exists.
		 *
		 * After changing the metadata options element, do not forget to refresh the cache for that object ID.
		 */
		public static function delete_meta_opts_key( $obj_id, $meta_key, $opts_key ) {

			$md_opts = static::get_meta( $obj_id, $meta_key, $single = true );	// Use static method from child.

			if ( ! is_array( $md_opts ) ) {	// Nothing to do.

				return false;	// No update.
			}

			if ( ! array_key_exists( $opts_key, $md_opts ) ) {	// Nothing to do.

				return false;	// No update.
			}

			unset( $md_opts[ $opts_key ] );

			if ( empty( $md_opts ) ) {	// Just in case.

				return static::delete_meta( $obj_id, $meta_key );	// Use static method from child.
			}

			return static::update_meta( $obj_id, $meta_key, $md_opts );	// Use static method from child.
		}

		/*
		 * See WpssoIntegSeoRankmath->filter_title_seed().
		 * See WpssoIntegSeoRankmath->filter_description_seed().
		 * See WpssoIntegSeoSeopress->filter_title_seed().
		 * See WpssoIntegSeoSeopress->filter_description_seed().
		 * See WpssoIntegSeoSeopress->filter_get_md_options().
		 * See WpssoIntegSeoWpMetaSeo->filter_title_seed().
		 * See WpssoIntegSeoWpMetaSeo->filter_description_seed().
		 */
		public static function get_mod_meta( array $mod, $meta_key = '', $single = false ) {

			if ( ! empty( $mod[ 'name' ] ) && ! empty( $mod[ 'id' ] ) ) {	// Just in case.

				return get_metadata( $mod[ 'name' ], $mod[ 'id' ], $meta_key, $single );
			}

			return $single ? '' : array();
		}

		protected static function must_be_extended( $method, $ret_val = null ) {

			$wpsso =& Wpsso::get_instance();

			if ( $wpsso->debug->enabled ) {

				$wpsso->debug->log( 'method must be extended', $class_seq = 2, $func_seq = 2 );
			}

			return $ret_val;
		}
	}
}
