<?php
defined( 'ABSPATH' ) || die( 'Cheatin\' uh?' );

class SQ_Models_Services_JsonLD extends SQ_Models_Abstract_Seo {

	private $_data = array();
	private $_types = array();
	private $_markups = array();

	public function __construct() {
		parent::__construct();

		if ( isset( $this->_post->sq->doseo ) && $this->_post->sq->doseo ) {
			if ( ! $this->_post->sq->do_jsonld ) {
				add_filter( 'sq_json_ld', array( $this, 'returnFalse' ) );

				return;
			}

			//decode the URL for json format
			if ( $this->_post->url ) {
				$this->_post->url = urldecode( esc_url( $this->_post->url ) );
			}

			//If not yet set
			$this->_post->sq->jsonld_types = array_filter( (array) $this->_post->sq->jsonld_types );
			if ( empty( $this->_post->sq->jsonld_types ) ) {
				//If not set, get the type saved by Open Graph
				if ( $this->_post->sq->og_type <> '' ) {
					$this->_post->sq->jsonld_types = array( $this->_post->sq->og_type );
				}
			}

			if ( ! empty( $this->_post->sq->jsonld_types ) ) {
				add_filter( 'sq_json_ld', array( $this, 'generateStructuredData' ) );

				add_filter( 'sq_json_ld', array( $this, 'generateJsonLd' ) );
				add_filter( 'sq_json_ld', array( $this, 'packJsonLd' ), 99 );

			}
		} else {
			add_filter( 'sq_json_ld', array( $this, 'returnFalse' ) );
		}

	}

	/**
	 * Sanitizes, encodes and outputs structured data.
	 *
	 * @return array|string
	 */
	public function generateJsonLd() {
		return $this->getStructuredData( $this->getDataTypes() ) ;
	}

	/**
	 * Pack the Structured Data
	 */
	public function packJsonLd( $data = array() ) {
		if ( ! empty( $data ) ) {
			//If custom JSON-LD and not Auto Draft
			if ( $this->_post->sq->jsonld && strpos( $this->_post->sq->jsonld, '"name":"Auto Draft"' ) === false ) {
				$safe = $this->renderCustomJsonLd( $this->_post->sq->jsonld );
				if ( $safe !== '' ) {
					return $safe;
				}

			} else {
				//Compatibility with ACF
				if ( SQ_Classes_Helpers_Tools::isPluginInstalled( 'advanced-custom-fields/acf.php' ) ) {
					if ( isset( $this->_post->ID ) && $this->_post->ID ) {
						if ( $_sq_jsonld_custom = get_post_meta( $this->_post->ID, '_sq_jsonld_custom', true ) ) {
							$safe = $this->renderCustomJsonLd( $_sq_jsonld_custom );
							if ( $safe !== '' ) {
								return $safe;
							}
						}
					}
				}

				//JSON_HEX_TAG | JSON_HEX_AMP keep "<", ">" and "&" out of the markup so
				//post-derived values (titles, descriptions, ...) can't break out of the wrapper.
				$flags = JSON_HEX_TAG | JSON_HEX_AMP;
				if ( SQ_DEBUG && ! SQ_Classes_Helpers_Tools::isAjax() ) {
					$flags = $flags | JSON_PRETTY_PRINT;
				}

				return '<script type="application/ld+json">' . wp_json_encode( $data, $flags ) . '</script>';
			}
		}

		return false;
	}

	/**
	 * Render custom JSON-LD safely. Accepts either raw JSON or a pre-wrapped
	 * <script type="application/ld+json">...</script> block. Whatever's passed in
	 * is reduced to its JSON payload, validated, and re-emitted inside a fresh
	 * wrapper. Anything that can't be parsed as JSON is dropped - this is the
	 * choke point that blocks </script> XSS payloads.
	 */
	private function renderCustomJsonLd( $raw ) {
		if ( ! is_string( $raw ) || $raw === '' ) {
			return '';
		}

		//Strip all script wrappers, including malicious ones smuggled inside.
		$stripped = preg_replace( '#</?script[^>]*>#i', '', $raw );
		$decoded  = json_decode( trim( $stripped ), true );

		if ( ! is_array( $decoded ) ) {
			return '';
		}

		//Encode with JSON_HEX_TAG | JSON_HEX_AMP so "<", ">" and "&" become "<"
		//etc. This is still valid JSON (parsers decode it back), but it makes it
		//impossible for a value that smuggled in (e.g. unicode-escaped "</script>")
		//to break out of the wrapper - the regex above is no longer the only defense.
		$json = wp_json_encode( $decoded, JSON_HEX_TAG | JSON_HEX_AMP );

		return '<script type="application/ld+json">' . $json . '</script>';
	}

	/**
	 * Sets data.
	 *
	 * @param array $data Structured data.
	 * @param bool $reset Unset data (default: false).
	 *
	 * @return bool
	 */
	public function setData( $data, $reset = false ) {

		if ( ! isset( $data['@type'] ) || ! preg_match( '|^[a-zA-Z]{1,20}$|', $data['@type'] ) ) {
			return false;
		} else {
			if ( $data['@type'] <> 'Review' && in_array( strtolower( $data['@type'] ), $this->_types ) ) {
				return false;
			}

			$this->_types[] = strtolower( $data['@type'] );
		}

		if ( $reset && isset( $this->_data ) ) {
			unset( $this->_data );
		}

		$this->_data[] = $data;

		return true;
	}

	/**
	 * Gets data.
	 *
	 * @return array
	 */
	public function getData() {
		return apply_filters( 'sq_json_ld_data', $this->_data );
	}

	/**
	 * Structures and returns data.
	 *
	 * List of types available by default for specific request:
	 *
	 * 'product',
	 * 'review',
	 * 'breadcrumblist',
	 * 'website',
	 * 'order',
	 *
	 * @param array $types Structured data types.
	 *
	 * @return array
	 */
	public function getStructuredData( $types ) {
		$data = array();

		// Put together the values of same type of structured data.
		foreach ( $this->getData() as $type => $value ) {
			if ( is_numeric( $type ) ) {
				$data[ $value['@type'] ][]               = $value;
				$data[ strtolower( $value['@type'] ) ][] = $value;
			} else {
				$data[ $type ][] = $value;
			}
		}

		//mage array unique
		$types = array_unique( $types );

		// Wrap the multiple values of each type inside a graph... Then add context to each type.
		foreach ( $data as $type => $value ) {
			$data[ $type ] = count( (array) $value ) > 1 ? array( '@graph' => $value ) : $value[0];
		}

		// If requested types, pick them up... Finally, change the associative array to an indexed one.
		$data = $types ? array_values( array_intersect_key( $data, array_flip( $types ) ) ) : array_values( $data );

		if ( ! empty( $data ) ) {
			$data = array( '@context' => 'https://schema.org', '@graph' => $data );
		}

		return $data;
	}

	/**
	 * Get data types for pages.
	 *
	 * @return array
	 */
	public function getDataTypes() {
		return array_filter( apply_filters( 'sq_structured_data_type_for_page', $this->_types ) );
	}

	/**
	 * Make sure the data is sanitized
	 *
	 * @param  $var
	 *
	 * @return array|string
	 */
	public function clean( $var ) {
		if ( is_array( $var ) ) {
			return array_map( array( $this, 'clean' ), $var );
		} else {
			return is_scalar( $var ) ? sanitize_text_field( $var ) : $var;
		}
	}

