<?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.' );
}

if ( ! class_exists( 'WpssoOpenGraph' ) ) {

	class WpssoOpenGraph {

		private $p;	// Wpsso class object.
		private $ns;	// WpssoOpenGraphNS class object.

		public function __construct( &$plugin ) {

			$this->p =& $plugin;

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

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

			/*
			 * Instantiate the WpssoOpenGraphNS class object.
			 */
			if ( ! class_exists( 'WpssoOpenGraphNS' ) ) {

				require_once WPSSO_PLUGINDIR . 'lib/opengraph-ns.php';
			}

			$this->ns = new WpssoOpenGraphNS( $plugin );

			$this->p->util->add_plugin_filters( $this, array(
				'plugin_image_sizes' => 1,
			) );

			$this->p->util->add_plugin_filters( $this, array(
				'get_post_options' => 3,
			), PHP_INT_MAX );
		}

		public function filter_plugin_image_sizes( array $sizes ) {

			$sizes[ 'og' ] = array(		// Option prefix.
				'name'         => 'opengraph',
				'label_transl' => _x( 'Open Graph (Facebook and oEmbed)', 'option label', 'wpsso' ),
			);

			return $sizes;
		}

		public function filter_get_post_options( array $md_opts, $post_id, array $mod ) {

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

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

			if ( is_admin() ) {	// Keep processing on the front-end to a minimum.

				/*
				 * If the Open Graph type isn't already hard-coded (ie. ':disabled'), then using the post type and
				 * the Schema type, check for a possible hard-coded Open Graph type.
				 */
				$md_opts = $this->maybe_update_post_og_type( $md_opts, $post_id, $mod );
			}

			return $md_opts;
		}

		/*
		 * Returns the open graph type id.
		 */
		public function get_mod_og_type_id( array $mod, $use_md_opts = true ) {

			return $this->get_mod_og_type( $mod, $get_id = true, $use_md_opts );
		}

		/*
		 * Returns the open graph namespace.
		 */
		public function get_mod_og_type_ns( array $mod, $use_md_opts = true ) {

			return $this->get_mod_og_type( $mod, $get_id = false, $use_md_opts );
		}

		/*
		 * Returns the open graph type id by default.
		 *
		 * Example: article, product, place, etc.
		 *
		 * Use $get_id = false to return the open graph namespace instead of the ID.
		 */
		public function get_mod_og_type( array $mod, $get_id = true, $use_md_opts = true ) {

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

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

			static $local_cache = array();

			$cache_salt = false;

			/*
			 * Archive pages can call this method several times.
			 *
			 * Optimize and cache post/term/user og type values.
			 */
			if ( ! empty( $mod[ 'obj' ] ) && $mod[ '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().
				 */
				$cache_salt = SucomUtil::get_mod_salt( $mod ) . '_get:' . (string) $get_id . '_use:' . (string) $use_md_opts;

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

					return $local_cache[ $cache_salt ];
				}
			}

			$type_id    = null;
			$og_type_ns = $this->p->cf[ 'head' ][ 'og_type_ns' ];

			/*
			 * Maybe get a custom open graph type id from the post, term, or user meta.
			 */
			if ( $use_md_opts ) {

				if ( ! empty( $mod[ 'obj' ] ) && $mod[ 'id' ] ) {

					$type_id = $mod[ 'obj' ]->get_options( $mod[ 'id' ], 'og_type' );	// Returns null if index key not found.

					if ( empty( $type_id ) || $type_id === 'none' || empty( $og_type_ns[ $type_id ] ) ) {	// Check for an invalid type id.

						$type_id = null;

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

						$this->p->debug->log( 'custom type id = ' . $type_id );
					}
				}
			}

			$is_custom = empty( $type_id ) ? false : true;

			if ( ! $is_custom ) {	// No custom open graph type id from the post, term, or user meta.

				/*
				 * Similar module type logic can be found in the following methods:
				 *
				 * See WpssoOpenGraph->get_mod_og_type().
				 * See WpssoPage->get_description().
				 * See WpssoPage->get_the_title().
				 * See WpssoSchema->get_mod_schema_type().
				 * See WpssoUtil->get_canonical_url().
				 */
				if ( $mod[ 'is_home' ] ) {	// Home page (static or blog archive).

					if ( $mod[ 'is_home_page' ] ) {	// Static front page (singular post).

						$type_id = $this->get_og_type_id( 'home_page' );

					} else $type_id = $this->get_og_type_id( 'home_posts' );

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

					if ( is_numeric( $mod[ 'comment_rating' ] ) ) {

						$type_id = $this->get_og_type_id( 'comment_review' );

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

						$type_id = $this->get_og_type_id( 'comment_reply' );

					} else $type_id = $this->get_og_type_id( 'comment' );

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

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

						if ( $mod[ 'is_post_type_archive' ] ) {	// The post ID may be 0.

							$type_id = $this->get_og_type_id( 'pta_' . $mod[ 'post_type' ] );

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

								$type_id = $this->get_og_type_id( 'archive_page' );
							}

						} else {

							$type_id = $this->get_og_type_id( $mod[ 'post_type' ] );

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

								$type_id = $this->get_og_type_id( 'page' );
							}
						}

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

						$this->p->debug->log( 'no post type' );
					}

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

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

						$type_id = $this->get_og_type_id( 'tax_' . $mod[ 'tax_slug' ] );
					}

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

						$type_id = $this->get_og_type_id( 'archive_page' );
					}

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

					$type_id = $this->get_og_type_id( 'user_page' );

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

					$type_id = $this->get_og_type_id( 'search_page' );

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

					$type_id = $this->get_og_type_id( 'archive_page' );
				}

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

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

						$this->p->debug->log( 'unable to determine og type id (using default)' );
					}

					$type_id = 'website';
				}
			}

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

				$this->p->debug->log( 'og type id before filter = ' . $type_id );
			}

			$type_id = apply_filters( 'wpsso_og_type', $type_id, $mod, $is_custom );

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

				$this->p->debug->log( 'og type id after filter = ' . $type_id );
			}

			$get_value = false;

			if ( empty( $type_id ) ) {

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

					$this->p->debug->log( 'returning false: og type id is empty' );
				}

			} elseif ( 'none' === $type_id ) {

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

					$this->p->debug->log( 'returning false: og type id is disabled' );
				}

			} elseif ( ! isset( $og_type_ns[ $type_id ] ) ) {

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

					$this->p->debug->log( 'returning false: og type id ' . $type_id . ' is unknown' );
				}

			} elseif ( ! $get_id ) {

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

					$this->p->debug->log( 'returning og type id namespace: ' . $og_type_ns[ $type_id ] );
				}

				$get_value = $og_type_ns[ $type_id ];

			} else {

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

					$this->p->debug->log( 'returning og type id: ' . $type_id );
				}

				$get_value = $type_id;
			}

			/*
			 * Optimize and cache post/term/user og type values.
			 */
			if ( $cache_salt ) {

				$local_cache[ $cache_salt ] = $get_value;
			}

			return $get_value;
		}

		/*
		 * $size_names can be a keyword (ie. 'opengraph' or 'schema'), a registered size name, or an array of size names.
		 *
		 * $size_name is passed as-is to WpssoMedia->get_all_images().
		 */
		public function get_array( array $mod, $size_names = 'opengraph', $md_pre = 'og' ) {

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

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

			$max_nums = $this->p->util->get_max_nums( $mod );

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

				$this->p->debug->mark( 'applying filters "wpsso_og_seed"' );	// Begin timer.
			}

			/*
			 * 'wpsso_og_seed' is hooked by e-commerce modules to provide product meta tags.
			 */
			$mt_og = apply_filters( 'wpsso_og_seed', SucomUtil::get_mt_og_seed(), $mod );

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

				$this->p->debug->log_arr( 'og_seed', $mt_og );

				$this->p->debug->mark( 'applying filters "wpsso_og_seed"' );	// End timer.
			}

			/*
			 * Facebook app id meta tag.
			 */
			if ( ! isset( $mt_og[ 'fb:app_id' ] ) ) {

				$mt_og[ 'fb:app_id' ] = $this->p->options[ 'fb_app_id' ];
			}

			/*
			 * Current ISO 8601 date and time (non-standard / internal meta tag).
			 */
			$mt_og[ 'og:time' ] = gmdate( 'c' );

			/*
			 * Type id meta tag.
			 */
			if ( ! isset( $mt_og[ 'og:type' ] ) ) {

				$mt_og[ 'og:type' ] = $this->get_mod_og_type_id( $mod );

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

				$this->p->debug->log( 'og:type already defined = ' . $mt_og[ 'og:type' ] );
			}

			$type_id = $mt_og[ 'og:type' ];

			/*
			 * URL meta tag.
			 */
			if ( ! isset( $mt_og[ 'og:url' ] ) ) {

				$mt_og[ 'og:url' ] = $this->p->util->get_canonical_url( $mod );

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

				$this->p->debug->log( 'og:url already defined = ' . $mt_og[ 'og:url' ] );
			}

			/*
			 * Redirect URL meta tag (non-standard / internal meta tag).
			 */
			$mt_og[ 'og:redirect_url' ] = $this->p->util->get_redirect_url( $mod );

			/*
			 * Locale meta tag.
			 */
			if ( ! isset( $mt_og[ 'og:locale' ] ) ) {

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

					$this->p->debug->log( 'calling get_fb_locale()' );
				}

				$mt_og[ 'og:locale' ] = $this->get_fb_locale( $mod );

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

				$this->p->debug->log( 'og:locale already defined = ' . $mt_og[ 'og:locale' ] );
			}

			/*
			 * Site name meta tag.
			 */
			if ( ! isset( $mt_og[ 'og:site_name' ] ) ) {

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

					$this->p->debug->log( 'getting site name for og:site_name meta tag' );
				}

				$mt_og[ 'og:site_name' ] = SucomUtilWP::get_site_name( $this->p->options, $mod );	// localized

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

				$this->p->debug->log( 'og:site_name already defined = ' . $mt_og[ 'og:site_name' ] );
			}

			/*
			 * Title meta tag.
			 */
			if ( ! isset( $mt_og[ 'og:title' ] ) ) {

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

					$this->p->debug->log( 'getting title for og:title meta tag' );
				}

				$mt_og[ 'og:title' ] = $this->p->page->get_title( $mod, $md_key = 'og_title', $max_len = 'og_title' );

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

					$this->p->debug->log( 'og:title value = ' . $mt_og[ 'og:title' ] );
				}

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

				$this->p->debug->log( 'og:title already defined = ' . $mt_og[ 'og:title' ] );
			}

			/*
			 * Description meta tag.
			 */
			if ( ! isset( $mt_og[ 'og:description' ] ) ) {

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

					$this->p->debug->log( 'getting description for og:description meta tag' );
				}

				$mt_og[ 'og:description' ] = $this->p->page->get_description( $mod, $md_key = 'og_desc', $max_len = 'og_desc', $num_hashtags = true );

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

					$this->p->debug->log( 'og:description value = ' . $mt_og[ 'og:description' ] );
				}

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

				$this->p->debug->log( 'og:description already defined = ' . $mt_og[ 'og:description' ] );
			}

			/*
			 * Updated date / time meta tag.
			 */
			if ( ! isset( $mt_og[ 'og:updated_time' ] ) ) {

				if ( $mod[ 'is_post' ] && $mod[ 'post_modified_time' ] ) {	// ISO 8601 date or false.

					$mt_og[ 'og:updated_time' ] = $mod[ 'post_modified_time' ];
				}
			}

			/*
			 * Get all videos.
			 *
			 * Call before getting all images to find / use preview images.
			 */
			if ( ! isset( $mt_og[ 'og:video' ] ) ) {

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

					$this->p->debug->log( 'getting videos for og:video meta tag' );
				}

				if ( $max_nums[ 'og_vid_max' ] > 0 ) {

					$mt_og[ 'og:video' ] = $this->p->media->get_all_videos( $max_nums[ 'og_vid_max' ], $mod, $md_pre );

					if ( empty( $mt_og[ 'og:video' ] ) ) {

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

							$this->p->debug->log( 'no videos returned' );
						}

						unset( $mt_og[ 'og:video' ] );

					} else {

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

							$this->p->debug->log( 'removing video images to avoid duplicates' );
						}

						foreach ( $mt_og[ 'og:video' ] as $key => $mt_single ) {

							if ( ! is_array( $mt_single ) ) {	// Just in case.

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

									$this->p->debug->log( 'video ignored: $mt_single is not an array' );
								}

								continue;
							}

							$mt_og[ 'og:video' ][ $key ] = SucomUtil::preg_grep_keys( '/^og:image/', $mt_single, $invert = true );
						}
					}

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

					$this->p->debug->log( 'videos disabled: maximum videos is 0 or less' );
				}
			}

			/*
			 * Get all images.
			 */
			if ( ! isset( $mt_og[ 'og:image' ] ) ) {

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

					$this->p->debug->log( 'getting images for og:image meta tag' );
				}

				if ( $max_nums[ 'og_img_max' ] > 0 ) {

					$mt_og[ 'og:image' ] = $this->p->media->get_all_images( $max_nums[ 'og_img_max' ], $size_names, $mod, $md_pre );

					if ( empty( $mt_og[ 'og:image' ] ) ) {

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

							$this->p->debug->log( 'no images returned' );
						}

						unset( $mt_og[ 'og:image' ] );
					}

				} else {

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

						$this->p->debug->log( 'skipped getting images: maximum images is 0 or less' );
					}
				}
			}

			/*
			 * Pre-define some basic open graph meta tags for this og:type. If the meta tag has an associated meta
			 * option name, then read it's value from the meta options.
			 */
			if ( $this->p->debug->enabled ) {

				$this->p->debug->log( 'checking og_type_mt array for known meta tags and md options' );
			}

			/*
			 * An array of Open Graph types, their meta tags, and their associated metadata keys.
			 */
			if ( isset( $this->p->cf[ 'head' ][ 'og_type_mt' ][ $type_id ] ) ) {	// Check if og:type is known.

				/*
				 * Optimize and call get_options() only once. Returns an empty string if no meta found.
				 */
				if ( ! empty( $mod[ 'obj' ] ) && $mod[ 'id' ] ) {

					$md_opts = $mod[ 'obj' ]->get_options( $mod[ 'id' ], $md_key = false, $filter_opts = true, $merge_defs = true );

				} else $md_opts = array();

				/*
				 * Add post/term/user meta data to the Open Graph meta tags.
				 */
				$this->add_data_og_type_md( $mt_og, $type_id, $md_opts );
			}

			/*
			 * If the module is a post object, define the author, publishing date, etc. These values may still be used
			 * by other non-article filters, and if the og:type is not an article, the meta tags will be sanitized (ie.
			 * non-valid meta tags removed) at the end of WpssoHead->get_head_array().
			 */
			if ( ! isset( $mt_og[ 'article:author' ] ) ) {

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

					$this->p->debug->log( 'getting names / URLs for article:author meta tags' );
				}

				$mt_og[ 'article:author' ] = array();

				if ( $mod[ 'is_post' ] ) {

					if ( $mod[ 'post_author' ] ) {

						$mt_og[ 'article:author:name' ] = $this->p->user->get_author_meta( $mod[ 'post_author' ], 'display_name' );
						$mt_og[ 'article:author' ]      = $this->p->user->get_authors_websites( $mod[ 'post_author' ], $meta_key = 'facebook' );
					}

					if ( $mod[ 'post_coauthors' ] ) {

						$og_profile_urls = $this->p->user->get_authors_websites( $mod[ 'post_coauthors' ], $meta_key = 'facebook' );

						$mt_og[ 'article:author' ] = array_merge( $mt_og[ 'article:author' ], $og_profile_urls );
					}

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

					if ( $mod[ 'comment_author' ] ) {

						$mt_og[ 'article:author:name' ] = $this->p->user->get_author_meta( $mod[ 'comment_author' ], 'display_name' );
						$mt_og[ 'article:author' ]      = $this->p->user->get_authors_websites( $mod[ 'comment_author' ], $meta_key = 'facebook' );

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

						$mt_og[ 'article:author:name' ] = $mod[ 'comment_author_name' ];
					}
				}
			}

			if ( ! isset( $mt_og[ 'article:publisher' ] ) ) {

				$mt_og[ 'article:publisher' ] = SucomUtilOptions::get_key_value( 'fb_publisher_url', $this->p->options, $mod );
			}

			if ( ! isset( $mt_og[ 'article:tag' ] ) ) {

				$mt_og[ 'article:tag' ] = $this->p->page->get_tag_names( $mod );
			}

			if ( ! isset( $mt_og[ 'article:published_time' ] ) ) {

				if ( $mod[ 'is_post' ] && $mod[ 'post_time' ] ) {	// ISO 8601 date or false.

					switch ( $mod[ 'post_status' ] ) {

						case 'auto-draft':
						case 'draft':
						case 'future':
						case 'inherit':	// Post revision.
						case 'pending':
						case 'trash':

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

								$this->p->debug->log( 'skipping article published time for post status ' .  $mod[ 'post_status' ] );
							}

							break;

						case 'expired':	// Previously published.
						case 'private':
						case 'publish':
						default:	// Any other post status.

							$mt_og[ 'article:published_time' ] = $mod[ 'post_time' ];

							break;
					}

				} elseif ( $mod[ 'is_comment' ] && $mod[ 'comment_time' ] ) {	// ISO 8601 date or false.

					$mt_og[ 'article:published_time' ] = $mod[ 'comment_time' ];
				}
			}

			if ( ! isset( $mt_og[ 'article:modified_time' ] ) ) {

				if ( $mod[ 'is_post' ] && $mod[ 'post_modified_time' ] ) {	// ISO 8601 date or false.

					$mt_og[ 'article:modified_time' ] = $mod[ 'post_modified_time' ];
				}
			}

			if ( ! isset( $mt_og[ 'article:reading_mins' ] ) ) {

				$mt_og[ 'article:reading_mins' ] = $this->p->page->get_reading_mins( $mod );
			}

			if ( ! empty( $this->p->cf[ 'head' ][ 'og_type_ns' ][ $type_id ] ) ) {

				$og_ns = $this->p->cf[ 'head' ][ 'og_type_ns' ][ $type_id ];	// Example: https://ogp.me/ns/product#

				$filter_name = 'wpsso_og_data_' . SucomUtil::sanitize_hookname( $og_ns );

				$mt_og = apply_filters( $filter_name, $mt_og, $mod );
			}

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

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

			$mt_og = apply_filters( 'wpsso_og', $mt_og, $mod );

			/*
			 * Expand inline variables.
			 */
			$mt_og = $this->p->util->inline->replace_variables( $mt_og, $mod );

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

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

			return $mt_og;
		}

		public function get_og_type_id( $opt_suffix, $default_id = false ) {

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

				$this->p->debug->log_args( array(
					'opt_suffix' => $opt_suffix,
					'default_id' => $default_id,
				) );
			}

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

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

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

				return $default_id;

			} elseif ( ! is_string( $opt_suffix ) ) {	// Must be a string.

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

					$this->p->debug->log( 'exiting early: opt_suffix is ' . gettype( $opt_suffix ) );
				}

				return $default_id;
			}

			$og_type_ns = $this->p->cf[ 'head' ][ 'og_type_ns' ];
			$opt_key    = 'og_type_for_' . $opt_suffix;
			$type_id    = isset( $this->p->options[ $opt_key ] ) ? $this->p->options[ $opt_key ] : $default_id;

			if ( empty( $type_id ) || 'none' === $type_id || empty( $og_type_ns[ $type_id ] ) ) {	// Invalid type id.

				if ( empty( $default_id ) || 'none' === $default_id || empty( $og_type_ns[ $default_id ] ) ) {	// Invalid default id.

					return false;
				}

				return $default_id;
			}

			return $type_id;
		}

		public function get_og_types() {

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

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

			return $this->p->cf[ 'head' ][ 'og_type_ns_compat' ];
		}

		public function get_og_types_select() {

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

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

			$og_type_ns = $this->get_og_types();

			$select = array();

			foreach ( $og_type_ns as $type_id => $type_ns ) {

				$type_url  = preg_replace( '/(^.*\/\/|#$)/', '', $type_ns );
				$type_name = preg_replace( '/(^.*\/ns\/|#$)/U', '', $type_url );

				switch ( $this->p->options[ 'plugin_og_types_select_format' ] ) {

					case 'name':	// Options default.

						$select[ $type_id ] = $type_name;

						break;

					case 'name_id':

						$select[ $type_id ] = $type_name . ' [' . $type_id . ']';

						break;

					case 'id':

						$select[ $type_id ] = $type_id;

						break;

					case 'id_url':

						$select[ $type_id ] = $type_id . ' | ' . $type_url;

						break;

					case 'id_name':

						$select[ $type_id ] = $type_id . ' | ' . $type_name;

						break;

					default:

						$select[ $type_id ] = $type_name;

						break;
				}
			}

			if ( defined( 'SORT_STRING' ) ) {	// Just in case.

				asort( $select, SORT_STRING );

			} else {

				asort( $select );
			}

			return $select;
		}

		/*
		 * Returns an optional and customized locale value for the og:locale meta tag.
		 *
		 * $mixed = 'default' | 'current' | post ID | $mod array
		 *
		 * See WpssoOpenGraph->get_array().
		 * See WpssoOptions->get_defaults().
		 */
		public function get_fb_locale( $mixed = 'current', $use_opts = true ) {

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

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

			/*
			 * Maybe get an alternate Facebook locale from the plugin settings.
			 */
			if ( $use_opts ) {

				$key_locale = SucomUtilOptions::get_key_locale( 'fb_locale', $this->p->options, $mixed );

				if ( ! empty( $this->p->options[ $key_locale ] ) ) {	// Maybe use an alternate locale value.

					$fb_locale = $this->p->options[ $key_locale ];

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

						$this->p->debug->log( 'returning fb locale "' . $fb_locale . '" from ' . $key_locale . ' options key' );
					}

					return $fb_locale;
				}
			}

			/*
			 * Maybe return the Facebook equivalent for this locale.
			 */
			if ( $this->p->debug->enabled ) {

				$this->p->debug->log( 'calling get_schema_lang()' );
			}

			$schema_lang = $this->p->schema->get_schema_lang( $mixed );

			$fb_languages = SucomUtil::get_publisher_languages( 'facebook' );

			/*
			 * Fix known exceptions.
			 */
			switch ( $schema_lang ) {

				case 'en_UK':

					$schema_lang = 'en_GB';

					break;

				case 'de_DE_formal':

					$schema_lang = 'de_DE';

					break;
			}

			if ( ! empty( $fb_languages[ $schema_lang ] ) ) {

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

					$this->p->debug->log( 'returning schema language "' . $schema_lang . '"' );
				}

				return $schema_lang;
			}

			/*
			 * Maybe fallback to the default locale.
			 */
			$def_locale = $this->p->schema->get_schema_lang( 'default' );

			if ( ! empty( $fb_languages[ $def_locale ] ) ) {

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

					$this->p->debug->log( 'returning default locale "' . $def_locale . '"' );
				}

				return $def_locale;
			}

			/*
			 * Fallback to en_US.
			 */
			if ( $this->p->debug->enabled ) {

				$this->p->debug->log( 'returning fallback locale "en_US"' );
			}

			return 'en_US';
		}

		/*
		 * Since WPSSO Core v15.0.0.
		 *
		 * See WpssoCmcfActions->check_product_image_urls().
		 * See WpssoCmcfXml->add_product_images().
		 * See WpssoGmfActions->check_product_image_urls().
		 * See WpssoGmfXml->add_product_images().
		 */
		public function get_product_retailer_item_image_urls( array $mt_single, $size_names = 'opengraph', $md_pre = 'og' ) {

			$image_urls = array();
			$mt_images  = $this->get_product_retailer_item_images( $mt_single, $size_names, $md_pre );

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

				foreach ( $mt_images as $mt_image ) {

					if ( $image_url = SucomUtil::get_first_og_image_url( $mt_image ) ) {

						$image_urls[] = $image_url;
					}
				}
			}

			return $image_urls;
		}

		/*
		 * Since WPSSO Core v15.0.0.
		 *
		 * WpssoOpenGraph->get_product_retailer_item_image_urls().
		 */
		public function get_product_retailer_item_images( array $mt_single, $size_names = 'opengraph', $md_pre = 'og' ) {

			if ( isset( $mt_single[ 'og:image' ] ) && is_array( $mt_single[ 'og:image' ] ) ) {	// Nothing to do.

				return $mt_single[ 'og:image' ];

			} elseif ( ! $mod = $this->get_product_retailer_item_mod( $mt_single ) ) {	// Returns false or array.

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

					$wpsso->debug->log( 'returning early: no post id for retailer item id' );
				}

				return array();
			}

			$max_nums  = $this->p->util->get_max_nums( $mod, 'og' );	// $mod must be an array.

			return $this->p->media->get_all_images( $max_nums[ 'og_img_max' ], $size_names, $mod, $md_pre );
		}

		/*
		 * Since WPSSO Core v15.0.0.
		 *
		 * See WpssoCmcfActions->check_product_image_urls().
		 * See WpssoGmfActions->check_product_image_urls().
		 * See WpssoOpenGraph->get_product_retailer_item_images().
		 * See WpssoSchema::add_offers_aggregate_data_mt().
		 * See WpssoSchema::add_offers_data_mt().
		 * See WpssoSchema::add_variants_data_mt().
		 */
		public function get_product_retailer_item_mod( array $mt_single ) {

			if ( ! empty( $mt_single[ 'product:retailer_item_id' ] ) && is_numeric( $mt_single[ 'product:retailer_item_id' ] ) ) {

				$mod = $this->p->post->get_mod( $mt_single[ 'product:retailer_item_id' ] );

				return empty( $mod[ 'id' ] ) ? false : $mod;	// Just in case.
			}

			return false;
		}

		/*
		 * Returns a string.
		 *
		 * Used for the 'product:retailer_category' meta tag value.
		 */
		public function get_product_retailer_category( $mod ) {

			$retailer_categories = $this->get_product_retailer_categories( $mod, $lists_max = 1 );

			return isset( $retailer_categories[ 0 ] ) ? $retailer_categories[ 0 ] : '';
		}

		/*
		 * Returns an array of strings.
		 */
		public function get_product_retailer_categories( $mod, $lists_max = 1 ) {

			/*
			 * Returns an associative array of term IDs and their names or objects.
			 *
			 * If the custom primary or default term ID exists in the post terms, it will be moved to the top.
			 */
			$post_terms = $this->p->post->get_primary_terms( $mod, $mod[ 'post_primary_tax_slug' ], $output = 'objects' );

			$category_names = array();

			foreach ( $post_terms as $parent_term_obj ) {

				$parent_term_id = $parent_term_obj->term_id;

				$ancestor_ids = get_ancestors( $parent_term_id, $mod[ 'post_primary_tax_slug' ], $resource_type = 'taxonomy' );

				if ( empty( $ancestor_ids ) || ! is_array( $ancestor_ids ) ) {

					$ancestor_ids = array( $parent_term_id );	// Just do the parent term.

				} else {

					$ancestor_ids = array_reverse( $ancestor_ids );	// Add ancestors in reverse order.

					$ancestor_ids[] = $parent_term_id;	// Add parent term last.
				}

				foreach ( $ancestor_ids as $term_id ) {

					$term_mod = $this->p->term->get_mod( $term_id );

					/*
					 * Use $title_sep = false to avoid adding term parent names in the term title.
					 *
					 * $md_key = 'schema_title_bc' will use array( 'schema_title_bc', 'schema_title_alt', 'schema_title', 'seo_title' )
					 */
					$category_names[ $parent_term_id ][ $term_id ] = $this->p->page->get_title( $term_mod,
						$md_key = 'schema_title_bc', $max_len = 'schema_title_bc', $title_sep = false );
				}
			}

			if ( $lists_max ) {

				$category_names = array_slice( $category_names, 0, $lists_max );
			}

			$retailer_categories = array();

			foreach ( $category_names as $parent_term_id => $term_ids ) {

				$retailer_categories[] = implode( ' > ', $term_ids );
			}

			return $retailer_categories;
		}

		/*
		 * Returns an array of strings.
		 */
		public function get_product_awards( array $mod ) {

			$awards = array();

			if ( ! empty( $mod[ 'obj' ] ) && $mod[ 'id' ] ) {

				$md_opts = $mod[ 'obj' ]->get_options( $mod[ 'id' ] );

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

					$awards = SucomUtil::get_multi_values( $md_opts, 'product_award' );
				}
			}

			$awards = apply_filters( 'wpsso_og_product_awards', $awards, $mod );

			return $awards;
		}

		/*
		 * Called by WpssoHead->get_head_array() before merging all meta tag arrays.
		 *
		 * Unset mis-matched og_type meta tags using the 'og_type_mt' array as a reference. For example, remove all
		 * 'article' meta tags if the og_type is 'website'. Removing only known meta tags (using the 'og_type_mt' array as
		 * a reference) protects internal meta tags that may be used later by WpssoHead->extract_head_info().
		 *
		 * The 'og_content_map' array is also checked for Schema values that need to be swapped for simpler Open Graph meta
		 * tag values.
		 */
		public function sanitize_mt_array( array $mt_og ) {

			/*
			 * Array of meta tags to allow, reject, and map.
			 */
			static $og_allow        = array();
			static $og_reject       = array();
			static $content_map     = array();
			static $local_recursion = 0;

			if ( $local_recursion > 32 ) {	// Just in case.

				return $mt_og;
			}

			if ( 0 === $local_recursion ) {	// Define the static variables once.

				/*
				 * The og:type is only needed when first run, to define the allow, reject, and map arrays.
				 */
				if ( empty( $mt_og[ 'og:type' ] ) ) {

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

						$this->p->debug->log( 'exiting early: og:type is empty and required' );
					}

					return $mt_og;
				}

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

					$this->p->debug->log( 'setting og allow, reject, and content map arrays' );
				}

				$og_type = $mt_og[ 'og:type' ];

				/*
				 * An array of Open Graph types, their meta tags, and their associated metadata keys.
				 */
				foreach ( $this->p->cf[ 'head' ][ 'og_type_mt' ] as $type_id => $og_type_mt_md ) {

					/*
					 * An array of meta tags and their associated metadata keys.
					 */
					foreach ( $og_type_mt_md as $mt_name => $md_key ) {

						if ( $og_type === $type_id ) {

							$og_allow[ $mt_name ] = true;	// Mark meta tag as allowed.

							/*
							 * If we have a content map for the meta tag, save the content mapping array.
							 *
							 * 'product:availability' => array(
							 * 	'https://schema.org/BackOrder'           => 'available for order',
							 * 	'https://schema.org/Discontinued'        => 'discontinued',
							 * 	'https://schema.org/InStock'             => 'in stock',
							 * 	'https://schema.org/InStoreOnly'         => 'in stock',
							 * 	'https://schema.org/LimitedAvailability' => 'in stock',
							 * 	'https://schema.org/OnlineOnly'          => 'in stock',
							 * 	'https://schema.org/OutOfStock'          => 'out of stock',
							 * 	'https://schema.org/PreOrder'            => 'available for order',
							 * 	'https://schema.org/PreSale'             => 'available for order',
							 * 	'https://schema.org/SoldOut'             => 'out of stock',
							 * ),
							 */
							if ( ! empty( $this->p->cf[ 'head' ][ 'og_content_map' ][ $mt_name ] ) ) {

								/*
								 * Save the content mapping array.
								 */
								$content_map[ $mt_name ] = $this->p->cf[ 'head' ][ 'og_content_map' ][ $mt_name ];
							}

						} else $og_reject[ $mt_name ] = true;	// Mark meta tag as disallowed.
					}
				}

				foreach ( array( 'product:offers', 'product:variants' ) as $mt_name ) {

					if ( ! empty( $mt_og[ $mt_name ] ) && is_array( $mt_og[ $mt_name ] ) ) {

						foreach ( $mt_og[ $mt_name ] as $num => &$mt_single ) {	// Allow changes to the variation array.

							unset( $mt_single[ 'product:brand' ] );	// Allow only a single brand value (in the parent product).

							foreach ( $mt_single as $mt_single_name => $mt_single_value ) {

								/*
								 * Avoid duplicate values (like prices) between the main product and its variations.
								 */
								if ( isset( $mt_og[ $mt_single_name ] ) && $mt_og[ $mt_single_name ] === $mt_single_value ) {

									/*
									 * Duplicate prices and values are removed first. Check if
									 * the currency or units meta tag needs to be removed as
									 * well (ie. if the price or value does not exist).
									 */
									if ( true === $this->have_currency_units_value( $mt_og, $mt_single_name ) ) {

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

											$this->p->debug->log( 'main product duplicate ' . $mt_single_name . ' kept' );
										}

										continue;
									}

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

										$this->p->debug->log( 'main product duplicate ' . $mt_single_name . ' removed' );
									}

									unset( $mt_og[ $mt_single_name ] );
								}
							}
						}
					}
				}
			}

			/*
			 * Check the meta tag names and their values.
			 */
			foreach ( $mt_og as $key => $val ) {

				if ( ! empty( $og_allow[ $key ] ) ) {	// Meta tag is allowed - check it's value.

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

						$this->p->debug->log( 'allowed meta tag ' . $key );
					}

					/*
					 * If we have a matching value in the content map, then assign the mapped value.
					 *
					 * 'https://schema.org/BackOrder' to 'available for order' for example.
					 */
					if ( isset( $content_map[ $key ][ $val ] ) ) {

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

							$this->p->debug->log( 'mapping meta tag ' . $key . ' = ' . $content_map[ $key ][ $val ] );
						}

						$mt_og[ $key ] = $content_map[ $key ][ $val ];

					/*
					 * If this is a currency or units meta tag without a price or value, then set the meta tag to null.
					 */
					} elseif ( false === $this->have_currency_units_value( $mt_og, $key ) ) {

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

							$this->p->debug->log( 'setting meta tag ' . $key . ' = null' );
						}

						$mt_og[ $key ] = null;
					}

				} elseif ( ! empty( $og_reject[ $key ] ) ) {

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

						$this->p->debug->log( 'rejected meta tag ' . $key );
					}

					unset( $mt_og[ $key ] );

				} elseif ( is_array( $val ) ) {

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

						if ( is_string( $key ) ) {

							$this->p->debug->log( 'sanitizing meta tag ' . $key . ' array' );
						}
					}

					$local_recursion++;

					$mt_og[ $key ] = $this->sanitize_mt_array( $val );

					$local_recursion--;
				}
			}

			return $mt_og;
		}

		/*
		 * Add post/term/user meta data to the Open Graph meta tags.
		 */
		public function add_data_og_type_md( array &$mt_og, $type_id, array $md_opts ) {	// Pass by reference is OK.

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

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

			/*
			 * An array of Open Graph types, their meta tags, and their associated metadata keys.
			 */
			if ( empty( $this->p->cf[ 'head' ][ 'og_type_mt' ][ $type_id ] ) ) {	// Just in case.

				return;
			}

			$og_type_mt_md = $this->p->cf[ 'head' ][ 'og_type_mt' ][ $type_id ];

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

				$this->p->debug->log( 'loading og_type_mt array for type id ' . $type_id );
			}

			$md_keys_multi = WpssoConfig::get_md_keys_multi();	// Uses a local cache.

			foreach ( $og_type_mt_md as $mt_name => $md_key_pre ) {

				if ( empty( $md_key_pre ) ) {	// Most of the metadata keys are empty.

					continue;

				} elseif ( false !== strpos( $mt_name, ':units' ) ) {	// Define units when we define the value.

					continue;
				}

				$is_multi   = empty( $md_keys_multi[ $md_key_pre ] ) ? false : true;
				$multi_keys = $is_multi ? array_keys( SucomUtil::preg_grep_keys( '/^' . $md_key_pre . '_[0-9]+$/', $md_opts ) ) : array( $md_key_pre );

				foreach ( $multi_keys as $md_key ) {

					$values = array();

					/*
					 * Use custom value(s) if available. Returns true or false.
					 */
					if ( ! $this->have_og_type_md_values( $values, $type_id, $md_opts, $mt_name, $md_key ) ) {

						$def_md_key = $this->get_def_md_key( $md_key );

						if ( isset( $mt_og[ $mt_name ] ) ) {	// Meta tag exists and is not null.

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

								$this->p->debug->log( $mt_name . ' value kept = ' . $mt_og[ $mt_name ] );
							}

						} elseif ( ! empty( $def_md_key ) ) {	// Add default value from the plugin settings.

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

								$this->p->debug->log( $mt_name . ' from options = ' . $this->p->options[ $def_md_key ] );
							}

							$values[ $mt_name ] = $this->p->options[ $def_md_key ];

						} else {	// Add missing meta tag with null value.

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

								$this->p->debug->log( $mt_name . ' = null' );
							}

							$values[ $mt_name ] = null;	// Use null so isset() returns false.
						}
					}

					foreach ( $values as $mt_name => $val ) {	// Can be an empty array.

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

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

								$this->p->debug->log( 'setting ' . $mt_name . ' = null for value none' );
							}

							$mt_og[ $mt_name ] = null;

						} elseif ( $is_multi ) {

							$mt_og[ $mt_name ][] = $val;

						} else $mt_og[ $mt_name ] = $val;
					}
				}
			}
		}

		/*
		 * Check for a default value from the settings.
		 *
		 * Open Graph defaults have an 'og_def_' prefix, except for 'product_currency'.
		 *
		 * Schema defaults have a 'schema_def_' prefix.
		 */
		public function get_def_md_key( $md_key ) {

			$def_md_key = null;

			if ( 'product_currency' === $md_key ) {

				if ( isset( $this->p->options[ 'og_def_currency' ] ) ) {	// Just in case.

					$def_md_key = 'og_def_currency';
				}

			} elseif ( isset( $this->p->options[ 'og_def_' . $md_key ] ) ) {

				$def_md_key = 'og_def_' . $md_key;

			} elseif ( isset( $this->p->options[ 'schema_def_' . $md_key ] ) ) {

				$def_md_key = 'schema_def_' . $md_key;
			}

			return $def_md_key;
		}

		/*
		 * If we have a GTIN number, try to improve the assigned property name.
		 *
		 * Pass $mt_og by reference to modify the array directly.
		 *
		 * A similar method exists as WpssoSchema::check_prop_value_gtin().
		 */
		public static function check_mt_value_gtin( &$mt_og, $mt_pre = 'product' ) {	// Pass by reference is OK.

			$wpsso =& Wpsso::get_instance();

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

				$wpsso->debug->log( 'checking ' . $mt_pre . ' gtin value' );
			}

			if ( isset( $mt_og[ $mt_pre . ':gtin' ] ) ) {

				/*
				 * The value may come from a custom field, so trim it, just in case.
				 */
				$mt_og[ $mt_pre . ':gtin' ] = preg_replace( '/\s+/', '', $mt_og[ $mt_pre . ':gtin' ] );

				$gtin_len = strlen( $mt_og[ $mt_pre . ':gtin' ] );

				switch ( $gtin_len ) {

					case 13:

						if ( empty( $mt_og[ $mt_pre . ':ean' ] ) ) {

							$mt_og[ $mt_pre . ':ean' ] = $mt_og[ $mt_pre . ':gtin' ];
						}

						break;

					case 12:

						if ( empty( $mt_og[ $mt_pre . ':upc' ] ) ) {

							$mt_og[ $mt_pre . ':upc' ] = $mt_og[ $mt_pre . ':gtin' ];
						}

						break;
				}
			}
		}

		public static function check_mt_value_price( &$mt_og, $mt_pre = 'product' ) {	// Pass by reference is OK.

			$wpsso =& Wpsso::get_instance();

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

				$wpsso->debug->log( 'checking ' . $mt_pre . ' price value' );
			}

			$def_currency_key  = $mt_pre . ':price:currency';
			$def_currency      = empty( $mt_og[ $def_currency_key ] ) ? $wpsso->options[ 'og_def_currency' ] : $mt_og[ $def_currency_key ];
			$min_adv_price_key = $mt_pre . ':min_advert_price:amount';
			$min_adv_price     = empty( $mt_og[ $min_adv_price_key ] ) ? 0 : $mt_og[ $min_adv_price_key ];

			foreach ( array( 'min_advert_price', 'original_price', 'pretax_price', 'price', 'sale_price', 'shipping_cost' ) as $price_name ) {

				$amount_key   = $mt_pre . ':' . $price_name . ':amount';
				$currency_key = $mt_pre . ':' . $price_name . ':currency';

				if ( isset( $mt_og[ $amount_key ] ) ) {

					if ( is_numeric( $mt_og[ $amount_key ] ) ) {	// Allow for price of 0.

						if ( $min_adv_price && $mt_og[ $amount_key ] < $min_adv_price ) {

							$mt_og[ $mt_pre . ':price:type' ] = 'https://schema.org/MinimumAdvertisedPrice';

							$mt_og[ $amount_key ] = $min_adv_price;
						}

						if ( empty( $mt_og[ $currency_key ] ) ) {

							$mt_og[ $currency_key ] = $def_currency;
						}

					} else {

						if ( ! empty( $mt_og[ $amount_key ] ) ) {	// Non-empty string, array, etc.

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

								$wpsso->debug->log( 'invalid ' . $amount_key . ' value = ' . print_r( $mt_og[ $amount_key ], true ) );
							}
						}

						$mt_og[ $amount_key ]   = null;
						$mt_og[ $currency_key ] = null;
					}
				}
			}
		}

		public static function check_mt_value_energy_efficiency( &$mt_og, $mt_pre = 'product' ) {	// Pass by reference is OK.

			$wpsso =& Wpsso::get_instance();

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

				$wpsso->debug->log( 'checking ' . $mt_pre . ' energy efficiency value' );
			}

			if ( empty( $mt_og[ $mt_pre . ':energy_efficiency:value' ] ) || 'none' === $mt_og[ $mt_pre . ':energy_efficiency:value' ] ) {

				$mt_og[ $mt_pre . ':energy_efficiency:value' ]     = null;
				$mt_og[ $mt_pre . ':energy_efficiency:min_value' ] = null;
				$mt_og[ $mt_pre . ':energy_efficiency:max_value' ] = null;
			}
		}

		private function have_currency_units_value( $mt_og, $mt_key ) {

			if ( preg_match( '/^(.*:)(currency|units)$/', $mt_key, $matches ) ) {

				switch ( $matches[ 2 ] ) {

					case 'currency': $mt_value_key = $matches[ 1 ] . 'amount'; break;
					case 'units':    $mt_value_key = $matches[ 1 ] . 'value';  break;
					default:         $mt_value_key = null; break;
				}

				if ( $mt_value_key ) {

					return isset( $mt_og[ $mt_value_key ] ) && '' !== $mt_og[ $mt_value_key ] ? true : false;
				}			
			}

			return null;
		}

		/*
		 * Use custom value(s) if available. Returns true or false.
		 */
		private function have_og_type_md_values( array &$values, $type_id, array $md_opts, $mt_name, $md_key ) {	// Pass by reference is OK.

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

				return false;
			}

			/*
			 * Check for meta tags that require a unit value and may need to be converted.
			 */
			if ( false !== strpos( $mt_name, ':value' ) && preg_match( '/^(.*):value$/', $mt_name, $mt_match ) ) {

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

					$this->p->debug->log( $mt_name . ' from metadata = ' . $md_opts[ $md_key ] );
				}

				$values[ $mt_name ] = $md_opts[ $md_key ];

				/*
				 * Check if the value meta tag needs a units meta tag.
				 */
				$mt_units_name = $mt_match[ 1 ] . ':units';

				/*
				 * An array of Open Graph types, their meta tags, and their associated metadata keys.
				 */
				$og_type_mt_md = $this->p->cf[ 'head' ][ 'og_type_mt' ][ $type_id ];

				if ( isset( $og_type_mt_md[ $mt_units_name ] ) ) {	// Value needs a units meta tag.

					$md_units_key = $og_type_mt_md[ $mt_units_name ];	// An empty string or metadata options key.

					$values[ $mt_units_name ] = $unit_text = WpssoSchema::get_unit_text( $md_key );

					if ( ! empty( $md_opts[ $md_units_key ] ) ) {	// Custom unit text found.

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

							$this->p->debug->log( $mt_units_name . ' from metadata = ' . $md_opts[ $md_units_key ] );
						}

						$values[ $mt_units_name ] = $md_opts[ $md_units_key ];
					}

					/*
					 * Since WPSSO Core v14.0.0.
					 *
					 * Check if we need to convert the value.
					 */
					if ( $values[ $mt_units_name ] !== $unit_text ) {

						$unit_value = WpssoUtilUnits::get_convert( $values[ $mt_name ], $unit_text, $values[ $mt_units_name ] );

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

							$this->p->debug->log( 'converted ' . $values[ $mt_name ] . ' ' . $values[ $mt_units_name ] .
								' to ' . $unit_value . ' ' . $unit_text );
						}

						$values[ $mt_name ]       = $unit_value;
						$values[ $mt_units_name ] = $unit_text;
					}
				}

				return true;
			}

			/*
			 * If the value does not need to be converted, then add the custom metadata value as-is.
			 */
			if ( $this->p->debug->enabled ) {

				$this->p->debug->log( $mt_name . ' from metadata = ' . $md_opts[ $md_key ] );
			}

			$values[ $mt_name ] = $md_opts[ $md_key ];

			return true;
		}

		/*
		 * If the Open Graph type isn't already hard-coded (ie. ':disabled'), then using the post type and the Schema type,
		 * check for a possible hard-coded Open Graph type.
		 */
		private function maybe_update_post_og_type( array $md_opts, $post_id, array $mod ) {

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

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

			if ( empty( $md_opts[ 'og_type:disabled' ] ) ) {	// Just in case.

				/*
				 * Check if the post type matches a pre-defined Open Graph type.
				 *
				 * For example, a post type of 'organization' would return 'website' for the Open Graph type.
				 *
				 * Returns false or an Open Graph type string.
				 */
				if ( $og_type = $this->p->post->get_post_type_og_type( $mod ) ) {

					$md_opts[ 'og_type' ] = $og_type;

					$md_opts[ 'og_type:disabled' ] = true;

				} else {

					/*
					 * Use the saved Schema type or get the default Schema type.
					 */
					if ( isset( $md_opts[ 'schema_type' ] ) ) {

						$type_id = $md_opts[ 'schema_type' ];

					} else $type_id = $this->p->schema->get_mod_schema_type_id( $mod, $use_md_opts = false );

					/*
					 * Check if the Schema type matches a pre-defined Open Graph type.
					 */
					if ( $og_type = $this->p->schema->get_schema_type_og_type( $type_id ) ) {

						$md_opts[ 'og_type' ] = $og_type;

						$md_opts[ 'og_type:disabled' ] = true;
					}
				}
			}

			return $md_opts;
		}
	}
}