	/**
	 * Set custom JsonLD from database postmeta
	 *
	 * @param  $markup
	 * @param  $post
	 * @param  $type
	 *
	 * @return mixed
	 */
	public function setCustomJsonLd( $markup, $post, $type ) {
		//Custom jsonld for this post id
		$sq_jsonld = get_post_meta( $post->ID, '_sq_jsonld_builder', true );

		if ( is_string( $sq_jsonld ) && ! empty( $sq_jsonld ) ) {
			$sq_jsonld = json_decode( $sq_jsonld, true );
		}

		if ( ! empty( $sq_jsonld ) && isset( $sq_jsonld[ strtolower( $type ) ] ) ) {
			foreach ( $sq_jsonld[ strtolower( $type ) ] as $key => $value ) {

				if ( is_array( $value ) ) {
					$value = @array_filter( $value );

					if ( empty( $value ) ) {
						continue;
					}

					foreach ( $value as $index => $value1 ) {
						if ( is_array( $value1 ) ) {
							$value1 = @array_filter( $value1 );
							if ( empty( $value1 ) ) {
								unset( $value[ $index ] );
							}
						}
					}

					if ( count( $value ) == 1 ) {
						continue;
					}

					//don't let numeric indexing
					if ( current( array_keys( $value ) ) == 0 ) {
						$value = array_values( $value );
					}
				}

				if ( $value ) {
					$markup[ $key ] = $value;
				}
			}
		}

		return $markup;

	}

	/**
	 * Generate JSON-LD Structured data based on jsonld types and post type
	 */
	public function generateStructuredData() {

		if ( $this->_post->post_type == 'home' ) {
			$jsonld_type      = SQ_Classes_Helpers_Tools::getOption( 'sq_jsonld_type' );
			$this->_markups[] = $this->getPublisherMarkup( $jsonld_type );
		}

		if ( in_array( 'website', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getWebsiteMarkup();
		}

		if ( in_array( 'newsarticle', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getArticleMarkup( 'NewsArticle' );
		}

		if ( in_array( 'article', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getArticleMarkup( 'Article' );
		}

		if ( in_array( 'QA Page', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getQAMarkup();
		}

		if ( in_array( 'question', $this->_post->sq->jsonld_types ) || in_array( 'FAQ page', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getQuestionMarkup();
		}

		if ( in_array( 'movie', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getMovieMarkup();
		}

		if ( in_array( 'video', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getVideoMarkup();
		}

		if ( in_array( 'recipe', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getRecipeMarkup();
		}

		if ( in_array( 'review', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getReviewMarkup();
		}

		if ( in_array( 'local store', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getLocalBusinessMarkup( 'Store' );
		}

		if ( in_array( 'local restaurant', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getLocalBusinessMarkup( 'Restaurant' );
		}

		if ( $this->_post->post_type == 'profile' || in_array( 'profile', $this->_post->sq->jsonld_types ) ) {
			if ( $this->_post->post_author <> '' ) {
				$author_url = $this->getAuthorUrl();

				$markup['@type'] = 'Person';
				$markup['@id']   = $author_url . '#person';
				$markup['url']   = $author_url;
				$markup['name']  = $this->cleanText( $this->getAuthor( 'display_name' ) );

				//keep the author's personal website as sameAs only when it's a valid absolute URL
				$website = $this->getAuthor( 'user_url' );
				if ( $website <> '' && $website <> $author_url && filter_var( $website, FILTER_VALIDATE_URL ) ) {
					$markup['sameAs'] = esc_url( $website );
				}

				$this->_markups[] = $markup;
			}
		}

		if ( in_array( 'person', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getPersonMarkup();
		}

		if ( in_array( 'organization', $this->_post->sq->jsonld_types ) ) {
			if ( $organization = $this->getPublisherMarkup( 'Organization' ) ) {
				$this->_markups[] = $organization;
			}
		}

		if ( in_array( 'event', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getEventMarkup();
		}

		if ( in_array( 'how to', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getHowToMarkup();
		}

		if ( in_array( 'course', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getCourseMarkup();
		}

		if ( in_array( 'book', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getBookMarkup();
		}

		if ( in_array( 'job posting', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getJobPostingMarkup();
		}

		if ( in_array( 'podcast', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getPodcastEpisodeMarkup();
		}

		if ( in_array( 'music', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getMusicMarkup();
		}

		if ( in_array( 'service', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getServiceMarkup();
		}

		if ( in_array( 'software application', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getSoftwareApplicationMarkup();
		}

		if ( in_array( 'video game', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getVideoGameMarkup();
		}

		if ( in_array( 'real estate', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getRealEstateListingMarkup();
		}

		if ( in_array( 'accommodation', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getAccommodationMarkup();
		}

		if ( in_array( 'about page', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getWebPageMarkup( 'AboutPage' );
		}

		if ( in_array( 'contact page', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getWebPageMarkup( 'ContactPage' );
		}

		if ( in_array( 'collection page', $this->_post->sq->jsonld_types ) ) {
			$this->_markups[] = $this->getWebPageMarkup( 'CollectionPage' );
		}

		if ( SQ_Classes_Helpers_Tools::getOption( 'sq_jsonld_woocommerce' ) ) {
			if ( SQ_Classes_Helpers_Tools::isPluginInstalled( 'woocommerce/woocommerce.php' ) ) {
				// Generate structured data for Woocommerce 3+.
				if ( $this->_post->post_type == 'product' || in_array( 'product', $this->_post->sq->jsonld_types ) ) {
					$this->_markups[] = $this->getWoocommerceProductMarkup();
				}
			}
		}

		//Generic Product markup when WooCommerce isn't providing the rich product data
		if ( in_array( 'product', $this->_post->sq->jsonld_types ) && ! ( SQ_Classes_Helpers_Tools::getOption( 'sq_jsonld_woocommerce' ) && SQ_Classes_Helpers_Tools::isPluginInstalled( 'woocommerce/woocommerce.php' ) ) ) {
			$this->_markups[] = $this->getProductMarkup();
		}

		if ( SQ_Classes_Helpers_Tools::getOption( 'sq_jsonld_breadcrumbs' ) ) {
			$this->_markups[] = $this->getBreadcrumbsMarkup();
		}

		//register the markup
		foreach ( $this->_markups as $markup ) {
			$this->setData( $markup );
		}
	}

	/**
	 * Prepare a text for a JSON-LD value.
	 *
	 * Titles and descriptions are stored HTML-escaped (see SQ_Classes_Helpers_Sanitize::clearTitle)
	 * because they are built for the <title> tag and meta attributes, where the browser decodes
	 * the entities back. JSON-LD lives inside a <script> block, which the HTML spec treats as raw
	 * text - character references are never decoded there, so an entity would be read literally
	 * (e.g. "Tenant Information &#038; Support") by Google and any other consumer.
	 *
	 * Decode the entities here so the schema carries plain text. wp_json_encode() escapes whatever
	 * needs escaping at the JSON level afterwards.
	 *
	 * @param string $text
	 *
	 * @return string
	 */
	public function cleanText( $text ) {
		if ( ! is_string( $text ) || $text === '' ) {
			return $text;
		}

		$text = html_entity_decode( $text, ENT_QUOTES, 'UTF-8' );

		//Non-breaking spaces survive the decode as U+00A0 and read as stray spacing in schema.
		$text = str_replace( "\xc2\xa0", ' ', $text );

		return trim( preg_replace( '/\s{2,}/', ' ', $text ) );
	}

	/**
	 * Set the markup for the Article, NewsArticle Schema
	 *
	 * @param  $post_type
	 *
	 * @return mixed
	 */
	public function getArticleMarkup( $post_type ) {
		$markup          = array();
		$markup['@type'] = $post_type;
		$markup['@id']   = $this->_post->url . '#' . strtolower( $post_type );
		$markup['url']   = $this->_post->url;

		if ( isset( $this->_post->sq->title ) ) {
			$markup['headline'] = $this->cleanText( $this->truncate( $this->_post->sq->title, 0, $this->_post->sq->jsonld_title_maxlength ) );
		}

		$markup['mainEntityOfPage'] = array(
			'@type' => 'WebPage',
			'url'   => $this->_post->url
		);

		if ( $this->_post->sq->og_media <> '' ) {
			$markup['thumbnailUrl'] = $this->_post->sq->og_media;
		}
		if ( isset( $this->_post->post_date ) ) {
			$markup['datePublished'] = wp_date( 'c', strtotime( $this->_post->post_date ) );
		}
		if ( isset( $this->_post->post_modified ) ) {
			$markup['dateModified'] = wp_date( 'c', strtotime( $this->_post->post_modified ) );
		}

		if ( $this->_post->sq->og_media <> '' ) {
			$markup['image'] = array(
				"@type"  => "ImageObject",
				"url"    => $this->_post->sq->og_media,
				"height" => 500,
				"width"  => 500,
			);
		} else {
			$this->_setMedia( $markup );
		}

		$markup['author'] = $this->getAuthorMarkup();

		if ( $publisher = $this->getPublisherMarkup( 'Organization' ) ) {
			$markup['publisher'] = $publisher;
		}

		if ( $this->_post->sq->keywords <> '' ) {
			$markup['keywords'] = $this->_post->sq->keywords;
		}

		return apply_filters( 'sq_jsonld_article_markup', $markup, $this->_post, $post_type );
	}

	/**
	 * Get Question Markup
	 *
	 * @return mixed
	 */
	public function getQAMarkup() {
		$type = 'QAPage';

		$markup          = array();
		$markup['@type'] = $type;
		$markup['@id']   = $this->_post->url . '#' . strtolower( $type );
		$markup['url']   = $this->_post->url;

		if ( isset( $this->_post->sq->title ) && isset( $this->_post->sq->description ) ) {
			$markup['mainEntity'][] = array(
				"@type"          => "Question",
				"name"           => $this->cleanText( $this->_post->sq->title ),
				"text"           => $this->cleanText( $this->_post->sq->title ),
				"answerCount"    => 1,
				"upvoteCount"    => 0,
				"dateCreated"    => wp_date( 'c', strtotime( $this->_post->post_date ) ),
				"author"         => $this->getAuthorMarkup(),
				"acceptedAnswer" => array(
					"@type"       => "Answer",
					"text"        => $this->cleanText( $this->_post->sq->description ),
					"upvoteCount" => 0,
					"url"         => $this->_post->url,
					"author"      => $this->getAuthorMarkup(),
					"dateCreated" => wp_date( 'c', strtotime( $this->_post->post_date ) ),
				)
			);
		}

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get Question Markup
	 *
	 * @return mixed
	 */
	public function getQuestionMarkup() {
		$type = 'FAQPage';

		$markup          = array();
		$markup['@type'] = $type;
		$markup['@id']   = $this->_post->url . '#' . strtolower( $type );
		$markup['url']   = $this->_post->url;

		if ( isset( $this->_post->sq->title ) && isset( $this->_post->sq->description ) ) {
			$markup['mainEntity'][] = array(
				"@type"          => "Question",
				"name"           => $this->cleanText( $this->truncate( $this->_post->sq->title, 0, $this->_post->sq->jsonld_title_maxlength ) ),
				"acceptedAnswer" => array(
					"@type" => "Answer",
					"text"  => $this->cleanText( $this->truncate( $this->_post->sq->description, 0, $this->_post->sq->jsonld_description_maxlength ) ),
				)
			);
		}

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get Movie Markup
	 *
	 * @return mixed
	 */
	public function getVideoMarkup() {
		$markup = array();
		$type   = 'VideoObject';

		if ( $videos = $this->getPostVideos() ) {
			$markup['@type'] = $type;
			$markup['@id']   = $this->_post->url . '#' . strtolower( $type );
			$markup['url']   = $this->_post->url;

			if ( isset( $this->_post->sq->title ) ) {
				$markup['name'] = $this->cleanText( $this->truncate( $this->_post->sq->title, 0, $this->_post->sq->jsonld_title_maxlength ) );
			}

			if ( isset( $this->_post->sq->description ) ) {
				$markup['description'] = $this->cleanText( $this->truncate( $this->_post->sq->description, 0, $this->_post->sq->jsonld_description_maxlength ) );
			}

			if ( isset( $this->_post->post_date ) ) {
				$markup['uploadDate'] = wp_date( 'c', strtotime( $this->_post->post_date ) );
			}

			if ( ! empty( $videos ) ) {
				foreach ( $videos as $index => $video ) {
					if ( $video['src'] <> '' ) {
						$markup['thumbnailUrl'] = array(
							$video['thumbnail']
						);

						$markup['embedUrl'] = $video['src'];
					}
				}

			}

			$markup['publisher'] = $this->getPublisherMarkup( 'Organization' );
		}

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get Movie Markup
	 *
	 * @return mixed
	 */
	public function getMovieMarkup() {
		$type = 'Movie';

		$markup          = array();
		$markup['@type'] = $type;
		$markup['@id']   = $this->_post->url . '#' . strtolower( $type );
		$markup['url']   = $this->_post->url;

		if ( isset( $this->_post->sq->title ) ) {
			$markup['name'] = $this->cleanText( $this->truncate( $this->_post->sq->title, 0, $this->_post->sq->jsonld_title_maxlength ) );
		}

		if ( isset( $this->_post->sq->description ) ) {
			$markup['description'] = $this->cleanText( $this->truncate( $this->_post->sq->description, 0, $this->_post->sq->jsonld_description_maxlength ) );
		}

		if ( isset( $this->_post->post_date ) ) {
			$markup['dateCreated'] = wp_date( 'c', strtotime( $this->_post->post_date ) );
		}

		if ( $this->_post->sq->og_media <> '' ) {
			$markup['image'] = array(
				"@type"  => "ImageObject",
				"url"    => $this->_post->sq->og_media,
				"height" => 500,
				"width"  => 500,
			);
		} else {
			$this->_setMedia( $markup );
		}

		$markup['director']  = $this->getAuthorMarkup();
		$markup['publisher'] = $this->getPublisherMarkup( 'Organization' );

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get Recipe Markup
	 *
	 * @return mixed
	 */
	public function getRecipeMarkup() {
		$type = 'Recipe';

		$markup          = array();
		$markup['@type'] = $type;
		$markup['@id']   = $this->_post->url . '#' . strtolower( $type );
		$markup['url']   = $this->_post->url;

		if ( isset( $this->_post->sq->title ) ) {
			$markup['name']     = $this->cleanText( $this->truncate( $this->_post->sq->title, 0, $this->_post->sq->jsonld_title_maxlength ) );
			$markup['headline'] = $this->cleanText( $this->truncate( $this->_post->sq->title, 0, $this->_post->sq->jsonld_title_maxlength ) );
			$markup['keywords'] = $this->cleanText( $this->truncate( $this->_post->sq->title, 0, $this->_post->sq->jsonld_title_maxlength ) );
		}

		if ( isset( $this->_post->sq->description ) ) {
			$markup['description'] = $this->cleanText( $this->truncate( $this->_post->sq->description, 0, $this->_post->sq->jsonld_description_maxlength ) );
		}

		if ( isset( $this->_post->post_date ) ) {
			$markup['datePublished'] = wp_date( 'c', strtotime( $this->_post->post_date ) );
		}

		if ( $this->_post->sq->og_media <> '' ) {
			$markup['image'] = array(
				"@type"  => "ImageObject",
				"url"    => $this->_post->sq->og_media,
				"height" => 500,
				"width"  => 500,
			);
		} else {
			$this->_setMedia( $markup );
		}

		$markup['mainEntityOfPage'] = array(
			'@type' => 'WebPage',
			'url'   => $this->_post->url
		);

		$markup['prepTime']  = 'PT10M';
		$markup['cookTime']  = 'PT20M';
		$markup['totalTime'] = 'PT30M';
		$markup['nutrition'] = array(
			'@type'    => 'NutritionInformation',
			'calories' => '0 calories'
		);

		if ( $this->_post->sq->keywords <> '' ) {
			$markup['keywords'] = $this->_post->sq->keywords;
		}

		if ( $videos = $this->getPostVideos() ) {
			foreach ( $videos as $index => $video ) {
				if ( $video['src'] <> '' ) {
					$markup['video'] = array(
						"@type"        => "VideoObject",
						"embedUrl"     => $videos['src'],
						"name"         => ( isset( $markup['name'] ) ? $markup['name'] : '' ),
						"description"  => ( isset( $markup['description'] ) ? $markup['description'] : '' ),
						"thumbnailUrl" => array(
							$video['thumbnail']
						),
						"height"       => 500,
						"width"        => 500,
					);
					break;
				}
			}
		}

		$markup['author']    = $this->getAuthorMarkup();
		$markup['publisher'] = $this->getPublisherMarkup( 'Organization' );

		if ( $this->_post->sq->keywords <> '' ) {
			$markup['keywords'] = $this->_post->sq->keywords;
		}

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get Review Markup
	 *
	 * @param array $reviewObject
	 *
	 * @return mixed
	 */
	public function getReviewMarkup( $reviewObject = false ) {
		$type = 'Review';

		$markup          = array();
		$markup['@type'] = $type;
		$markup['@id']   = $this->_post->url . '#' . strtolower( $type );
		$markup['url']   = $this->_post->url;

		if ( isset( $this->_post->sq->title ) ) {
			$markup['name'] = $this->cleanText( $this->truncate( $this->_post->sq->title, 0, $this->_post->sq->jsonld_title_maxlength ) );
		}

		if ( isset( $this->_post->sq->description ) ) {
			$markup['reviewBody'] = $this->cleanText( $this->truncate( $this->_post->sq->description, 0, $this->_post->sq->jsonld_description_maxlength ) );
		}

		$markup['author']    = $this->getAuthorMarkup();
		$markup['publisher'] = $this->getPublisherMarkup( 'Organization' );

		if ( ! $reviewObject ) {
			$reviewObject = $this->getPublisherMarkup( 'Organization' );
		}

		$markup['itemReviewed'] = $reviewObject;

		$markup['reviewRating'] = array(
			'@type'       => 'Rating',
			'ratingValue' => 5,
		);

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Build a generic schema markup filled only with the data Squirrly SEO can
	 * derive automatically from the current post/site (title, description, url,
	 * image, dates, author, publisher, keywords). Type-specific generators below
	 * reuse this and add their own auto-fillable fields on top.
	 *
	 * @param string $type schema.org @type
	 * @param array  $args flags controlling which auto fields to include
	 *
	 * @return array
	 */
	protected function getGenericMarkup( $type, $args = array() ) {
		$args = array_merge( array(
			'name_key'      => 'name',   // 'name', 'headline' or 'title'
			'description'   => true,
			'image'         => true,
			'date_key'      => false,    // e.g. 'datePublished', 'datePosted', 'uploadDate'
			'modified_key'  => false,    // e.g. 'dateModified'
			'author'        => false,    // false or the property key (e.g. 'author', 'director')
			'publisher'     => false,    // false or the property key (e.g. 'publisher', 'organizer', 'provider')
			'keywords'      => false,
		), $args );

		$markup          = array();
		$markup['@type'] = $type;
		$markup['@id']   = $this->_post->url . '#' . strtolower( $type );
		$markup['url']   = $this->_post->url;

		if ( isset( $this->_post->sq->title ) ) {
			$markup[ $args['name_key'] ] = $this->cleanText( $this->truncate( $this->_post->sq->title, 0, $this->_post->sq->jsonld_title_maxlength ) );
		}

		if ( $args['description'] && isset( $this->_post->sq->description ) ) {
			$markup['description'] = $this->cleanText( $this->truncate( $this->_post->sq->description, 0, $this->_post->sq->jsonld_description_maxlength ) );
		}

		if ( $args['date_key'] && isset( $this->_post->post_date ) ) {
			$markup[ $args['date_key'] ] = wp_date( 'c', strtotime( $this->_post->post_date ) );
		}

		if ( $args['modified_key'] && isset( $this->_post->post_modified ) ) {
			$markup[ $args['modified_key'] ] = wp_date( 'c', strtotime( $this->_post->post_modified ) );
		}

		if ( $args['image'] ) {
			if ( $this->_post->sq->og_media <> '' ) {
				$markup['image'] = array(
					"@type"  => "ImageObject",
					"url"    => $this->_post->sq->og_media,
					"height" => 500,
					"width"  => 500,
				);
			} else {
				$this->_setMedia( $markup );
			}
		}

		if ( $args['author'] ) {
			if ( $author = $this->getAuthorMarkup() ) {
				$markup[ $args['author'] ] = $author;
			}
		}

		if ( $args['publisher'] ) {
			if ( $publisher = $this->getPublisherMarkup( 'Organization' ) ) {
				$markup[ $args['publisher'] ] = $publisher;
			}
		}

		if ( $args['keywords'] && $this->_post->sq->keywords <> '' ) {
			$markup['keywords'] = $this->_post->sq->keywords;
		}

		return $markup;
	}

	/**
	 * Get the markup for the Person Schema (the entity described by the page)
	 *
	 * @return mixed
	 */
	public function getPersonMarkup() {
		$type   = 'Person';
		$markup = $this->getGenericMarkup( $type );

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get the markup for the Product Schema (non-WooCommerce)
	 *
	 * @return mixed
	 */
	public function getProductMarkup() {
		$type = 'Product';

		//A Product is only eligible for Google when it has offers, review or
		//aggregateRating - none of which can be derived from post content. To avoid
		//"Missing field" issues we leave it empty by default (WooCommerce products
		//are handled by getWoocommerceProductMarkup). The filter lets custom code
		//add the commerce data when it has it.
		$markup = array();

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get the markup for the Event Schema
	 *
	 * @return mixed
	 */
	public function getEventMarkup() {
		$type = 'Event';

		//Event is a Google-validated rich result: startDate and location are
		//required. Pull the dates from The Events Calendar and the location from
		//the site Organization address; if either is missing we don't emit the
		//markup so it never fails the Rich Results test.
		$start = $end = '';
		if ( $this->_post->ID && SQ_Classes_Helpers_Tools::isPluginInstalled( 'the-events-calendar/the-events-calendar.php' ) ) {
			$start = get_post_meta( (int) $this->_post->ID, '_EventStartDate', true );
			$end   = get_post_meta( (int) $this->_post->ID, '_EventEndDate', true );
		}

		$location     = false;
		$organization = $this->getPublisherMarkup( 'Organization' );
		if ( ! empty( $organization['address'] ) ) {
			$location = array(
				'@type'   => 'Place',
				'name'    => ( ! empty( $organization['name'] ) ? $organization['name'] : ( isset( $this->_post->sq->title ) ? $this->cleanText( $this->_post->sq->title ) : '' ) ),
				'address' => $organization['address'],
			);
		}

		if ( $start == '' || ! $location ) {
			return array();
		}

		$markup                        = $this->getGenericMarkup( $type, array( 'publisher' => 'organizer' ) );
		$markup['eventStatus']         = 'https://schema.org/EventScheduled';
		$markup['eventAttendanceMode'] = 'https://schema.org/OfflineEventAttendanceMode';
		$markup['startDate']           = wp_date( 'c', strtotime( $start ) );
		if ( $end <> '' ) {
			$markup['endDate'] = wp_date( 'c', strtotime( $end ) );
		}
		$markup['location'] = $location;

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get the markup for the HowTo Schema
	 *
	 * @return mixed
	 */
	public function getHowToMarkup() {
		$type   = 'HowTo';
		$markup = $this->getGenericMarkup( $type );

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get the markup for the Course Schema
	 *
	 * @return mixed
	 */
	public function getCourseMarkup() {
		$type   = 'Course';
		$markup = $this->getGenericMarkup( $type, array( 'publisher' => 'provider' ) );

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get the markup for the Book Schema
	 *
	 * @return mixed
	 */
	public function getBookMarkup() {
		$type   = 'Book';
		$markup = $this->getGenericMarkup( $type, array( 'author' => 'author' ) );

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get the markup for the JobPosting Schema
	 *
	 * @return mixed
	 */
	public function getJobPostingMarkup() {
		$type = 'JobPosting';

		//JobPosting is a Google-validated rich result requiring a jobLocation.
		//Build it from the site Organization address; skip when none is configured
		//so we never emit an incomplete JobPosting that fails the Rich Results test.
		$organization = $this->getPublisherMarkup( 'Organization' );
		if ( empty( $organization['address'] ) ) {
			return array();
		}

		$markup = $this->getGenericMarkup( $type, array(
			'name_key'  => 'title',
			'date_key'  => 'datePosted',
			'publisher' => 'hiringOrganization',
		) );

		$markup['jobLocation'] = array(
			'@type'   => 'Place',
			'address' => $organization['address'],
		);

		if ( $this->_post->ID ) {
			$markup['identifier'] = array(
				'@type' => 'PropertyValue',
				'name'  => 'Job ID',
				'value' => (int) $this->_post->ID,
			);
		}

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get the markup for the PodcastEpisode Schema
	 *
	 * @return mixed
	 */
	public function getPodcastEpisodeMarkup() {
		$type   = 'PodcastEpisode';
		$markup = $this->getGenericMarkup( $type, array( 'date_key' => 'datePublished' ) );

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get the markup for the Music Schema
	 *
	 * @return mixed
	 */
	public function getMusicMarkup() {
		$type   = 'MusicGroup';
		$markup = $this->getGenericMarkup( $type );

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get the markup for the Service Schema
	 *
	 * @return mixed
	 */
	public function getServiceMarkup() {
		$type   = 'Service';
		$markup = $this->getGenericMarkup( $type, array( 'publisher' => 'provider' ) );

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get the markup for the SoftwareApplication Schema
	 *
	 * @return mixed
	 */
	public function getSoftwareApplicationMarkup() {
		$type = 'SoftwareApplication';

		//SoftwareApplication requires offers or aggregateRating for Google, which
		//cannot be derived from post content, so we leave it empty by default to
		//avoid "Missing field" issues. The filter lets custom code populate it.
		$markup = array();

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get the markup for the VideoGame Schema
	 *
	 * @return mixed
	 */
	public function getVideoGameMarkup() {
		$type   = 'VideoGame';
		$markup = $this->getGenericMarkup( $type );

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get the markup for the RealEstateListing Schema
	 *
	 * @return mixed
	 */
	public function getRealEstateListingMarkup() {
		$type   = 'RealEstateListing';
		$markup = $this->getGenericMarkup( $type, array( 'date_key' => 'datePosted' ) );

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get the markup for the Accommodation Schema
	 *
	 * @return mixed
	 */
	public function getAccommodationMarkup() {
		$type   = 'Accommodation';
		$markup = $this->getGenericMarkup( $type );

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get the markup for a WebPage subtype Schema (AboutPage, ContactPage, CollectionPage)
	 *
	 * @param string $type
	 *
	 * @return mixed
	 */
	public function getWebPageMarkup( $type = 'AboutPage' ) {
		$markup = $this->getGenericMarkup( $type, array(
			'date_key'     => 'datePublished',
			'modified_key' => 'dateModified',
		) );

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get Local Business Markup
	 *
	 * @param string $type
	 *
	 * @return mixed
	 */
	public function getLocalBusinessMarkup( $type = 'Store' ) {
		$organization = $this->getPublisherMarkup( 'Organization' );

		$markup          = array();
		$markup['@type'] = $type;
		$markup['@id']   = $this->_post->url . '#' . strtolower( $type );
		$markup['url']   = $this->_post->url;


		if ( isset( $organization['name'] ) && $organization['name'] ) {
			$markup['name'] = $organization['name'];
		} elseif ( isset( $this->_post->sq->title ) ) {
			$markup['name'] = $this->_post->sq->title;
			$markup['name'] = str_replace( '"', '\"', $markup['name'] );
		}

		if ( isset( $organization['logo'] ) && ! empty( $organization['logo'] ) ) {
			$markup['image'] = $organization['logo'];
		}
		if ( isset( $organization['address'] ) && ! empty( $organization['address'] ) ) {
			$markup['address'] = $organization['address'];
		}
		if ( isset( $organization['place']['geo'] ) && ! empty( $organization['place']['geo'] ) ) {
			$markup['geo'] = $organization['place']['geo'];
		}
		if ( isset( $organization['contactPoint']['telephone'] ) && ! empty( $organization['contactPoint']['telephone'] ) ) {
			$markup['telephone'] = $organization['contactPoint']['telephone'];
		}

		$markup['priceRange'] = "$";
		$markup['menu']       = $this->_post->url;

		//Set default local SEO data
		$jsonldLocal = SQ_Classes_Helpers_Tools::getOption( 'sq_jsonld_local' );
		if ( ! empty( $jsonldLocal ) ) {
			foreach ( $jsonldLocal as $key => $value ) {

				if ( is_array( $value ) ) {
					$value = @array_filter( $value );

					if ( empty( $value ) ) {
						continue;
					}

					foreach ( $value as $index => $value1 ) {
						if ( is_array( $value1 ) ) {
							$value1 = @array_filter( $value1 );
							if ( $key == 'openingHoursSpecification' ) {
								if ( count( $value1 ) < 3 ) {
									unset( $value[ $index ] );
								}
							} elseif ( count( $value1 ) < 2 ) {
								unset( $value[ $index ] );
							}

						}
					}

					if ( count( $value ) == 1 ) {
						continue;
					}

					if ( current( array_keys( $value ) ) == 0 ) {
						$value = array_values( $value );
					}
				}

				if ( $value ) {
					//create cuisine array
					if ( $key == 'servesCuisine' && is_string( $value ) ) {
						$value = explode( ',', $value );
					}
					$markup[ $key ] = $value;
				}
			}
		}

		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get the markup for the website Schema
	 *
	 * @return mixed
	 */
	public function getWebsiteMarkup() {
		$jsonld      = SQ_Classes_Helpers_Tools::getOption( 'sq_jsonld' );
		$jsonld_type = SQ_Classes_Helpers_Tools::getOption( 'sq_jsonld_type' );
		$type        = 'WebSite';

		$markup          = array();
		$markup['@type'] = $type;
		$markup['@id']   = $this->_post->url . '#' . strtolower( $type );
		$markup['url']   = $this->_post->url;

		if ( $jsonld[ $jsonld_type ]['name'] ) {
			$markup['name'] = $jsonld[ $jsonld_type ]['name'];
		} elseif ( isset( $this->_post->sq->title ) ) {
			$markup['name'] = $this->cleanText( $this->truncate( $this->_post->sq->title, 0, $this->_post->sq->jsonld_title_maxlength ) );
		}

		if ( isset( $this->_post->sq->description ) ) {
			$markup['headline'] = $this->cleanText( $this->truncate( $this->_post->sq->description, 0, $this->_post->sq->jsonld_description_maxlength ) );
		}

		$markup['mainEntityOfPage'] = array(
			'@type' => 'WebPage',
			'url'   => $this->_post->url
		);

		if ( $this->_post->sq->og_media <> '' ) {
			$markup['thumbnailUrl'] = $this->_post->sq->og_media;
		}
		if ( isset( $this->_post->post_date ) ) {
			$markup['datePublished'] = wp_date( 'c', strtotime( $this->_post->post_date ) );
		}
		if ( isset( $this->_post->post_modified ) ) {
			$markup['dateModified'] = wp_date( 'c', strtotime( $this->_post->post_modified ) );
		}

		if ( $this->_post->sq->og_media <> '' ) {
			$markup['image'] = array(
				"@type"  => "ImageObject",
				"url"    => $this->_post->sq->og_media,
				"height" => 500,
				"width"  => 700,
			);
		} else {
			$this->_setMedia( $markup );
		}

		//Show search bar for products and shops
		//the str_replace is added for compatibility with WPML plugin
		if ( $this->_post->post_type == 'product' || $this->_post->post_type == 'shop' ) {
			$markup['potentialAction'] = array(
				'@type'       => 'SearchAction',
				'target'      => str_replace( 'search_term_string', '{search_term_string}', home_url( '?s=search_term_string&post_type=product' ) ),
				'query-input' => 'required name=search_term_string',
			);
		} else {
			$markup['potentialAction'] = array(
				'@type'       => 'SearchAction',
				'target'      => str_replace( 'search_term_string', '{search_term_string}', home_url( '?s=search_term_string' ) ),
				'query-input' => 'required name=search_term_string',
			);
		}

		if ( $author = $this->getAuthorMarkup() ) {
			$markup['author'] = $author;
		}

		if ( $publisher = $this->getPublisherMarkup( $jsonld_type ) ) {
			$markup['publisher'] = $publisher;
		}

		if ( $this->_post->sq->keywords <> '' ) {
			$markup['keywords'] = $this->_post->sq->keywords;
		}


		return apply_filters( 'sq_jsonld_' . strtolower( $type ) . '_markup', $markup, $this->_post, $type );
	}

	/**
	 * Get the markup for the Author Schema
	 */
	public function getAuthorMarkup() {
		$markup = array();

		if ( SQ_Classes_Helpers_Tools::getOption( 'sq_jsonld_global_person' ) ) {
			$markup = $this->getPublisherMarkup( 'Person' );
		} else {

			$author_url   = $this->getAuthorUrl();
			$display_name = $this->getAuthor( 'display_name' );

			if ( $author_url <> '' && $display_name <> '' ) {
				$markup = array(
					"@type" => "Person",
					"@id"   => $author_url . "#person",
					"url"   => $author_url,
					"name"  => $this->cleanText( $display_name ),
				);

				//keep the author's personal website as sameAs only when it's a valid absolute URL
				$website = $this->getAuthor( 'user_url' );
				if ( $website <> '' && $website <> $author_url && filter_var( $website, FILTER_VALIDATE_URL ) ) {
					$markup['sameAs'] = esc_url( $website );
				}
			}
		}

		return apply_filters( 'sq_jsonld_author_markup', $markup, $this->_post, 'author', $this->_author );

	}

	/**
	 * Get the publisher markup
	 *
	 * @param  $jsonld_type
	 *
	 * @return array|bool
	 */
	public function getPublisherMarkup( $jsonld_type ) {
		$jsonld = SQ_Classes_Helpers_Tools::getOption( 'sq_jsonld' );

		$markup['@type'] = $jsonld_type;
		$markup['@id']   = $this->_post->url . '#' . strtolower( $jsonld_type );
		$markup['url']   = $this->_post->url;

		if ( isset( $jsonld[ $jsonld_type ] ) ) {
			$markup = array(
				"@type" => $jsonld_type,
				"@id"   => $this->_post->url . "#$jsonld_type",
				"url"   => $this->_post->url,
				"name"  => ( $jsonld[ $jsonld_type ]['name'] ? $jsonld[ $jsonld_type ]['name'] : get_bloginfo( 'title' ) ),
			);

			foreach ( $jsonld[ $jsonld_type ] as $key => $value ) {
				if ( $key == 'place' ) {
					continue;
				} //don't show geo for the organization schema
				if ( is_array( $value ) ) {
					$value = @array_filter( $value );

					if ( empty( $value ) ) {
						continue;
					}

					foreach ( $value as $key1 => $value1 ) {
						if ( is_array( $value1 ) ) {
							$value1 = @array_filter( $value1 );
							if ( count( $value1 ) == 1 ) {
								unset( $value[ $key1 ] );
							}
						}
					}

					if ( count( $value ) == 1 ) {
						continue;
					}

					$value['@id'] = $this->_post->url . "#" . strtolower( $key );
				}

				if ( $value ) {

					if ( $key == 'logo' && isset( $value['url'] ) && $value['url'] <> '' ) {
						if ( defined( 'WP_CONTENT_DIR' ) && $imagepath = str_replace( content_url(), WP_CONTENT_DIR, $value['url'] ) ) {
							if ( file_exists( $imagepath ) ) {
								list( $width, $height ) = @getimagesize( $imagepath );
								$value['width']  = $width;
								$value['height'] = $height;
							}
						}

						if ( $jsonld[ $jsonld_type ]['name'] ) {
							$value['caption'] = $jsonld[ $jsonld_type ]['name'];
						}
					}

					$markup[ $key ] = $value;

				}
			}

			$socials = SQ_Classes_Helpers_Tools::getOption( 'socials' );

			//Load the social media
			$jsonld_socials = array();
			if ( isset( $socials['facebook_site'] ) && $socials['facebook_site'] <> '' ) {
				$jsonld_socials[] = $socials['facebook_site'];
			}
			if ( isset( $socials['twitter_site'] ) && $socials['twitter_site'] <> '' ) {
				$jsonld_socials[] = $socials['twitter_site'];
			}
			if ( isset( $socials['instagram_url'] ) && $socials['instagram_url'] <> '' ) {
				$jsonld_socials[] = $socials['instagram_url'];
			}
			if ( isset( $socials['linkedin_url'] ) && $socials['linkedin_url'] <> '' ) {
				$jsonld_socials[] = $socials['linkedin_url'];
			}
			if ( isset( $socials['pinterest_url'] ) && $socials['pinterest_url'] <> '' ) {
				$jsonld_socials[] = $socials['pinterest_url'];
			}
			if ( isset( $socials['youtube_url'] ) && $socials['youtube_url'] <> '' ) {
				$jsonld_socials[] = $socials['youtube_url'];
			}

			if ( ! empty( $jsonld_socials ) ) {
				$markup['sameAs'] = $jsonld_socials;
			}
		}

		//Normalise the country to an ISO 3166-1 alpha-2 code for addressCountry
		if ( isset( $markup['address']['addressCountry'] ) && $markup['address']['addressCountry'] ) {
			$markup['address']['addressCountry'] = SQ_Classes_Helpers_Sanitize::getCountryCode( $markup['address']['addressCountry'] );
		}

		return apply_filters( 'sq_jsonld_publisher_markup', $markup, $this->_post, $jsonld_type );
	}

	/**
	 * Generates BreadcrumbList structured data.
	 */
	public function getBreadcrumbsMarkup() {
		$root = $crumbs = $lists = $markup = array();

		//show the breadcrumbs
		if ( $this->_post->post_type <> 'home' ) {
			///////////////////////////// Home Page
			$post = SQ_Classes_ObjController::getClass( 'SQ_Models_Snippet' )->setHomePage();

			if ( $post->ID == 0 || $this->_post->ID <> $post->ID ) {
				$root[] = array(
					( $post->sq->title <> '' ? $post->sq->title : $post->post_title ),
					$post->url,
				);
			}

			if ( $this->_post->post_type == 'category' && isset( $this->_post->term_id ) && isset( $this->_post->taxonomy ) ) {
				$parents = get_ancestors( $this->_post->term_id, $this->_post->taxonomy );
				if ( ! empty( $parents ) ) {
					$parents = array_reverse( $parents );

					foreach ( $parents as $parent ) {
						$parent = get_term( $parent );
						if ( ! is_wp_error( $parent ) ) {
							$crumbs[] = array(
								$parent->name,
								get_term_link( $parent->term_id, $this->_post->taxonomy ),
							);
						}
					}

					$lists[] = $crumbs;
				}
			} elseif ( $this->_post->post_type == 'product' ) {
				if ( class_exists( 'WC_Product' ) ) {
					$product = new WC_Product( $this->_post->ID );

					//Get all categories
					if ( $product instanceof WC_Product ) {
						$taxonomy = 'product_cat';

						if ( (int) $this->_post->sq->primary_category > 0 ) {
							//check if the primary category was selected by the client
							$category_ids = array( (int) $this->_post->sq->primary_category );
						} else {
							$category_ids = $product->get_category_ids();
						}

						if ( ! empty( $category_ids ) ) {
							foreach ( $category_ids as $category ) {
								$parents = get_ancestors( $category, $taxonomy );

								if ( ! empty( $parents ) ) {

									foreach ( $parents as $parent ) {
										$parent = get_term( $parent );
										if ( ! is_wp_error( $parent ) ) {
											$crumbs[] = array(
												$parent->name,
												get_term_link( $parent->term_id, $taxonomy ),
											);
										}
									}

									$category = get_term( $category, $taxonomy );
									if ( isset( $category->name ) && $category->name <> '' ) {
										$crumbs[] = array(
											$category->name,
											get_term_link( $category->term_id, $taxonomy ),
										);
									}

									$lists[] = $crumbs;
								} else {

									$category = get_term( $category, $taxonomy );
									if ( isset( $category->name ) && $category->name <> '' ) {
										$crumbs[] = array(
											$category->name,
											get_term_link( $category->term_id, $taxonomy ),
										);
									}

									$lists[] = $crumbs;
								}
							}
						}
					}
				}
			} else {
				/////////////////////// Parent Categories
				if ( (int) $this->_post->sq->primary_category > 0 ) {
					$categories = array( get_category( (int) $this->_post->sq->primary_category ) );
				} else {
					$categories = get_the_category( $this->_post->ID );
				}

				if ( ! empty( $categories ) ) {
					foreach ( $categories as $category ) {
						if ( isset( $category->term_id ) && isset( $category->taxonomy ) ) {
							$crumbs  = [];
							$parents = get_ancestors( $category->term_id, $category->taxonomy );

							if ( ! empty( $parents ) ) {
								$parents = array_reverse( $parents );

								foreach ( $parents as $parent ) {
									$parent = get_term( $parent );
									if ( isset( $parent->name ) && $parent->name <> '' ) {
										$crumbs[] = array(
											$parent->name,
											get_term_link( $parent->term_id, $category->taxonomy ),
										);
									}
								}

								if ( isset( $category->name ) && $category->name <> '' ) {
									$crumbs[] = array(
										$category->name,
										get_term_link( $category->term_id, $category->taxonomy ),
									);
								}

								$lists[] = $crumbs;
							} elseif ( isset( $category->name ) && $category->name <> '' ) {
								$crumbs[] = array(
									$category->name,
									get_term_link( $category->term_id, $category->taxonomy ),
								);

								$lists[] = $crumbs;
							}
						}
					}
				}
			}


			if ( ! empty( $crumbs ) ) {
				$markup['@type']           = 'BreadcrumbList';
				$markup['@id']             = $this->_post->url . '#' . 'breadcrumblist';
				$markup['itemListElement'] = array();

				foreach ( $lists as $list ) {
					//merge and reset the keys
					$list = array_merge( $root, $list );
					$list = array_values( $list );

					////////////////////// Current post
					$list[] = array(
						( $this->_post->sq->title <> '' ? $this->_post->sq->title : $this->_post->post_title ),
						$this->_post->url,
					);

					$itemListElement = array();
					foreach ( $list as $key => $crumb ) {
						$itemListElement[ $key ] = array(
							'@type'    => 'ListItem',
							'position' => $key + 1,
							'item'     => array(
								'@id'  => $crumb[1],
								'name' => $this->cleanText( $crumb[0] )
							),
						);
					}

					$markup['itemListElement'][] = $itemListElement;
				}
			}
		}

		return apply_filters( 'sq_jsonld_breadcrumbs_markup', $markup, $this->_post, 'breadcrumbs' );

	}

	/**
	 *
	 * Set the Image from Feature image
	 *
	 * @param $markup
	 */
	public function _setMedia( &$markup ) {
		$images = $this->getPostImages();
		if ( ! empty( $images ) ) {
			$image = current( $images );
			if ( isset( $image['src'] ) ) {
				$markup['image'] = array(
					"@type"  => "ImageObject",
					"url"    => $image['src'],
					"height" => 500,
					"width"  => 500,
				);
				if ( isset( $image['width'] ) ) {
					$markup['image']["width"] = $image['width'];
				}
				if ( isset( $image['height'] ) ) {
					$markup['image']["height"] = $image['height'];
				}
			}
		}
	}

	/**
	 * Generates Product structured data.
	 *
	 * @return array|void
	 */
	public function getWoocommerceProductMarkup() {
		$markup = array();

		try {

			if ( ! function_exists( 'wc_get_product' ) ) {
				return $markup;
			}

			if ( ! $product = wc_get_product( $this->_post->ID ) ) {
				return $markup;
			}

			if ( ! method_exists( $product, 'get_id' ) || ! method_exists( $product, 'get_price' ) ) {
				return $markup;
			}

			$shop_name = get_bloginfo( 'name' );
			$shop_url  = home_url();

			if ( function_exists( 'get_woocommerce_currency' ) ) {
				$currency = get_woocommerce_currency();
			}

			$sq_woocommerce = get_post_meta( $this->_post->ID, '_sq_woocommerce', true );
			$wc_fields      = array( 'mpn'  => 'mpn',
			                         'gtin' => 'gtin',
			                         'ean'  => 'gtin13',
			                         'upc'  => 'gtin12',
			                         'isbn' => 'isbn'
			);

			$markup['@type'] = 'Product';
			$markup['url']   = get_permalink( $product->get_id() );
			$markup['@id']   = $markup['url'] . '#' . 'product';

			if ( method_exists( $product, 'get_name' ) ) {
				$markup['name'] = $this->cleanText( $product->get_name() );
			} else {
				$markup['name'] = $this->cleanText( $product->get_title() );
			}

			if ( method_exists( $product, 'get_short_description' ) ) {
				$markup['description'] = $this->cleanText( wp_strip_all_tags( $product->get_short_description() ? $product->get_short_description() : $product->get_description() ) );
			}

			if ( method_exists( $product, 'get_image_id' ) ) {
				if ( $image = wp_get_attachment_url( $product->get_image_id() ) ) {
					$markup['image'] = $image;
				}
			}

			//By default, set the price available for 1 year
			$price_valid_until = gmdate( 'Y-m-d', strtotime( '+12 Month' ) );
			if ( method_exists( $product, 'get_date_on_sale_to' ) && method_exists( $product, 'get_date_on_sale_from' ) && is_object( $product->get_date_on_sale_to() ) && is_object( $product->get_date_on_sale_from() ) ) {
				if ( $product->get_date_on_sale_from() && method_exists( $product->get_date_on_sale_from(), 'getTimestamp' ) ) {
					if ( $product->get_date_on_sale_from() && gmdate( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) <= gmdate( 'Y-m-d' ) ) {
						if ( method_exists( $product->get_date_on_sale_to(), 'getTimestamp' ) ) {
							//Set the price available until the offer ends
							$price_valid_until = gmdate( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() );
						}
					}
				}
			}

			//Get the product price
			$price = $product->get_price();

			//Get the price with VAT if exists
			if ( function_exists( 'wc_get_price_including_tax' ) && function_exists( 'wc_tax_enabled' ) && function_exists( 'wc_prices_include_tax' ) ) {
				if ( wc_tax_enabled() && ! wc_prices_include_tax() && 'incl' === get_option( 'woocommerce_tax_display_shop' ) ) {
					$price = wc_get_price_including_tax( $product, array( 'price' => $price ) );
				}
			}

			$offer_price = 0;
			if ( function_exists( 'wc_format_decimal' ) && function_exists( 'wc_get_price_decimals' ) ) {
				$offer_price = wc_format_decimal( $price, wc_get_price_decimals() );
			}

			$markup_offer = array(
				'@type'           => 'Offer',
				'price'           => $offer_price,
				'priceValidUntil' => $price_valid_until,
				'url'             => get_permalink( $product->get_id() ),
				'priceCurrency'   => $currency,
				'availability'    => 'https://schema.org/' . $stock = ( $product->is_in_stock() ? 'InStock' : 'OutOfStock' ),
				'sku'             => ( method_exists( $product, 'get_sku' ) ) ? ( $product->get_sku() <> '' ? $product->get_sku() : '' ) : '',
				'image'           => ( method_exists( $product, 'get_image_id' ) ) ? wp_get_attachment_url( $product->get_image_id() ) : '-',
				'description'     => ( method_exists( $product, 'get_description' ) ? $this->cleanText( $product->get_description() ) : $this->cleanText( $product->get_title() ) ),
				'seller'          => array(
					'@type' => 'Organization',
					'name'  => $shop_name,
					'url'   => $shop_url,
				),
			);

			//Get the variation prices
			if ( $product->is_type( 'variable' ) && method_exists( $product, 'get_variation_prices' ) && method_exists( $product, 'get_variation_regular_price' ) && method_exists( $product, 'get_variation_sale_price' ) ) {
				$prices = $product->get_variation_prices();

				if ( isset( $prices['price'] ) ) {
					//Get the product price
					$price    = $product->get_price();
					$minprice = $product->get_variation_price();
					$maxprice = $product->get_variation_price( 'max' );

					//Get the price with VAT if exists
					if ( function_exists( 'wc_get_price_including_tax' ) && function_exists( 'wc_tax_enabled' ) && function_exists( 'wc_prices_include_tax' ) ) {
						if ( wc_tax_enabled() && ! wc_prices_include_tax() && 'incl' === get_option( 'woocommerce_tax_display_shop' ) ) {
							$price    = wc_get_price_including_tax( $product, array( 'price' => $price ) );
							$minprice = wc_get_price_including_tax( $product, array( 'price' => $minprice ) );
							$maxprice = wc_get_price_including_tax( $product, array( 'price' => $maxprice ) );
						}
					}

					if ( function_exists( 'wc_format_decimal' ) && function_exists( 'wc_get_price_decimals' ) ) {
						$markup_offer['priceSpecification'] = array(
							'@type'                 => 'UnitPriceSpecification',
							'price'                 => wc_format_decimal( $price, wc_get_price_decimals() ),
							'minPrice'              => wc_format_decimal( $minprice, wc_get_price_decimals() ),
							'maxPrice'              => wc_format_decimal( $maxprice, wc_get_price_decimals() ),
							'priceCurrency'         => $currency,
							'valueAddedTaxIncluded' => wc_prices_include_tax() ? 'true' : 'false'
						);
					}

					unset( $markup_offer['price'] );
					unset( $markup_offer['priceCurrency'] );
				}
			} elseif ( function_exists( 'wc_prices_include_tax' ) && function_exists( 'wc_format_decimal' ) && function_exists( 'wc_get_price_decimals' ) ) {
				$markup_offer['priceSpecification'] = array(
					'@type'                 => 'UnitPriceSpecification',
					'price'                 => wc_format_decimal( $product->get_price(), wc_get_price_decimals() ),
					'priceCurrency'         => $currency,
					'valueAddedTaxIncluded' => wc_prices_include_tax() ? 'true' : 'false'
				);

				unset( $markup_offer['price'] );
				unset( $markup_offer['priceCurrency'] );
			}

			$markup['sku'] = ( method_exists( $product, 'get_sku' ) ) ? ( $product->get_sku() <> '' ? $product->get_sku() : '' ) : '';


			//Set default values if WooCommerce default is active
			if ( SQ_Classes_Helpers_Tools::getOption( 'sq_jsonld_product_defaults' ) ) {
				//Get all categories
				$categories = $product->get_category_ids();
				if ( ! empty( $categories ) ) {
					foreach ( $categories as $category ) {
						$category = get_term( $category, 'product_cat' );
						if ( isset( $category->name ) && $category->name <> '' ) {
							$markup['brand'] = array(
								'@type' => 'Brand',
								'name'  => $category->name,
							);
						}
					}
				}

				if ( $markup['sku'] == '' ) {
					$markup['sku'] = '-';
				}
				if ( $markup_offer['sku'] == '' ) {
					$markup_offer['sku'] = '-';
				}
				$markup['mpn']       = '-';
				$markup_offer['mpn'] = '-';
			}

			//Set custom values if WooCommerce custom is active
			if ( SQ_Classes_Helpers_Tools::getOption( 'sq_jsonld_product_custom' ) ) {

				foreach ( $wc_fields as $field => $jsonkey ) {
					if ( isset( $sq_woocommerce[ $field ] ) && $sq_woocommerce[ $field ] <> '' ) {
						$markup[ $jsonkey ]       = $sq_woocommerce[ $field ];
						$markup_offer[ $jsonkey ] = $sq_woocommerce[ $field ];
					}
				}

				if ( isset( $sq_woocommerce['brand'] ) && $sq_woocommerce['brand'] <> '' ) {
					$markup['brand'] = array(
						'@type' => 'Brand',
						'name'  => $sq_woocommerce['brand'],
					);
				}
			}


			//Set the offer
			$markup['offers'] = $markup_offer;

			if ( SQ_Classes_Helpers_Tools::getOption( 'sq_jsonld_product_defaults' ) ) {
				//If rating and reviews
				if ( method_exists( $product, 'get_rating_count' ) && $product->get_rating_count() ) {

					//Only if it's set in Squirrly to remove duplicates
					//otherwise let Woocommerce show the reviews
					if ( SQ_Classes_Helpers_Tools::getOption( 'sq_jsonld_clearcode' ) ) {
						$markup['aggregateRating'] = array(
							'@type'       => 'AggregateRating',
							'ratingValue' => $product->get_average_rating(),
							'ratingCount' => $product->get_rating_count(),
							'reviewCount' => $product->get_review_count(),
						);

						//Set the reviews
						$markup['review'] = $this->getProductReviewMarkup( $product );
					}

				} else { //add default data?

					//Add data if no reviews for Google validation
					$markup['aggregateRating'] = array(
						'@type'       => 'AggregateRating',
						'ratingValue' => 5,
						'ratingCount' => 1,
						'reviewCount' => 1,
					);

					$markup['review'][] = array(
						'@type'         => 'Review',
						'reviewRating'  => array(
							'@type'       => 'Rating',
							'ratingValue' => 5,
						),
						'author'        => array(
							'@type' => 'Person',
							'name'  => ( isset( $markup['name'] ) ? $markup['name'] : 'John Doe' ),
						),
						'reviewBody'    => ( isset( $markup['description'] ) ? $markup['description'] : '-' ),
						'datePublished' => ( method_exists( $product, 'get_date_created' ) && $product->get_date_created() && method_exists( $product->get_date_created(), 'getTimestamp' ) ) ? gmdate( 'Y-m-d', $product->get_date_created()->getTimestamp() ) : '',
					);
				}
			}

			$otherbrands = array();

			//compatible with Perfect Woocommerce Brands
			if ( SQ_Classes_Helpers_Tools::isPluginInstalled( 'perfect-woocommerce-brands/perfect-woocommerce-brands.php' ) ) {
				$brands = wp_get_post_terms( $product->get_id(), 'pwb-brand' );
				foreach ( $brands as $brand ) {
					$otherbrands[] = $brand->name;
				}
			}

			//compatible with YITH WooCommerce Brands Add-on
			if ( SQ_Classes_Helpers_Tools::isPluginInstalled( 'yith-woocommerce-brands-add-on/init.php' ) ) {
				$brands = wp_get_post_terms( $product->get_id(), 'yith_product_brand' );
				foreach ( $brands as $brand ) {
					$otherbrands[] = $brand->name;
				}
			}

			if ( ! empty( $otherbrands ) ) {
				$markup['brand'] = $otherbrands;
			}


			return $markup;

		} catch ( Exception $e ) {

		}
	}

	/**
	 * Generates Review structured data.
	 *
	 * @param  $product
	 *
	 * @return array | false
	 */
	public function getProductReviewMarkup( $product ) {
		$markup = array();

		if ( ! method_exists( $product, 'get_id' ) ) {
			return false;
		}

		if ( function_exists( 'wc_review_ratings_enabled' ) && wc_review_ratings_enabled() && function_exists( 'get_comments' ) && function_exists( 'get_comment_meta' ) ) {

			$reviews = get_comments( array(
					'number'      => 10,
					'post_id'     => $product->get_id(),
					'status'      => 'approve',
					'post_status' => 'publish',
					'post_type'   => 'product',
					'parent'      => 0,
					'meta_query'  => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
						array(
							'key'     => 'rating',
							'type'    => 'NUMERIC',
							'compare' => '>',
							'value'   => 0,
						),
					),
				) );

			if ( ! empty( $reviews ) ) {
				foreach ( $reviews as $review ) {
					if ( method_exists( $review, 'comment_ID' ) ) {
						$markup[] = array(
							'@type'         => 'Review',
							'reviewRating'  => array(
								'@type'       => 'Rating',
								'bestRating'  => '5',
								'ratingValue' => get_comment_meta( $review->comment_ID, 'rating', true ),
								'worstRating' => '1',
							),
							'author'        => array(
								'@type' => 'Person',
								'name'  => get_comment_author( $review ),
							),
							'reviewBody'    => get_comment_text( $review ),
							'datePublished' => get_comment_date( 'c', $review ),
						);
					}


				}
			}
		}

		return $markup;

	}

}
