Your IP : 216.73.216.162


Current Path : /home/x/b/o/xbodynamge/namtation/wp-content/
Upload File :
Current File : /home/x/b/o/xbodynamge/namtation/wp-content/generators.tar

open-graph-image-generator.php000066600000013747151123365170012405 0ustar00<?php

namespace Yoast\WP\SEO\Generators;

use Error;
use Yoast\WP\SEO\Context\Meta_Tags_Context;
use Yoast\WP\SEO\Helpers\Image_Helper;
use Yoast\WP\SEO\Helpers\Open_Graph\Image_Helper as Open_Graph_Image_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Url_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Values\Open_Graph\Images;

/**
 * Represents the generator class for the Open Graph images.
 */
class Open_Graph_Image_Generator implements Generator_Interface {

	/**
	 * The Open Graph image helper.
	 *
	 * @var Open_Graph_Image_Helper
	 */
	protected $open_graph_image;

	/**
	 * The image helper.
	 *
	 * @var Image_Helper
	 */
	protected $image;

	/**
	 * The URL helper.
	 *
	 * @var Url_Helper
	 */
	protected $url;

	/**
	 * The options helper.
	 *
	 * @var Options_Helper
	 */
	private $options;

	/**
	 * Images constructor.
	 *
	 * @codeCoverageIgnore
	 *
	 * @param Open_Graph_Image_Helper $open_graph_image Image helper for Open Graph.
	 * @param Image_Helper            $image            The image helper.
	 * @param Options_Helper          $options          The options helper.
	 * @param Url_Helper              $url              The url helper.
	 */
	public function __construct(
		Open_Graph_Image_Helper $open_graph_image,
		Image_Helper $image,
		Options_Helper $options,
		Url_Helper $url
	) {
		$this->open_graph_image = $open_graph_image;
		$this->image            = $image;
		$this->options          = $options;
		$this->url              = $url;
	}

	/**
	 * Retrieves the images for an indexable.
	 *
	 * For legacy reasons some plugins might expect we filter a WPSEO_Opengraph_Image object. That might cause
	 * type errors. This is why we try/catch our filters.
	 *
	 * @param Meta_Tags_Context $context The context.
	 *
	 * @return array The images.
	 */
	public function generate( Meta_Tags_Context $context ) {
		$image_container        = $this->get_image_container();
		$backup_image_container = $this->get_image_container();

		try {
			/**
			 * Filter: wpseo_add_opengraph_images - Allow developers to add images to the Open Graph tags.
			 *
			 * @api Yoast\WP\SEO\Values\Open_Graph\Images The current object.
			 */
			\apply_filters( 'wpseo_add_opengraph_images', $image_container );
		} catch ( Error $error ) {
			$image_container = $backup_image_container;
		}

		$this->add_from_indexable( $context->indexable, $image_container );
		$backup_image_container = $image_container;

		try {
			/**
			 * Filter: wpseo_add_opengraph_additional_images - Allows to add additional images to the Open Graph tags.
			 *
			 * @api Yoast\WP\SEO\Values\Open_Graph\Images The current object.
			 */
			\apply_filters( 'wpseo_add_opengraph_additional_images', $image_container );
		} catch ( Error $error ) {
			$image_container = $backup_image_container;
		}

		$this->add_from_templates( $context, $image_container );
		$this->add_from_default( $image_container );

		return $image_container->get_images();
	}

	/**
	 * Retrieves the images for an author archive indexable.
	 *
	 * This is a custom method to address the case of Author Archives, since they always have an Open Graph image
	 * set in the indexable (even if it is an empty default Gravatar).
	 *
	 * @param Meta_Tags_Context $context The context.
	 *
	 * @return array The images.
	 */
	public function generate_for_author_archive( Meta_Tags_Context $context ) {
		$image_container = $this->get_image_container();

		$this->add_from_templates( $context, $image_container );
		if ( $image_container->has_images() ) {
			return $image_container->get_images();
		}

		return $this->generate( $context );
	}

	/**
	 * Adds an image based on the given indexable.
	 *
	 * @param Indexable $indexable       The indexable.
	 * @param Images    $image_container The image container.
	 */
	protected function add_from_indexable( Indexable $indexable, Images $image_container ) {
		if ( $indexable->open_graph_image_meta ) {
			$image_container->add_image_by_meta( $indexable->open_graph_image_meta );
			return;
		}

		if ( $indexable->open_graph_image_id ) {
			$image_container->add_image_by_id( $indexable->open_graph_image_id );
			return;
		}

		if ( $indexable->open_graph_image ) {
			$meta_data = [];
			if ( $indexable->open_graph_image_meta && \is_string( $indexable->open_graph_image_meta ) ) {
				$meta_data = \json_decode( $indexable->open_graph_image_meta, true );
			}

			$image_container->add_image(
				\array_merge(
					(array) $meta_data,
					[
						'url' => $indexable->open_graph_image,
					]
				)
			);
		}
	}

	/**
	 * Retrieves the default Open Graph image.
	 *
	 * @param Images $image_container The image container.
	 */
	protected function add_from_default( Images $image_container ) {
		if ( $image_container->has_images() ) {
			return;
		}

		$default_image_id = $this->options->get( 'og_default_image_id', '' );
		if ( $default_image_id ) {
			$image_container->add_image_by_id( $default_image_id );

			return;
		}

		$default_image_url = $this->options->get( 'og_default_image', '' );
		if ( $default_image_url ) {
			$image_container->add_image_by_url( $default_image_url );
		}
	}

	/**
	 * Retrieves the default Open Graph image.
	 *
	 * @param Meta_Tags_Context $context         The context.
	 * @param Images            $image_container The image container.
	 */
	protected function add_from_templates( Meta_Tags_Context $context, Images $image_container ) {
		if ( $image_container->has_images() ) {
			return;
		}

		if ( $context->presentation->open_graph_image_id ) {
			$image_container->add_image_by_id( $context->presentation->open_graph_image_id );
			return;
		}

		if ( $context->presentation->open_graph_image ) {
			$image_container->add_image_by_url( $context->presentation->open_graph_image );
		}
	}

	/**
	 * Retrieves an instance of the image container.
	 *
	 * @codeCoverageIgnore
	 *
	 * @return Images The image container.
	 */
	protected function get_image_container() {
		$image_container = new Images( $this->image, $this->url );
		$image_container->set_helpers( $this->open_graph_image );

		return $image_container;
	}
}
breadcrumbs-generator.php000066600000027104151123365170011546 0ustar00<?php

namespace Yoast\WP\SEO\Generators;

use Yoast\WP\SEO\Context\Meta_Tags_Context;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Pagination_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Helpers\Url_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * Represents the generator class for the breadcrumbs.
 */
class Breadcrumbs_Generator implements Generator_Interface {

	/**
	 * The indexable repository.
	 *
	 * @var Indexable_Repository
	 */
	private $repository;

	/**
	 * The options helper.
	 *
	 * @var Options_Helper
	 */
	private $options;

	/**
	 * The current page helper.
	 *
	 * @var Current_Page_Helper
	 */
	private $current_page_helper;

	/**
	 * The post type helper.
	 *
	 * @var Post_Type_Helper
	 */
	private $post_type_helper;

	/**
	 * The URL helper.
	 *
	 * @var Url_Helper
	 */
	private $url_helper;

	/**
	 * The pagination helper.
	 *
	 * @var Pagination_Helper
	 */
	private $pagination_helper;

	/**
	 * Breadcrumbs_Generator constructor.
	 *
	 * @param Indexable_Repository $repository          The repository.
	 * @param Options_Helper       $options             The options helper.
	 * @param Current_Page_Helper  $current_page_helper The current page helper.
	 * @param Post_Type_Helper     $post_type_helper    The post type helper.
	 * @param Url_Helper           $url_helper          The URL helper.
	 * @param Pagination_Helper    $pagination_helper   The pagination helper.
	 */
	public function __construct(
		Indexable_Repository $repository,
		Options_Helper $options,
		Current_Page_Helper $current_page_helper,
		Post_Type_Helper $post_type_helper,
		Url_Helper $url_helper,
		Pagination_Helper $pagination_helper
	) {
		$this->repository          = $repository;
		$this->options             = $options;
		$this->current_page_helper = $current_page_helper;
		$this->post_type_helper    = $post_type_helper;
		$this->url_helper          = $url_helper;
		$this->pagination_helper   = $pagination_helper;
	}

	/**
	 * Generates the breadcrumbs.
	 *
	 * @param Meta_Tags_Context $context The meta tags context.
	 *
	 * @return array An array of associative arrays that each have a 'text' and a 'url'.
	 */
	public function generate( Meta_Tags_Context $context ) {
		$static_ancestors = [];
		$breadcrumbs_home = $this->options->get( 'breadcrumbs-home' );
		if ( $breadcrumbs_home !== '' && ! \in_array( $this->current_page_helper->get_page_type(), [ 'Home_Page', 'Static_Home_Page' ], true ) ) {
			$front_page_id = $this->current_page_helper->get_front_page_id();
			if ( $front_page_id === 0 ) {
				$static_ancestors[] = $this->repository->find_for_home_page();
			}
			else {
				$static_ancestor = $this->repository->find_by_id_and_type( $front_page_id, 'post' );
				if ( $static_ancestor->post_status !== 'unindexed' ) {
					$static_ancestors[] = $static_ancestor;
				}
			}
		}
		$page_for_posts = \get_option( 'page_for_posts' );
		if ( $this->should_have_blog_crumb( $page_for_posts, $context ) ) {
			$static_ancestor = $this->repository->find_by_id_and_type( $page_for_posts, 'post' );
			if ( $static_ancestor->post_status !== 'unindexed' ) {
				$static_ancestors[] = $static_ancestor;
			}
		}
		if (
			$context->indexable->object_type === 'post'
			&& $context->indexable->object_sub_type !== 'post'
			&& $context->indexable->object_sub_type !== 'page'
			&& $this->post_type_helper->has_archive( $context->indexable->object_sub_type )
		) {
			$static_ancestors[] = $this->repository->find_for_post_type_archive( $context->indexable->object_sub_type );
		}
		if ( $context->indexable->object_type === 'term' ) {
			$parent = $this->get_taxonomy_post_type_parent( $context->indexable->object_sub_type );
			if ( $parent && $parent !== 'post' && $this->post_type_helper->has_archive( $parent ) ) {
				$static_ancestors[] = $this->repository->find_for_post_type_archive( $parent );
			}
		}

		// Get all ancestors of the indexable and append itself to get all indexables in the full crumb.
		$indexables   = $this->repository->get_ancestors( $context->indexable );
		$indexables[] = $context->indexable;

		if ( ! empty( $static_ancestors ) ) {
			\array_unshift( $indexables, ...$static_ancestors );
		}

		$indexables = \apply_filters( 'wpseo_breadcrumb_indexables', $indexables, $context );

		$callback = function ( Indexable $ancestor ) {
			$crumb = [
				'url'  => $ancestor->permalink,
				'text' => $ancestor->breadcrumb_title,
			];
			switch ( $ancestor->object_type ) {
				case 'post':
					$crumb = $this->get_post_crumb( $crumb, $ancestor );
					break;
				case 'post-type-archive':
					$crumb = $this->get_post_type_archive_crumb( $crumb, $ancestor );
					break;
				case 'term':
					$crumb = $this->get_term_crumb( $crumb, $ancestor );
					break;
				case 'system-page':
					$crumb = $this->get_system_page_crumb( $crumb, $ancestor );
					break;
				case 'user':
					$crumb = $this->get_user_crumb( $crumb, $ancestor );
					break;
				case 'date-archive':
					$crumb = $this->get_date_archive_crumb( $crumb );
					break;
			}
			return $crumb;
		};
		$crumbs   = \array_map( $callback, $indexables );

		if ( $breadcrumbs_home !== '' ) {
			$crumbs[0]['text'] = $breadcrumbs_home;
		}

		$crumbs = $this->add_paged_crumb( $crumbs, $context->indexable );

		/**
		 * Filter: 'wpseo_breadcrumb_links' - Allow the developer to filter the Yoast SEO breadcrumb links, add to them, change order, etc.
		 *
		 * @param array $crumbs The crumbs array.
		 */
		$filtered_crumbs = \apply_filters( 'wpseo_breadcrumb_links', $crumbs );

		// Basic check to make sure the filtered crumbs are in an array.
		if ( ! \is_array( $filtered_crumbs ) ) {
			\_doing_it_wrong(
				'Filter: \'wpseo_breadcrumb_links\'',
				'The `wpseo_breadcrumb_links` filter should return a multi-dimensional array.',
				'YoastSEO v20.0'
			);
		}
		else {
			$crumbs = $filtered_crumbs;
		}

		$filter_callback = static function( $link_info, $index ) use ( $crumbs ) {
			/**
			 * Filter: 'wpseo_breadcrumb_single_link_info' - Allow developers to filter the Yoast SEO Breadcrumb link information.
			 *
			 * @api array $link_info The breadcrumb link information.
			 *
			 * @param int $index The index of the breadcrumb in the list.
			 * @param array $crumbs The complete list of breadcrumbs.
			 */
			return \apply_filters( 'wpseo_breadcrumb_single_link_info', $link_info, $index, $crumbs );
		};
		return \array_map( $filter_callback, $crumbs, \array_keys( $crumbs ) );
	}

	/**
	 * Returns the modified post crumb.
	 *
	 * @param array     $crumb    The crumb.
	 * @param Indexable $ancestor The indexable.
	 *
	 * @return array The crumb.
	 */
	private function get_post_crumb( $crumb, $ancestor ) {
		$crumb['id'] = $ancestor->object_id;

		return $crumb;
	}

	/**
	 * Returns the modified post type crumb.
	 *
	 * @param array     $crumb    The crumb.
	 * @param Indexable $ancestor The indexable.
	 *
	 * @return array The crumb.
	 */
	private function get_post_type_archive_crumb( $crumb, $ancestor ) {
		$crumb['ptarchive'] = $ancestor->object_sub_type;

		return $crumb;
	}

	/**
	 * Returns the modified term crumb.
	 *
	 * @param array     $crumb    The crumb.
	 * @param Indexable $ancestor The indexable.
	 *
	 * @return array The crumb.
	 */
	private function get_term_crumb( $crumb, $ancestor ) {
		$crumb['term_id']  = $ancestor->object_id;
		$crumb['taxonomy'] = $ancestor->object_sub_type;

		return $crumb;
	}

	/**
	 * Returns the modified system page crumb.
	 *
	 * @param array     $crumb    The crumb.
	 * @param Indexable $ancestor The indexable.
	 *
	 * @return array The crumb.
	 */
	private function get_system_page_crumb( $crumb, $ancestor ) {
		if ( $ancestor->object_sub_type === 'search-result' ) {
			$crumb['text'] = $this->options->get( 'breadcrumbs-searchprefix' ) . ' ' . \esc_html( \get_search_query() );
			$crumb['url']  = \get_search_link();
		}
		elseif ( $ancestor->object_sub_type === '404' ) {
			$crumb['text'] = $this->options->get( 'breadcrumbs-404crumb' );
		}

		return $crumb;
	}

	/**
	 * Returns the modified user crumb.
	 *
	 * @param array     $crumb    The crumb.
	 * @param Indexable $ancestor The indexable.
	 *
	 * @return array The crumb.
	 */
	private function get_user_crumb( $crumb, $ancestor ) {
		$display_name  = \get_the_author_meta( 'display_name', $ancestor->object_id );
		$crumb['text'] = $this->options->get( 'breadcrumbs-archiveprefix' ) . ' ' . $display_name;

		return $crumb;
	}

	/**
	 * Returns the modified date archive crumb.
	 *
	 * @param array $crumb The crumb.
	 *
	 * @return array The crumb.
	 */
	protected function get_date_archive_crumb( $crumb ) {
		$home_url = $this->url_helper->home();
		$prefix   = $this->options->get( 'breadcrumbs-archiveprefix' );

		if ( \is_day() ) {
			$day           = \esc_html( \get_the_date() );
			$crumb['url']  = $home_url . \get_the_date( 'Y/m/d' ) . '/';
			$crumb['text'] = $prefix . ' ' . $day;
		}
		elseif ( \is_month() ) {
			$month         = \esc_html( \trim( \single_month_title( ' ', false ) ) );
			$crumb['url']  = $home_url . \get_the_date( 'Y/m' ) . '/';
			$crumb['text'] = $prefix . ' ' . $month;
		}
		elseif ( \is_year() ) {
			$year          = \get_the_date( 'Y' );
			$crumb['url']  = $home_url . $year . '/';
			$crumb['text'] = $prefix . ' ' . $year;
		}

		return $crumb;
	}

	/**
	 * Returns whether or not a blog crumb should be added.
	 *
	 * @param int               $page_for_posts The page for posts ID.
	 * @param Meta_Tags_Context $context        The meta tags context.
	 *
	 * @return bool Whether or not a blog crumb should be added.
	 */
	protected function should_have_blog_crumb( $page_for_posts, $context ) {
		// When there is no page configured as blog page.
		if ( \get_option( 'show_on_front' ) !== 'page' || ! $page_for_posts ) {
			return false;
		}

		if ( $context->indexable->object_type === 'term' ) {
			$parent = $this->get_taxonomy_post_type_parent( $context->indexable->object_sub_type );
			return $parent === 'post';
		}

		if ( $this->options->get( 'breadcrumbs-display-blog-page' ) !== true ) {
			return false;
		}

		// When the current page is the home page, searchpage or isn't a singular post.
		if ( \is_home() || \is_search() || ! \is_singular( 'post' ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Returns the post type parent of a given taxonomy.
	 *
	 * @param string $taxonomy The taxonomy.
	 *
	 * @return string|false The parent if it exists, false otherwise.
	 */
	protected function get_taxonomy_post_type_parent( $taxonomy ) {
		$parent = $this->options->get( 'taxonomy-' . $taxonomy . '-ptparent' );

		if ( empty( $parent ) || (string) $parent === '0' ) {
			return false;
		}

		return $parent;
	}

	/**
	 * Adds a crumb for the current page, if we're on an archive page or paginated post.
	 *
	 * @param array     $crumbs            The array of breadcrumbs.
	 * @param Indexable $current_indexable The current indexable.
	 *
	 * @return array The breadcrumbs.
	 */
	protected function add_paged_crumb( array $crumbs, $current_indexable ) {
		$is_simple_page = $this->current_page_helper->is_simple_page();

		// If we're not on a paged page do nothing.
		if ( ! $is_simple_page && ! $this->current_page_helper->is_paged() ) {
			return $crumbs;
		}

		// If we're not on a paginated post do nothing.
		if ( $is_simple_page && $current_indexable->number_of_pages === null ) {
			return $crumbs;
		}

		$current_page_number = $this->pagination_helper->get_current_page_number();
		if ( $current_page_number <= 1 ) {
			return $crumbs;
		}

		$crumbs[] = [
			'text' => \sprintf(
				/* translators: %s expands to the current page number */
				\__( 'Page %s', 'wordpress-seo' ),
				$current_page_number
			),
		];

		return $crumbs;
	}
}
twitter-image-generator.php000066600000004553151123365170012042 0ustar00<?php

namespace Yoast\WP\SEO\Generators;

use Yoast\WP\SEO\Context\Meta_Tags_Context;
use Yoast\WP\SEO\Helpers\Image_Helper;
use Yoast\WP\SEO\Helpers\Twitter\Image_Helper as Twitter_Image_Helper;
use Yoast\WP\SEO\Helpers\Url_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Values\Images;

/**
 * Represents the generator class for the Twitter images.
 */
class Twitter_Image_Generator implements Generator_Interface {

	/**
	 * The image helper.
	 *
	 * @var Image_Helper
	 */
	protected $image;

	/**
	 * The URL helper.
	 *
	 * @var Url_Helper
	 */
	protected $url;

	/**
	 * The Twitter image helper.
	 *
	 * @var Twitter_Image_Helper
	 */
	protected $twitter_image;

	/**
	 * Twitter_Image_Generator constructor.
	 *
	 * @codeCoverageIgnore
	 *
	 * @param Image_Helper         $image         The image helper.
	 * @param Url_Helper           $url           The url helper.
	 * @param Twitter_Image_Helper $twitter_image The Twitter image helper.
	 */
	public function __construct( Image_Helper $image, Url_Helper $url, Twitter_Image_Helper $twitter_image ) {
		$this->image         = $image;
		$this->url           = $url;
		$this->twitter_image = $twitter_image;
	}

	/**
	 * Retrieves the images for an indexable.
	 *
	 * @param Meta_Tags_Context $context The context.
	 *
	 * @return array The images.
	 */
	public function generate( Meta_Tags_Context $context ) {
		$image_container = $this->get_image_container();

		$this->add_from_indexable( $context->indexable, $image_container );

		return $image_container->get_images();
	}

	/**
	 * Adds an image based on the given indexable.
	 *
	 * @param Indexable $indexable       The indexable.
	 * @param Images    $image_container The image container.
	 */
	protected function add_from_indexable( Indexable $indexable, Images $image_container ) {
		if ( $indexable->twitter_image_id ) {
			$image_container->add_image_by_id( $indexable->twitter_image_id );
			return;
		}

		if ( $indexable->twitter_image ) {
			$image_container->add_image_by_url( $indexable->twitter_image );
		}
	}

	/**
	 * Retrieves an instance of the image container.
	 *
	 * @codeCoverageIgnore
	 *
	 * @return Images The image container.
	 */
	protected function get_image_container() {
		$image_container             = new Images( $this->image, $this->url );
		$image_container->image_size = $this->twitter_image->get_image_size();

		return $image_container;
	}
}
schema/breadcrumb.php000066600000012070151123365170010633 0ustar00<?php

namespace Yoast\WP\SEO\Generators\Schema;

use Yoast\WP\SEO\Config\Schema_IDs;

/**
 * Returns schema Breadcrumb data.
 */
class Breadcrumb extends Abstract_Schema_Piece {

	/**
	 * Determine if we should add a breadcrumb attribute.
	 *
	 * @return bool
	 */
	public function is_needed() {
		if ( $this->context->indexable->object_type === 'unknown' ) {
			return false;
		}

		if ( $this->context->indexable->object_type === 'system-page' && $this->context->indexable->object_sub_type === '404' ) {
			return false;
		}

		return true;
	}

	/**
	 * Returns Schema breadcrumb data to allow recognition of page's position in the site hierarchy.
	 *
	 * @link https://developers.google.com/search/docs/data-types/breadcrumb
	 *
	 * @return bool|array Array on success, false on failure.
	 */
	public function generate() {
		$breadcrumbs   = $this->context->presentation->breadcrumbs;
		$list_elements = [];

		// In case of pagination, replace the last breadcrumb, because it only contains "Page [number]" and has no URL.
		if (
			(
				$this->helpers->current_page->is_paged()
				|| $this->context->indexable->number_of_pages > 1
			) && (
				// Do not replace the last breadcrumb on static post pages.
				! $this->helpers->current_page->is_static_posts_page()
				// Do not remove the last breadcrumb if only one exists (bugfix for custom paginated frontpages).
				&& \count( $breadcrumbs ) > 1
			)
		) {
			\array_pop( $breadcrumbs );
		}

		// Only output breadcrumbs that are not hidden.
		$breadcrumbs = \array_filter( $breadcrumbs, [ $this, 'not_hidden' ] );

		\reset( $breadcrumbs );

		/*
		 * Check whether at least one of the breadcrumbs is broken.
		 * If so, do not output anything.
		 */
		foreach ( $breadcrumbs as $breadcrumb ) {
			if ( $this->is_broken( $breadcrumb ) ) {
				return false;
			}
		}

		// Create the last breadcrumb.
		$last_breadcrumb = \array_pop( $breadcrumbs );
		$breadcrumbs[]   = $this->format_last_breadcrumb( $last_breadcrumb );

		// If this is a static front page, prevent nested pages from creating a trail.
		if ( $this->helpers->current_page->is_home_static_page() ) {

			// Check if we're dealing with a nested page.
			if ( \count( $breadcrumbs ) > 1 ) {

				// Store the breadcrumbs home variable before dropping the parent page from the Schema.
				$breadcrumbs_home = $breadcrumbs[0]['text'];
				$breadcrumbs      = [ \array_pop( $breadcrumbs ) ];

				// Make the child page show the breadcrumbs home variable rather than its own title.
				$breadcrumbs[0]['text'] = $breadcrumbs_home;
			}
		}

		$breadcrumbs = \array_filter( $breadcrumbs, [ $this, 'not_empty_text' ] );
		$breadcrumbs = \array_values( $breadcrumbs );

		// Create intermediate breadcrumbs.
		foreach ( $breadcrumbs as $index => $breadcrumb ) {
			$list_elements[] = $this->create_breadcrumb( $index, $breadcrumb );
		}

		return [
			'@type'           => 'BreadcrumbList',
			'@id'             => $this->context->canonical . Schema_IDs::BREADCRUMB_HASH,
			'itemListElement' => $list_elements,
		];
	}

	/**
	 * Returns a breadcrumb array.
	 *
	 * @param int   $index      The position in the list.
	 * @param array $breadcrumb The position in the list.
	 *
	 * @return array A breadcrumb listItem.
	 */
	private function create_breadcrumb( $index, $breadcrumb ) {
		$crumb = [
			'@type'    => 'ListItem',
			'position' => ( $index + 1 ),
			'name'     => $this->helpers->schema->html->smart_strip_tags( $breadcrumb['text'] ),
		];

		if ( ! empty( $breadcrumb['url'] ) ) {
			$crumb['item'] = $breadcrumb['url'];
		}

		return $crumb;
	}

	/**
	 * Creates the last breadcrumb in the breadcrumb list, omitting the URL per Google's spec.
	 *
	 * @link https://developers.google.com/search/docs/data-types/breadcrumb
	 *
	 * @param array $breadcrumb The position in the list.
	 *
	 * @return array The last of the breadcrumbs.
	 */
	private function format_last_breadcrumb( $breadcrumb ) {
		unset( $breadcrumb['url'] );

		return $breadcrumb;
	}

	/**
	 * Tests if the breadcrumb is broken.
	 * A breadcrumb is considered broken:
	 * - when it is not an array.
	 * - when it has no URL or text.
	 *
	 * @param array $breadcrumb The breadcrumb to test.
	 *
	 * @return bool `true` if the breadcrumb is broken.
	 */
	private function is_broken( $breadcrumb ) {
		// A breadcrumb is broken if it is not an array.
		if ( ! \is_array( $breadcrumb ) ) {
			return true;
		}

		// A breadcrumb is broken if it does not contain a URL or text.
		if ( ! \array_key_exists( 'url', $breadcrumb ) || ! \array_key_exists( 'text', $breadcrumb ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Checks whether the breadcrumb is not set to be hidden.
	 *
	 * @param array $breadcrumb The breadcrumb array.
	 *
	 * @return bool If the breadcrumb should not be hidden.
	 */
	private function not_hidden( $breadcrumb ) {
		return empty( $breadcrumb['hide_in_schema'] );
	}

	/**
	 * Checks whether the breadcrumb has a not empty text.
	 *
	 * @param array $breadcrumb The breadcrumb array.
	 *
	 * @return bool If the breadcrumb has a not empty text.
	 */
	private function not_empty_text( $breadcrumb ) {
		return ! empty( $breadcrumb['text'] );
	}
}
schema/main-image.php000066600000002216151123365170010532 0ustar00<?php

namespace Yoast\WP\SEO\Generators\Schema;

use Yoast\WP\SEO\Config\Schema_IDs;

/**
 * Returns ImageObject schema data.
 */
class Main_Image extends Abstract_Schema_Piece {

	/**
	 * Determines whether or not a piece should be added to the graph.
	 *
	 * @return bool
	 */
	public function is_needed() {
		return true;
	}

	/**
	 * Adds a main image for the current URL to the schema if there is one.
	 *
	 * This can be either the featured image or the first image in the content of the page.
	 *
	 * @return false|array Image Schema.
	 */
	public function generate() {
		$image_id = $this->context->canonical . Schema_IDs::PRIMARY_IMAGE_HASH;

		// The featured image.
		if ( $this->context->main_image_id ) {
			$generated_schema              = $this->helpers->schema->image->generate_from_attachment_id( $image_id, $this->context->main_image_id );
			$this->context->main_image_url = $generated_schema['url'];

			return $generated_schema;
		}

		// The first image in the content.
		if ( $this->context->main_image_url ) {
			return $this->helpers->schema->image->generate_from_url( $image_id, $this->context->main_image_url );
		}

		return false;
	}
}
schema/howto.php000066600000011644151123365170007673 0ustar00<?php

namespace Yoast\WP\SEO\Generators\Schema;

use Yoast\WP\SEO\Config\Schema_IDs;

/**
 * Returns schema HowTo data.
 */
class HowTo extends Abstract_Schema_Piece {

	/**
	 * Determines whether or not a piece should be added to the graph.
	 *
	 * @return bool
	 */
	public function is_needed() {
		return ! empty( $this->context->blocks['yoast/how-to-block'] );
	}

	/**
	 * Renders a list of questions, referencing them by ID.
	 *
	 * @return array Our Schema graph.
	 */
	public function generate() {
		$graph = [];

		foreach ( $this->context->blocks['yoast/how-to-block'] as $index => $block ) {
			$this->add_how_to( $graph, $block, $index );
		}

		return $graph;
	}

	/**
	 * Adds the duration of the task to the Schema.
	 *
	 * @param array $data       Our How-To schema data.
	 * @param array $attributes The block data attributes.
	 */
	private function add_duration( &$data, $attributes ) {
		if ( empty( $attributes['hasDuration'] ) ) {
			return;
		}

		$days    = empty( $attributes['days'] ) ? 0 : $attributes['days'];
		$hours   = empty( $attributes['hours'] ) ? 0 : $attributes['hours'];
		$minutes = empty( $attributes['minutes'] ) ? 0 : $attributes['minutes'];

		if ( ( $days + $hours + $minutes ) > 0 ) {
			$data['totalTime'] = \esc_attr( 'P' . $days . 'DT' . $hours . 'H' . $minutes . 'M' );
		}
	}

	/**
	 * Adds the steps to our How-To output.
	 *
	 * @param array $data  Our How-To schema data.
	 * @param array $steps Our How-To block's steps.
	 */
	private function add_steps( &$data, $steps ) {
		foreach ( $steps as $step ) {
			$schema_id   = $this->context->canonical . '#' . \esc_attr( $step['id'] );
			$schema_step = [
				'@type' => 'HowToStep',
				'url'   => $schema_id,
			];

			if ( isset( $step['jsonText'] ) ) {
				$json_text = $this->helpers->schema->html->sanitize( $step['jsonText'] );
			}

			if ( isset( $step['jsonName'] ) ) {
				$json_name = $this->helpers->schema->html->smart_strip_tags( $step['jsonName'] );
			}

			if ( empty( $json_name ) ) {
				if ( empty( $step['text'] ) ) {
					continue;
				}

				$schema_step['text'] = '';

				$this->add_step_image( $schema_step, $step );

				// If there is no text and no image, don't output the step.
				if ( empty( $json_text ) && empty( $schema_step['image'] ) ) {
					continue;
				}

				if ( ! empty( $json_text ) ) {
					$schema_step['text'] = $json_text;
				}
			}

			elseif ( empty( $json_text ) ) {
				$schema_step['text'] = $json_name;
			}
			else {
				$schema_step['name'] = $json_name;

				$this->add_step_description( $schema_step, $json_text );
				$this->add_step_image( $schema_step, $step );
			}

			$data['step'][] = $schema_step;
		}
	}

	/**
	 * Checks if we have a step description, if we do, add it.
	 *
	 * @param array  $schema_step Our Schema output for the Step.
	 * @param string $json_text   The step text.
	 */
	private function add_step_description( &$schema_step, $json_text ) {
		$schema_step['itemListElement'] = [
			[
				'@type' => 'HowToDirection',
				'text'  => $json_text,
			],
		];
	}

	/**
	 * Checks if we have a step image, if we do, add it.
	 *
	 * @param array $schema_step Our Schema output for the Step.
	 * @param array $step        The step block data.
	 */
	private function add_step_image( &$schema_step, $step ) {
		foreach ( $step['text'] as $line ) {
			if ( \is_array( $line ) && isset( $line['type'] ) && $line['type'] === 'img' ) {
				$schema_step['image'] = $this->get_image_schema( \esc_url( $line['props']['src'] ) );
			}
		}
	}

	/**
	 * Generates the HowTo schema for a block.
	 *
	 * @param array $graph Our Schema data.
	 * @param array $block The How-To block content.
	 * @param int   $index The index of the current block.
	 */
	protected function add_how_to( &$graph, $block, $index ) {
		$data = [
			'@type'            => 'HowTo',
			'@id'              => $this->context->canonical . '#howto-' . ( $index + 1 ),
			'name'             => $this->helpers->schema->html->smart_strip_tags( $this->helpers->post->get_post_title_with_fallback( $this->context->id ) ),
			'mainEntityOfPage' => [ '@id' => $this->context->main_schema_id ],
			'description'      => '',
		];

		if ( $this->context->has_article ) {
			$data['mainEntityOfPage'] = [ '@id' => $this->context->main_schema_id . Schema_IDs::ARTICLE_HASH ];
		}

		if ( isset( $block['attrs']['jsonDescription'] ) ) {
			$data['description'] = $this->helpers->schema->html->sanitize( $block['attrs']['jsonDescription'] );
		}

		$this->add_duration( $data, $block['attrs'] );
		$this->add_steps( $data, $block['attrs']['steps'] );

		$data = $this->helpers->schema->language->add_piece_language( $data );

		$graph[] = $data;
	}

	/**
	 * Generates the image schema from the attachment $url.
	 *
	 * @param string $url Attachment url.
	 *
	 * @return array Image schema.
	 */
	protected function get_image_schema( $url ) {
		$schema_id = $this->context->canonical . '#schema-image-' . \md5( $url );

		return $this->helpers->schema->image->generate_from_url( $schema_id, $url );
	}
}
schema/website.php000066600000005151151123365170010171 0ustar00<?php

namespace Yoast\WP\SEO\Generators\Schema;

use Yoast\WP\SEO\Config\Schema_IDs;

/**
 * Returns schema Website data.
 */
class Website extends Abstract_Schema_Piece {

	/**
	 * Determines whether or not a piece should be added to the graph.
	 *
	 * @return bool
	 */
	public function is_needed() {
		return true;
	}

	/**
	 * Outputs code to allow recognition of the internal search engine.
	 *
	 * @return array Website data blob.
	 */
	public function generate() {
		$data = [
			'@type'       => 'WebSite',
			'@id'         => $this->context->site_url . Schema_IDs::WEBSITE_HASH,
			'url'         => $this->context->site_url,
			'name'        => $this->helpers->schema->html->smart_strip_tags( $this->context->site_name ),
			'description' => \get_bloginfo( 'description' ),
		];

		if ( $this->context->site_represents_reference ) {
			$data['publisher'] = $this->context->site_represents_reference;
		}

		$data = $this->add_alternate_name( $data );
		$data = $this->internal_search_section( $data );
		$data = $this->helpers->schema->language->add_piece_language( $data );

		return $data;
	}

	/**
	 * Returns an alternate name if one was specified in the Yoast SEO settings.
	 *
	 * @param array $data The website data array.
	 *
	 * @return array
	 */
	private function add_alternate_name( $data ) {
		if ( $this->context->alternate_site_name !== '' ) {
			$data['alternateName'] = $this->helpers->schema->html->smart_strip_tags( $this->context->alternate_site_name );
		}

		return $data;
	}

	/**
	 * Adds the internal search JSON LD code to the homepage if it's not disabled.
	 *
	 * @link https://developers.google.com/search/docs/data-types/sitelinks-searchbox
	 *
	 * @param array $data The website data array.
	 *
	 * @return array
	 */
	private function internal_search_section( $data ) {
		/**
		 * Filter: 'disable_wpseo_json_ld_search' - Allow disabling of the json+ld output.
		 *
		 * @api bool $display_search Whether or not to display json+ld search on the frontend.
		 */
		if ( \apply_filters( 'disable_wpseo_json_ld_search', false ) ) {
			return $data;
		}

		/**
		 * Filter: 'wpseo_json_ld_search_url' - Allows filtering of the search URL for Yoast SEO.
		 *
		 * @api string $search_url The search URL for this site with a `{search_term_string}` variable.
		 */
		$search_url = \apply_filters( 'wpseo_json_ld_search_url', $this->context->site_url . '?s={search_term_string}' );

		$data['potentialAction'][] = [
			'@type'       => 'SearchAction',
			'target'      => [
				'@type'       => 'EntryPoint',
				'urlTemplate' => $search_url,
			],
			'query-input' => 'required name=search_term_string',
		];

		return $data;
	}
}
schema/abstract-schema-piece.php000066600000001145151123365170012652 0ustar00<?php

namespace Yoast\WP\SEO\Generators\Schema;

use Yoast\WP\SEO\Context\Meta_Tags_Context;
use Yoast\WP\SEO\Surfaces\Helpers_Surface;

/**
 * Class Abstract_Schema_Piece.
 */
abstract class Abstract_Schema_Piece {

	/**
	 * The meta tags context.
	 *
	 * @var Meta_Tags_Context
	 */
	public $context;

	/**
	 * The helpers surface
	 *
	 * @var Helpers_Surface
	 */
	public $helpers;

	/**
	 * Generates the schema piece.
	 *
	 * @return mixed
	 */
	abstract public function generate();

	/**
	 * Determines whether the schema piece is needed.
	 *
	 * @return bool
	 */
	abstract public function is_needed();
}
schema/organization.php000066600000005012151123365170011227 0ustar00<?php

namespace Yoast\WP\SEO\Generators\Schema;

use Yoast\WP\SEO\Config\Schema_IDs;

/**
 * Returns schema Organization data.
 */
class Organization extends Abstract_Schema_Piece {

	/**
	 * Determines whether an Organization graph piece should be added.
	 *
	 * @return bool
	 */
	public function is_needed() {
		return $this->context->site_represents === 'company';
	}

	/**
	 * Returns the Organization Schema data.
	 *
	 * @return array The Organization schema.
	 */
	public function generate() {
		$logo_schema_id = $this->context->site_url . Schema_IDs::ORGANIZATION_LOGO_HASH;

		if ( $this->context->company_logo_meta ) {
			$logo = $this->helpers->schema->image->generate_from_attachment_meta( $logo_schema_id, $this->context->company_logo_meta, $this->context->company_name );
		}
		else {
			$logo = $this->helpers->schema->image->generate_from_attachment_id( $logo_schema_id, $this->context->company_logo_id, $this->context->company_name );
		}

		$organization = [
			'@type' => 'Organization',
			'@id'   => $this->context->site_url . Schema_IDs::ORGANIZATION_HASH,
			'name'  => $this->helpers->schema->html->smart_strip_tags( $this->context->company_name ),
		];

		if ( ! empty( $this->context->company_alternate_name ) ) {
			$organization['alternateName'] = $this->context->company_alternate_name;
		}

		$organization['url']   = $this->context->site_url;
		$organization['logo']  = $logo;
		$organization['image'] = [ '@id' => $logo['@id'] ];

		$same_as = \array_values( \array_unique( $this->fetch_social_profiles() ) );
		if ( ! empty( $same_as ) ) {
			$organization['sameAs'] = $same_as;
		}

		return $organization;
	}

	/**
	 * Retrieve the social profiles to display in the organization schema.
	 *
	 * @return array An array of social profiles.
	 */
	private function fetch_social_profiles() {
		$social_profiles = $this->helpers->options->get( 'other_social_urls', [] );
		$profiles        = \array_map( '\urldecode', \array_filter( $social_profiles ) );

		$facebook = $this->helpers->options->get( 'facebook_site', '' );
		if ( $facebook !== '' ) {
			$profiles[] = \urldecode( $facebook );
		}

		$twitter = $this->helpers->options->get( 'twitter_site', '' );
		if ( $twitter !== '' ) {
			$profiles[] = 'https://twitter.com/' . $twitter;
		}

		/**
		 * Filter: 'wpseo_schema_organization_social_profiles' - Allows filtering social profiles for the
		 * represented organization.
		 *
		 * @api string[] $profiles
		 */
		$profiles = \apply_filters( 'wpseo_schema_organization_social_profiles', $profiles );

		return $profiles;
	}
}
schema/author.php000066600000005710151123365170010032 0ustar00<?php

namespace Yoast\WP\SEO\Generators\Schema;

/**
 * Returns schema Author data.
 */
class Author extends Person {

	/**
	 * Determine whether we should return Person schema.
	 *
	 * @return bool
	 */
	public function is_needed() {
		if ( $this->context->indexable->object_type === 'user' ) {
			return true;
		}

		if (
			$this->context->indexable->object_type === 'post'
			&& $this->helpers->schema->article->is_author_supported( $this->context->indexable->object_sub_type )
			&& $this->context->schema_article_type !== 'None'
		) {
			return true;
		}

		return false;
	}

	/**
	 * Returns Person Schema data.
	 *
	 * @return bool|array Person data on success, false on failure.
	 */
	public function generate() {
		$user_id = $this->determine_user_id();
		if ( ! $user_id ) {
			return false;
		}

		$data = $this->build_person_data( $user_id );

		if ( $this->site_represents_current_author() === false ) {
			$data['@type'] = [ 'Person' ];
			unset( $data['logo'] );
		}

		// If this is an author page, the Person object is the main object, so we set it as such here.
		if ( $this->context->indexable->object_type === 'user' ) {
			$data['mainEntityOfPage'] = [
				'@id' => $this->context->main_schema_id,
			];
		}

		// If this is a post and the author archives are enabled, set the author archive url as the author url.
		if ( $this->context->indexable->object_type === 'post' ) {
			if ( $this->helpers->options->get( 'disable-author' ) !== true ) {
				$data['url'] = $this->helpers->user->get_the_author_posts_url( $user_id );
			}
		}

		return $data;
	}

	/**
	 * Determines a User ID for the Person data.
	 *
	 * @return bool|int User ID or false upon return.
	 */
	protected function determine_user_id() {
		$user_id = 0;

		if ( $this->context->indexable->object_type === 'post' ) {
			$user_id = (int) $this->context->post->post_author;
		}

		if ( $this->context->indexable->object_type === 'user' ) {
			$user_id = $this->context->indexable->object_id;
		}

		/**
		 * Filter: 'wpseo_schema_person_user_id' - Allows filtering of user ID used for person output.
		 *
		 * @api int|bool $user_id The user ID currently determined.
		 */
		$user_id = \apply_filters( 'wpseo_schema_person_user_id', $user_id );

		if ( \is_int( $user_id ) && $user_id > 0 ) {
			return $user_id;
		}

		return false;
	}

	/**
	 * An author should not have an image from options, this only applies to persons.
	 *
	 * @param array   $data      The Person schema.
	 * @param string  $schema_id The string used in the `@id` for the schema.
	 * @param bool    $add_hash  Whether or not the person's image url hash should be added to the image id.
	 * @param WP_User $user_data User data.
	 *
	 * @return array The Person schema.
	 */
	protected function set_image_from_options( $data, $schema_id, $add_hash = false, $user_data = null ) {
		if ( $this->site_represents_current_author( $user_data ) ) {
			return parent::set_image_from_options( $data, $schema_id, $add_hash, $user_data );
		}

		return $data;
	}
}
schema/person.php000066600000022133151123365170010034 0ustar00<?php

namespace Yoast\WP\SEO\Generators\Schema;

use WP_User;
use Yoast\WP\SEO\Config\Schema_IDs;

/**
 * Returns schema Person data.
 */
class Person extends Abstract_Schema_Piece {

	/**
	 * Array of the social profiles we display for a Person.
	 *
	 * @var string[]
	 */
	private $social_profiles = [
		'facebook',
		'instagram',
		'linkedin',
		'pinterest',
		'twitter',
		'myspace',
		'youtube',
		'soundcloud',
		'tumblr',
		'wikipedia',
	];

	/**
	 * The Schema type we use for this class.
	 *
	 * @var string[]
	 */
	protected $type = [ 'Person', 'Organization' ];

	/**
	 * Determine whether we should return Person schema.
	 *
	 * @return bool
	 */
	public function is_needed() {
		// Using an author piece instead.
		if ( $this->site_represents_current_author() ) {
			return false;
		}

		return $this->context->site_represents === 'person' || $this->context->indexable->object_type === 'user';
	}

	/**
	 * Returns Person Schema data.
	 *
	 * @return bool|array Person data on success, false on failure.
	 */
	public function generate() {
		$user_id = $this->determine_user_id();
		if ( ! $user_id ) {
			return false;
		}

		return $this->build_person_data( $user_id );
	}

	/**
	 * Determines a User ID for the Person data.
	 *
	 * @return bool|int User ID or false upon return.
	 */
	protected function determine_user_id() {
		/**
		 * Filter: 'wpseo_schema_person_user_id' - Allows filtering of user ID used for person output.
		 *
		 * @api int|bool $user_id The user ID currently determined.
		 */
		$user_id = \apply_filters( 'wpseo_schema_person_user_id', $this->context->site_user_id );

		// It should to be an integer higher than 0.
		if ( \is_int( $user_id ) && $user_id > 0 ) {
			return $user_id;
		}

		return false;
	}

	/**
	 * Retrieve a list of social profile URLs for Person.
	 *
	 * @param array $same_as_urls Array of SameAs URLs.
	 * @param int   $user_id      User ID.
	 *
	 * @return string[] A list of SameAs URLs.
	 */
	protected function get_social_profiles( $same_as_urls, $user_id ) {
		/**
		 * Filter: 'wpseo_schema_person_social_profiles' - Allows filtering of social profiles per user.
		 *
		 * @param int $user_id The current user we're grabbing social profiles for.
		 *
		 * @api string[] $social_profiles The array of social profiles to retrieve. Each should be a user meta field
		 *                                key. As they are retrieved using the WordPress function `get_the_author_meta`.
		 */
		$social_profiles = \apply_filters( 'wpseo_schema_person_social_profiles', $this->social_profiles, $user_id );

		// We can only handle an array.
		if ( ! \is_array( $social_profiles ) ) {
			return $same_as_urls;
		}

		foreach ( $social_profiles as $profile ) {
			// Skip non-string values.
			if ( ! \is_string( $profile ) ) {
				continue;
			}

			$social_url = $this->url_social_site( $profile, $user_id );
			if ( $social_url ) {
				$same_as_urls[] = $social_url;
			}
		}

		return $same_as_urls;
	}

	/**
	 * Builds our array of Schema Person data for a given user ID.
	 *
	 * @param int  $user_id  The user ID to use.
	 * @param bool $add_hash Wether or not the person's image url hash should be added to the image id.
	 *
	 * @return array An array of Schema Person data.
	 */
	protected function build_person_data( $user_id, $add_hash = false ) {
		$user_data = \get_userdata( $user_id );
		$data      = [
			'@type' => $this->type,
			'@id'   => $this->helpers->schema->id->get_user_schema_id( $user_id, $this->context ),
		];

		// Safety check for the `get_userdata` WP function, which could return false.
		if ( $user_data === false ) {
			return $data;
		}

		$data['name'] = $this->helpers->schema->html->smart_strip_tags( $user_data->display_name );
		$data         = $this->add_image( $data, $user_data, $add_hash );

		if ( ! empty( $user_data->description ) ) {
			$data['description'] = $this->helpers->schema->html->smart_strip_tags( $user_data->description );
		}

		$data = $this->add_same_as_urls( $data, $user_data, $user_id );

		/**
		 * Filter: 'wpseo_schema_person_data' - Allows filtering of schema data per user.
		 *
		 * @param array $data    The schema data we have for this person.
		 * @param int   $user_id The current user we're collecting schema data for.
		 */
		$data = \apply_filters( 'wpseo_schema_person_data', $data, $user_id );

		return $data;
	}

	/**
	 * Returns an ImageObject for the persons avatar.
	 *
	 * @param array   $data      The Person schema.
	 * @param WP_User $user_data User data.
	 * @param bool    $add_hash  Wether or not the person's image url hash should be added to the image id.
	 *
	 * @return array The Person schema.
	 */
	protected function add_image( $data, $user_data, $add_hash = false ) {
		$schema_id = $this->context->site_url . Schema_IDs::PERSON_LOGO_HASH;

		$data = $this->set_image_from_options( $data, $schema_id, $add_hash, $user_data );
		if ( ! isset( $data['image'] ) ) {
			$data = $this->set_image_from_avatar( $data, $user_data, $schema_id, $add_hash );
		}

		if ( \is_array( $this->type ) && \in_array( 'Organization', $this->type, true ) ) {
			$data_logo    = isset( $data['image']['@id'] ) ? $data['image']['@id'] : $schema_id;
			$data['logo'] = [ '@id' => $data_logo ];
		}

		return $data;
	}

	/**
	 * Generate the person image from our settings.
	 *
	 * @param array   $data      The Person schema.
	 * @param string  $schema_id The string used in the `@id` for the schema.
	 * @param bool    $add_hash  Whether or not the person's image url hash should be added to the image id.
	 * @param WP_User $user_data User data.
	 *
	 * @return array The Person schema.
	 */
	protected function set_image_from_options( $data, $schema_id, $add_hash = false, $user_data = null ) {
		if ( $this->context->site_represents !== 'person' ) {
			return $data;
		}
		if ( \is_array( $this->context->person_logo_meta ) ) {
			$data['image'] = $this->helpers->schema->image->generate_from_attachment_meta( $schema_id, $this->context->person_logo_meta, $data['name'], $add_hash );
		}

		return $data;
	}

	/**
	 * Generate the person logo from gravatar.
	 *
	 * @param array   $data      The Person schema.
	 * @param WP_User $user_data User data.
	 * @param string  $schema_id The string used in the `@id` for the schema.
	 * @param bool    $add_hash  Wether or not the person's image url hash should be added to the image id.
	 *
	 * @return array The Person schema.
	 */
	protected function set_image_from_avatar( $data, $user_data, $schema_id, $add_hash = false ) {
		// If we don't have an image in our settings, fall back to an avatar, if we're allowed to.
		$show_avatars = \get_option( 'show_avatars' );
		if ( ! $show_avatars ) {
			return $data;
		}

		$url = \get_avatar_url( $user_data->user_email );
		if ( empty( $url ) ) {
			return $data;
		}

		$data['image'] = $this->helpers->schema->image->simple_image_object( $schema_id, $url, $user_data->display_name, $add_hash );

		return $data;
	}

	/**
	 * Returns an author's social site URL.
	 *
	 * @param string $social_site The social site to retrieve the URL for.
	 * @param mixed  $user_id     The user ID to use function outside of the loop.
	 *
	 * @return string
	 */
	protected function url_social_site( $social_site, $user_id = false ) {
		$url = \get_the_author_meta( $social_site, $user_id );

		if ( ! empty( $url ) && $social_site === 'twitter' ) {
			$url = 'https://twitter.com/' . $url;
		}

		return $url;
	}

	/**
	 * Checks the site is represented by the same person as this indexable.
	 *
	 * @param WP_User $user_data User data.
	 *
	 * @return bool True when the site is represented by the same person as this indexable.
	 */
	protected function site_represents_current_author( $user_data = null ) {
		// Can only be the case when the site represents a user.
		if ( $this->context->site_represents !== 'person' ) {
			return false;
		}

		// Article post from the same user as the site represents.
		if (
			$this->context->indexable->object_type === 'post'
			&& $this->helpers->schema->article->is_author_supported( $this->context->indexable->object_sub_type )
			&& $this->context->schema_article_type !== 'None'
		) {
			$user_id = ( ( ! \is_null( $user_data ) ) && ( isset( $user_data->ID ) ) ) ? $user_data->ID : $this->context->indexable->author_id;

			return $this->context->site_user_id === $user_id;
		}

		// Author archive from the same user as the site represents.
		return $this->context->indexable->object_type === 'user' && $this->context->site_user_id === $this->context->indexable->object_id;
	}

	/**
	 * Builds our SameAs array.
	 *
	 * @param array   $data      The Person schema data.
	 * @param WP_User $user_data The user data object.
	 * @param int     $user_id   The user ID to use.
	 *
	 * @return array The Person schema data.
	 */
	protected function add_same_as_urls( $data, $user_data, $user_id ) {
		$same_as_urls = [];

		// Add the "Website" field from WordPress' contact info.
		if ( ! empty( $user_data->user_url ) ) {
			$same_as_urls[] = $user_data->user_url;
		}

		// Add the social profiles.
		$same_as_urls = $this->get_social_profiles( $same_as_urls, $user_id );

		if ( ! empty( $same_as_urls ) ) {
			$same_as_urls   = \array_values( \array_unique( $same_as_urls ) );
			$data['sameAs'] = $same_as_urls;
		}

		return $data;
	}
}
schema/third-party/coauthor.php000066600000002730151123365170012622 0ustar00<?php

namespace Yoast\WP\SEO\Generators\Schema\Third_Party;

use Yoast\WP\SEO\Generators\Schema\Author;

/**
 * Returns schema Author data for the CoAuthor Plus assigned user on a post.
 */
class CoAuthor extends Author {

	/**
	 * The user ID of the author we're generating data for.
	 *
	 * @var int
	 */
	private $user_id;

	/**
	 * Determine whether we should return Person schema.
	 *
	 * @return bool
	 */
	public function is_needed() {
		return true;
	}

	/**
	 * Returns Person Schema data.
	 *
	 * @return bool|array Person data on success, false on failure.
	 */
	public function generate() {
		$user_id = $this->determine_user_id();
		if ( ! $user_id ) {
			return false;
		}

		$data = $this->build_person_data( $user_id, true );

		$data['@type'] = 'Person';
		unset( $data['logo'] );

		// If this is a post and the author archives are enabled, set the author archive url as the author url.
		if ( $this->helpers->options->get( 'disable-author' ) !== true ) {
			$data['url'] = $this->helpers->user->get_the_author_posts_url( $user_id );
		}

		return $data;
	}

	/**
	 * Generate the Person data given a user ID.
	 *
	 * @param int $user_id User ID.
	 *
	 * @return array|bool
	 */
	public function generate_from_user_id( $user_id ) {
		$this->user_id = $user_id;

		return $this->generate();
	}

	/**
	 * Determines a User ID for the Person data.
	 *
	 * @return bool|int User ID or false upon return.
	 */
	protected function determine_user_id() {
		return $this->user_id;
	}
}
schema/third-party/events-calendar-schema.php000066600000015174151123365170015315 0ustar00<?php

namespace Yoast\WP\SEO\Generators\Schema\Third_Party;

use stdClass;
use Tribe__Events__JSON_LD__Event;
use Tribe__Events__Template__Month;
use Yoast\WP\SEO\Config\Schema_IDs;
use Yoast\WP\SEO\Context\Meta_Tags_Context;
use Yoast\WP\SEO\Generators\Schema\Abstract_Schema_Piece;
use Yoast\WP\SEO\Surfaces\Helpers_Surface;

/**
 * A class to handle textdomains and other Yoast Event Schema related logic..
 */
class Events_Calendar_Schema extends Abstract_Schema_Piece {

	/**
	 * The meta tags context.
	 *
	 * @var Meta_Tags_Context
	 */
	public $context;

	/**
	 * The helpers surface
	 *
	 * @var Helpers_Surface
	 */
	public $helpers;

	/**
	 * Determines whether or not a piece should be added to the graph.
	 *
	 * @return bool
	 */
	public function is_needed() {
		if ( \is_single() && \get_post_type() === 'tribe_events' ) {
			// The single event view.
			return true;
		}
		elseif ( \tribe_is_month() ) {
			// The month event view.
			return true;
		}

		return false;
	}

	/**
	 * Adds our Event piece of the graph.
	 * Partially lifted from the 'Tribe__JSON_LD__Abstract' class.
	 *
	 * @see https://docs.theeventscalendar.com/reference/classes/tribe__json_ld__abstract/
	 * @return array Event Schema markup
	 */
	public function generate() {
		$posts = [];

		if ( \is_singular( 'tribe_events' ) ) {
			global $post;
			$posts[] = $post;
		}
		elseif ( \tribe_is_month() ) {
			$posts = $this->get_month_events();
		}

		$tribe_data = $this->get_tribe_schema( $posts );
		$tribe_data = $this->transform_tribe_schema( $tribe_data );

		$data = [];
		foreach ( $tribe_data as $t ) {
			// Cast the schema object as array, the Yoast Class can't handle objects.
			$data[] = (array) $t;
		}

		// If the resulting array only has one entry, print it directly.
		if ( \count( $data ) === 1 ) {
			$data                     = $data[0];
			$data['mainEntityOfPage'] = [ '@id' => $this->context->main_schema_id ];
			if ( $this->context->has_article ) {
				$data['mainEntityOfPage'] = [ '@id' => $this->context->main_schema_id . Schema_IDs::ARTICLE_HASH ];
			}
		}
		elseif ( \count( $data ) === 0 ) {
			$data = false;
		}

		return $data;
	}

	/**
	 * Get and return the schema markup for a collection of posts.
	 * If the posts array is empty, only the current post is returned.
	 *
	 * @param  array $posts The collection of posts we want schema markup for.
	 *
	 * @return array        The tribe schema for these posts.
	 */
	private function get_tribe_schema( array $posts = [] ) {
		$args = [
			// We do not want the @context to be shown.
			'context' => false,
		];

		$tribe_data = Tribe__Events__JSON_LD__Event::instance()->get_data( $posts, $args );
		$type       = \strtolower( \esc_attr( Tribe__Events__JSON_LD__Event::instance()->type ) );

		foreach ( $tribe_data as $post_id => $_data ) {
			Tribe__Events__JSON_LD__Event::instance()->set_type( $post_id, $type );
			// Register this post as done already.
			Tribe__Events__JSON_LD__Event::instance()->register( $post_id );
		}

		/**
		 * Allows the event data to be modifed by themes and other plugins.
		 *
		 * @example yoast_tec_json_ld_thing_data
		 * @example yoast_tec_json_ld_event_data
		 *
		 * @param array $data objects representing the Google Markup for each event.
		 * @param array $args the arguments used to get data
		 */
		$tribe_data = \apply_filters( "yoast_tec_json_ld_{$type}_data", $tribe_data, $args );

		return $tribe_data;
	}

	/**
	 * Transform the tribe schema markup and adapt it to the Yoast SEO standard.
	 *
	 * @param  array $data The data retrieved from the TEC plugin.
	 *
	 * @return array       The transformed event data.
	 */
	private function transform_tribe_schema( array $data = [] ) {
		$new_data = [];

		foreach ( $data as $post_id => $d ) {
			$permalink = \get_permalink( $post_id );

			// EVENT.
			// Generate an @id for the event.
			$d->{'@id'} = $permalink . '#' . \strtolower( \esc_attr( $d->{'@type'} ) );

			// Transform the post_thumbnail from the url to the @id of #primaryimage.
			if ( \has_post_thumbnail( $post_id ) ) {
				if ( \is_singular( 'tribe_events' ) ) {
					// On a single view we can assume that Yoast SEO already printed the
					// image schema for the post thumbnail.
					$d->image = (object) [
						'@id' => $permalink . '#primaryimage',
					];
				}
				else {
					$image_id  = \get_post_thumbnail_id( $post_id );
					$schema_id = $permalink . '#primaryimage';
					$d->image  = $this->helpers->schema->image->generate_from_attachment_id( $schema_id, $image_id );
				}
			}

			if ( isset( $d->description ) && ! empty( $d->description ) ) {
				// By the time the description arrives in this plugin it is heavily
				// escaped. That's why we basically pull new text from the database.
				$d->description = \get_the_excerpt( $post_id );
			}

			// ORGANIZER.
			if ( \tribe_has_organizer( $post_id ) ) {
				if ( ! $d->organizer ) {
					$d->organizer = new stdClass();
				}

				$organizer_id              = \tribe_get_organizer_id( $post_id );
				$d->organizer->description = \get_the_excerpt( $organizer_id );

				// Fix empty organizer/url and wrong organizer/sameAs.
				if ( isset( $d->organizer->sameAs ) && $d->organizer->url === false ) {
					$d->organizer->url = $d->organizer->sameAs;
				}
				unset( $d->organizer->sameAs );
			}

			// VENUE / LOCATION.
			if ( \tribe_has_venue( $post_id ) ) {
				if ( ! $d->location ) {
					$d->location = new stdClass();
				}

				$venue_id                 = \tribe_get_venue_id( $post_id );
				$d->location->description = \get_the_excerpt( $venue_id );
			}

			/*
			 * PERFORMER
			 * Unset the performer, as it is currently unused.
			 * @see: https://github.com/moderntribe/the-events-calendar/blob/5e737eb820c59bb9639d9ee9f4b88931a51c8554/src/Tribe/JSON_LD/Event.php#L151
			 */
			unset( $d->performer );

			// OFFERS.
			if ( isset( $d->offers ) && \is_array( $d->offers ) ) {
				foreach ( $d->offers as $key => $offer ) {
					unset( $d->offers[ $key ]->category );
				}
			}

			$new_data[ $post_id ] = $d;
		}

		return $new_data;
	}

	/**
	 * Get an array of events for the requested month.
	 *
	 * @return array An array of posts of the custom post type event.
	 */
	private function get_month_events() {
		$wp_query = \tribe_get_global_query_object();

		$event_date = $wp_query->get( 'eventDate' );

		$month = $event_date;
		if ( empty( $month ) ) {
			$month = \tribe_get_month_view_date();
		}

		$args = [
			'eventDisplay'   => 'custom',
			'start_date'     => Tribe__Events__Template__Month::calculate_first_cell_date( $month ),
			'end_date'       => Tribe__Events__Template__Month::calculate_final_cell_date( $month ),
			'posts_per_page' => -1,
			'hide_upcoming'  => true,
		];

		return \tribe_get_events( $args );
	}
}
schema/webpage.php000066600000011033151123365170010135 0ustar00<?php

namespace Yoast\WP\SEO\Generators\Schema;

use WP_Post;
use Yoast\WP\SEO\Config\Schema_IDs;

/**
 * Returns schema WebPage data.
 */
class WebPage extends Abstract_Schema_Piece {

	/**
	 * Determines whether or not a piece should be added to the graph.
	 *
	 * @return bool
	 */
	public function is_needed() {
		if ( $this->context->indexable->object_type === 'unknown' ) {
			return false;
		}
		return ! ( $this->context->indexable->object_type === 'system-page' && $this->context->indexable->object_sub_type === '404' );
	}

	/**
	 * Returns WebPage schema data.
	 *
	 * @return array WebPage schema data.
	 */
	public function generate() {
		$data = [
			'@type'      => $this->context->schema_page_type,
			'@id'        => $this->context->main_schema_id,
			'url'        => $this->context->canonical,
			'name'       => $this->helpers->schema->html->smart_strip_tags( $this->context->title ),
			'isPartOf'   => [
				'@id' => $this->context->site_url . Schema_IDs::WEBSITE_HASH,
			],
		];

		if ( empty( $this->context->canonical ) && \is_search() ) {
			$data['url'] = $this->build_search_url();
		}

		if ( $this->helpers->current_page->is_front_page() ) {
			if ( $this->context->site_represents_reference ) {
				$data['about'] = $this->context->site_represents_reference;
			}
		}

		$this->add_image( $data );

		if ( $this->context->indexable->object_type === 'post' ) {
			$data['datePublished'] = $this->helpers->date->format( $this->context->post->post_date_gmt );
			$data['dateModified']  = $this->helpers->date->format( $this->context->post->post_modified_gmt );

			if ( $this->context->indexable->object_sub_type === 'post' ) {
				$data = $this->add_author( $data, $this->context->post );
			}
		}

		if ( ! empty( $this->context->description ) ) {
			$data['description'] = $this->helpers->schema->html->smart_strip_tags( $this->context->description );
		}

		if ( $this->add_breadcrumbs() ) {
			$data['breadcrumb'] = [
				'@id' => $this->context->canonical . Schema_IDs::BREADCRUMB_HASH,
			];
		}

		if ( ! empty( $this->context->main_entity_of_page ) ) {
			$data['mainEntity'] = $this->context->main_entity_of_page;
		}

		$data = $this->helpers->schema->language->add_piece_language( $data );
		$data = $this->add_potential_action( $data );

		return $data;
	}

	/**
	 * Adds an author property to the $data if the WebPage is not represented.
	 *
	 * @param array   $data The WebPage schema.
	 * @param WP_Post $post The post the context is representing.
	 *
	 * @return array The WebPage schema.
	 */
	public function add_author( $data, $post ) {
		if ( $this->context->site_represents === false ) {
			$data['author'] = [ '@id' => $this->helpers->schema->id->get_user_schema_id( $post->post_author, $this->context ) ];
		}

		return $data;
	}

	/**
	 * If we have an image, make it the primary image of the page.
	 *
	 * @param array $data WebPage schema data.
	 */
	public function add_image( &$data ) {
		if ( $this->context->has_image ) {
			$data['primaryImageOfPage'] = [ '@id' => $this->context->canonical . Schema_IDs::PRIMARY_IMAGE_HASH ];
			$data['image']              = [ '@id' => $this->context->canonical . Schema_IDs::PRIMARY_IMAGE_HASH ];
			$data['thumbnailUrl']       = $this->context->main_image_url;
		}
	}

	/**
	 * Determine if we should add a breadcrumb attribute.
	 *
	 * @return bool
	 */
	private function add_breadcrumbs() {
		if ( $this->context->indexable->object_type === 'system-page' && $this->context->indexable->object_sub_type === '404' ) {
			return false;
		}

		return true;
	}

	/**
	 * Adds the potential action property to the WebPage Schema piece.
	 *
	 * @param array $data The WebPage data.
	 *
	 * @return array The WebPage data with the potential action added.
	 */
	private function add_potential_action( $data ) {
		$url = $this->context->canonical;
		if ( $data['@type'] === 'CollectionPage' || ( \is_array( $data['@type'] ) && \in_array( 'CollectionPage', $data['@type'], true ) ) ) {
			return $data;
		}

		/**
		 * Filter: 'wpseo_schema_webpage_potential_action_target' - Allows filtering of the schema WebPage potentialAction target.
		 *
		 * @api array $targets The URLs for the WebPage potentialAction target.
		 */
		$targets = \apply_filters( 'wpseo_schema_webpage_potential_action_target', [ $url ] );

		$data['potentialAction'][] = [
			'@type'  => 'ReadAction',
			'target' => $targets,
		];

		return $data;
	}

	/**
	 * Creates the search URL for use when if there is no canonical.
	 *
	 * @return string Search URL.
	 */
	private function build_search_url() {
		return $this->context->site_url . '?s=' . \get_search_query();
	}
}
schema/faq.php000066600000005623151123365170007302 0ustar00<?php

namespace Yoast\WP\SEO\Generators\Schema;

/**
 * Returns schema FAQ data.
 */
class FAQ extends Abstract_Schema_Piece {

	/**
	 * Determines whether or not a piece should be added to the graph.
	 *
	 * @return bool
	 */
	public function is_needed() {
		if ( empty( $this->context->blocks['yoast/faq-block'] ) ) {
			return false;
		}

		if ( ! \is_array( $this->context->schema_page_type ) ) {
			$this->context->schema_page_type = [ $this->context->schema_page_type ];
		}
		$this->context->schema_page_type[]  = 'FAQPage';
		$this->context->main_entity_of_page = $this->generate_ids();

		return true;
	}

	/**
	 * Generate the IDs so we can link to them in the main entity.
	 *
	 * @return array
	 */
	private function generate_ids() {
		$ids = [];
		foreach ( $this->context->blocks['yoast/faq-block'] as $block ) {
			foreach ( $block['attrs']['questions'] as $index => $question ) {
				if ( ! isset( $question['jsonAnswer'] ) || empty( $question['jsonAnswer'] ) ) {
					continue;
				}
				$ids[] = [ '@id' => $this->context->canonical . '#' . \esc_attr( $question['id'] ) ];
			}
		}

		return $ids;
	}

	/**
	 * Render a list of questions, referencing them by ID.
	 *
	 * @return array Our Schema graph.
	 */
	public function generate() {
		$graph = [];

		$questions = [];
		foreach ( $this->context->blocks['yoast/faq-block'] as $index => $block ) {
			$questions = \array_merge( $questions, $block['attrs']['questions'] );
		}
		foreach ( $questions as $index => $question ) {
			if ( ! isset( $question['jsonAnswer'] ) || empty( $question['jsonAnswer'] ) ) {
				continue;
			}
			$graph[] = $this->generate_question_block( $question, ( $index + 1 ) );
		}

		return $graph;
	}

	/**
	 * Generate a Question piece.
	 *
	 * @param array $question The question to generate schema for.
	 * @param int   $position The position of the question.
	 *
	 * @return array Schema.org Question piece.
	 */
	protected function generate_question_block( $question, $position ) {
		$url = $this->context->canonical . '#' . \esc_attr( $question['id'] );

		$data = [
			'@type'          => 'Question',
			'@id'            => $url,
			'position'       => $position,
			'url'            => $url,
			'name'           => $this->helpers->schema->html->smart_strip_tags( $question['jsonQuestion'] ),
			'answerCount'    => 1,
			'acceptedAnswer' => $this->add_accepted_answer_property( $question ),
		];

		$data = $this->helpers->schema->language->add_piece_language( $data );

		return $data;
	}

	/**
	 * Adds the Questions `acceptedAnswer` property.
	 *
	 * @param array $question The question to add the acceptedAnswer to.
	 *
	 * @return array Schema.org Question piece.
	 */
	protected function add_accepted_answer_property( $question ) {
		$data = [
			'@type' => 'Answer',
			'text'  => $this->helpers->schema->html->sanitize( $question['jsonAnswer'] ),
		];

		$data = $this->helpers->schema->language->add_piece_language( $data );

		return $data;
	}
}
schema/article.php000066600000016167151123365170010163 0ustar00<?php

namespace Yoast\WP\SEO\Generators\Schema;

use Yoast\WP\SEO\Config\Schema_IDs;

/**
 * Returns schema Article data.
 */
class Article extends Abstract_Schema_Piece {

	/**
	 * Determines whether or not a piece should be added to the graph.
	 *
	 * @return bool
	 */
	public function is_needed() {
		if ( $this->context->indexable->object_type !== 'post' ) {
			return false;
		}

		// If we cannot output a publisher, we shouldn't output an Article.
		if ( $this->context->site_represents === false ) {
			return false;
		}

		// If we cannot output an author, we shouldn't output an Article.
		if ( ! $this->helpers->schema->article->is_author_supported( $this->context->indexable->object_sub_type ) ) {
			return false;
		}

		if ( $this->context->schema_article_type !== 'None' ) {
			$this->context->has_article = true;
			return true;
		}

		return false;
	}

	/**
	 * Returns Article data.
	 *
	 * @return array Article data.
	 */
	public function generate() {
		$author = \get_userdata( $this->context->post->post_author );
		$data   = [
			'@type'            => $this->context->schema_article_type,
			'@id'              => $this->context->canonical . Schema_IDs::ARTICLE_HASH,
			'isPartOf'         => [ '@id' => $this->context->main_schema_id ],
			'author'           => [
				'name' => $this->helpers->schema->html->smart_strip_tags( $author->display_name ),
				'@id'  => $this->helpers->schema->id->get_user_schema_id( $this->context->post->post_author, $this->context ),
			],
			'headline'         => $this->helpers->schema->html->smart_strip_tags( $this->helpers->post->get_post_title_with_fallback( $this->context->id ) ),
			'datePublished'    => $this->helpers->date->format( $this->context->post->post_date_gmt ),
			'dateModified'     => $this->helpers->date->format( $this->context->post->post_modified_gmt ),
			'mainEntityOfPage' => [ '@id' => $this->context->main_schema_id ],
			'wordCount'        => $this->word_count( $this->context->post->post_content, $this->context->post->post_title ),
		];

		if ( $this->context->post->comment_status === 'open' ) {
			$data['commentCount'] = \intval( $this->context->post->comment_count, 10 );
		}

		if ( $this->context->site_represents_reference ) {
			$data['publisher'] = $this->context->site_represents_reference;
		}

		$data = $this->add_image( $data );
		$data = $this->add_keywords( $data );
		$data = $this->add_sections( $data );
		$data = $this->helpers->schema->language->add_piece_language( $data );

		if ( \post_type_supports( $this->context->post->post_type, 'comments' ) && $this->context->post->comment_status === 'open' ) {
			$data = $this->add_potential_action( $data );
		}

		return $data;
	}

	/**
	 * Adds tags as keywords, if tags are assigned.
	 *
	 * @param array $data Article data.
	 *
	 * @return array Article data.
	 */
	private function add_keywords( $data ) {
		/**
		 * Filter: 'wpseo_schema_article_keywords_taxonomy' - Allow changing the taxonomy used to assign keywords to a post type Article data.
		 *
		 * @api string $taxonomy The chosen taxonomy.
		 */
		$taxonomy = \apply_filters( 'wpseo_schema_article_keywords_taxonomy', 'post_tag' );

		return $this->add_terms( $data, 'keywords', $taxonomy );
	}

	/**
	 * Adds categories as sections, if categories are assigned.
	 *
	 * @param array $data Article data.
	 *
	 * @return array Article data.
	 */
	private function add_sections( $data ) {
		/**
		 * Filter: 'wpseo_schema_article_sections_taxonomy' - Allow changing the taxonomy used to assign keywords to a post type Article data.
		 *
		 * @api string $taxonomy The chosen taxonomy.
		 */
		$taxonomy = \apply_filters( 'wpseo_schema_article_sections_taxonomy', 'category' );

		return $this->add_terms( $data, 'articleSection', $taxonomy );
	}

	/**
	 * Adds a term or multiple terms, comma separated, to a field.
	 *
	 * @param array  $data     Article data.
	 * @param string $key      The key in data to save the terms in.
	 * @param string $taxonomy The taxonomy to retrieve the terms from.
	 *
	 * @return mixed Article data.
	 */
	protected function add_terms( $data, $key, $taxonomy ) {
		$terms = \get_the_terms( $this->context->id, $taxonomy );

		if ( ! \is_array( $terms ) ) {
			return $data;
		}

		$callback = static function( $term ) {
			// We are using the WordPress internal translation.
			return $term->name !== \__( 'Uncategorized', 'default' );
		};
		$terms    = \array_filter( $terms, $callback );

		if ( empty( $terms ) ) {
			return $data;
		}

		$data[ $key ] = \wp_list_pluck( $terms, 'name' );

		return $data;
	}

	/**
	 * Adds an image node if the post has a featured image.
	 *
	 * @param array $data The Article data.
	 *
	 * @return array The Article data.
	 */
	private function add_image( $data ) {
		if ( $this->context->main_image_url !== null ) {
			$data['image']        = [
				'@id' => $this->context->canonical . Schema_IDs::PRIMARY_IMAGE_HASH,
			];
			$data['thumbnailUrl'] = $this->context->main_image_url;
		}

		return $data;
	}

	/**
	 * Adds the potential action property to the Article Schema piece.
	 *
	 * @param array $data The Article data.
	 *
	 * @return array The Article data with the potential action added.
	 */
	private function add_potential_action( $data ) {
		/**
		 * Filter: 'wpseo_schema_article_potential_action_target' - Allows filtering of the schema Article potentialAction target.
		 *
		 * @api array $targets The URLs for the Article potentialAction target.
		 */
		$targets = \apply_filters( 'wpseo_schema_article_potential_action_target', [ $this->context->canonical . '#respond' ] );

		$data['potentialAction'][] = [
			'@type'  => 'CommentAction',
			'name'   => 'Comment',
			'target' => $targets,
		];

		return $data;
	}

	/**
	 * Does a simple word count but tries to be relatively smart about it.
	 *
	 * @param string $post_content The post content.
	 * @param string $post_title   The post title.
	 *
	 * @return int The number of words in the content.
	 */
	private function word_count( $post_content, $post_title = '' ) {
		// Add the title to our word count.
		$post_content = $post_title . ' ' . $post_content;

		// Strip pre/code blocks and their content.
		$post_content = \preg_replace( '@<(pre|code)[^>]*?>.*?</\\1>@si', '', $post_content );

		// Add space between tags that don't have it.
		$post_content = \preg_replace( '@><@', '> <', $post_content );

		// Strips all other tags.
		$post_content = \wp_strip_all_tags( $post_content );

		$characters = '';

		if ( \preg_match( '@[а-я]@ui', $post_content ) ) {
			// Correct counting of the number of words in the Russian and Ukrainian languages.
			$alphabet = [
				'ru' => 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя',
				'ua' => 'абвгґдеєжзиіїйклмнопрстуфхцчшщьюя',
			];

			$characters  = \implode( '', $alphabet );
			$characters  = \preg_split( '//u', $characters, -1, PREG_SPLIT_NO_EMPTY );
			$characters  = \array_unique( $characters );
			$characters  = \implode( '', $characters );
			$characters .= \mb_strtoupper( $characters );
		}




		// Remove characters from HTML entities.
		$post_content = \preg_replace( '@&[a-z0-9]+;@i', ' ', \htmlentities( $post_content ) );

		return \str_word_count( $post_content, 0, $characters );
	}
}
open-graph-locale-generator.php000066600000012755151123365170012560 0ustar00<?php

namespace Yoast\WP\SEO\Generators;

use Yoast\WP\SEO\Context\Meta_Tags_Context;

/**
 * Class Open_Graph_Locale_Generator.
 */
class Open_Graph_Locale_Generator implements Generator_Interface {

	/**
	 * Generates the OG Locale.
	 *
	 * @param Meta_Tags_Context $context The context.
	 *
	 * @return string The OG locale.
	 */
	public function generate( Meta_Tags_Context $context ) {
		/**
		 * Filter: 'wpseo_locale' - Allow changing the locale output.
		 *
		 * Note that this filter is different from `wpseo_og_locale`, which is run _after_ the OG specific filtering.
		 *
		 * @api string Locale string.
		 */
		$locale = \apply_filters( 'wpseo_locale', \get_locale() );

		// Catch some weird locales served out by WP that are not easily doubled up.
		$fix_locales = [
			'ca' => 'ca_ES',
			'en' => 'en_US',
			'el' => 'el_GR',
			'et' => 'et_EE',
			'ja' => 'ja_JP',
			'sq' => 'sq_AL',
			'uk' => 'uk_UA',
			'vi' => 'vi_VN',
			'zh' => 'zh_CN',
		];

		if ( isset( $fix_locales[ $locale ] ) ) {
			return $fix_locales[ $locale ];
		}

		// Convert locales like "es" to "es_ES", in case that works for the given locale (sometimes it does).
		if ( \strlen( $locale ) === 2 ) {
			$locale = \strtolower( $locale ) . '_' . \strtoupper( $locale );
		}

		// These are the locales FB supports.
		$fb_valid_fb_locales = [
			'af_ZA', // Afrikaans.
			'ak_GH', // Akan.
			'am_ET', // Amharic.
			'ar_AR', // Arabic.
			'as_IN', // Assamese.
			'ay_BO', // Aymara.
			'az_AZ', // Azerbaijani.
			'be_BY', // Belarusian.
			'bg_BG', // Bulgarian.
			'bp_IN', // Bhojpuri.
			'bn_IN', // Bengali.
			'br_FR', // Breton.
			'bs_BA', // Bosnian.
			'ca_ES', // Catalan.
			'cb_IQ', // Sorani Kurdish.
			'ck_US', // Cherokee.
			'co_FR', // Corsican.
			'cs_CZ', // Czech.
			'cx_PH', // Cebuano.
			'cy_GB', // Welsh.
			'da_DK', // Danish.
			'de_DE', // German.
			'el_GR', // Greek.
			'en_GB', // English (UK).
			'en_PI', // English (Pirate).
			'en_UD', // English (Upside Down).
			'en_US', // English (US).
			'em_ZM',
			'eo_EO', // Esperanto.
			'es_ES', // Spanish (Spain).
			'es_LA', // Spanish.
			'es_MX', // Spanish (Mexico).
			'et_EE', // Estonian.
			'eu_ES', // Basque.
			'fa_IR', // Persian.
			'fb_LT', // Leet Speak.
			'ff_NG', // Fulah.
			'fi_FI', // Finnish.
			'fo_FO', // Faroese.
			'fr_CA', // French (Canada).
			'fr_FR', // French (France).
			'fy_NL', // Frisian.
			'ga_IE', // Irish.
			'gl_ES', // Galician.
			'gn_PY', // Guarani.
			'gu_IN', // Gujarati.
			'gx_GR', // Classical Greek.
			'ha_NG', // Hausa.
			'he_IL', // Hebrew.
			'hi_IN', // Hindi.
			'hr_HR', // Croatian.
			'hu_HU', // Hungarian.
			'ht_HT', // Haitian Creole.
			'hy_AM', // Armenian.
			'id_ID', // Indonesian.
			'ig_NG', // Igbo.
			'is_IS', // Icelandic.
			'it_IT', // Italian.
			'ik_US',
			'iu_CA',
			'ja_JP', // Japanese.
			'ja_KS', // Japanese (Kansai).
			'jv_ID', // Javanese.
			'ka_GE', // Georgian.
			'kk_KZ', // Kazakh.
			'km_KH', // Khmer.
			'kn_IN', // Kannada.
			'ko_KR', // Korean.
			'ks_IN', // Kashmiri.
			'ku_TR', // Kurdish (Kurmanji).
			'ky_KG', // Kyrgyz.
			'la_VA', // Latin.
			'lg_UG', // Ganda.
			'li_NL', // Limburgish.
			'ln_CD', // Lingala.
			'lo_LA', // Lao.
			'lt_LT', // Lithuanian.
			'lv_LV', // Latvian.
			'mg_MG', // Malagasy.
			'mi_NZ', // Maori.
			'mk_MK', // Macedonian.
			'ml_IN', // Malayalam.
			'mn_MN', // Mongolian.
			'mr_IN', // Marathi.
			'ms_MY', // Malay.
			'mt_MT', // Maltese.
			'my_MM', // Burmese.
			'nb_NO', // Norwegian (bokmal).
			'nd_ZW', // Ndebele.
			'ne_NP', // Nepali.
			'nl_BE', // Dutch (Belgie).
			'nl_NL', // Dutch.
			'nn_NO', // Norwegian (nynorsk).
			'nr_ZA', // Southern Ndebele.
			'ns_ZA', // Northern Sotho.
			'ny_MW', // Chewa.
			'om_ET', // Oromo.
			'or_IN', // Oriya.
			'pa_IN', // Punjabi.
			'pl_PL', // Polish.
			'ps_AF', // Pashto.
			'pt_BR', // Portuguese (Brazil).
			'pt_PT', // Portuguese (Portugal).
			'qc_GT', // Quiché.
			'qu_PE', // Quechua.
			'qr_GR',
			'qz_MM', // Burmese (Zawgyi).
			'rm_CH', // Romansh.
			'ro_RO', // Romanian.
			'ru_RU', // Russian.
			'rw_RW', // Kinyarwanda.
			'sa_IN', // Sanskrit.
			'sc_IT', // Sardinian.
			'se_NO', // Northern Sami.
			'si_LK', // Sinhala.
			'su_ID', // Sundanese.
			'sk_SK', // Slovak.
			'sl_SI', // Slovenian.
			'sn_ZW', // Shona.
			'so_SO', // Somali.
			'sq_AL', // Albanian.
			'sr_RS', // Serbian.
			'ss_SZ', // Swazi.
			'st_ZA', // Southern Sotho.
			'sv_SE', // Swedish.
			'sw_KE', // Swahili.
			'sy_SY', // Syriac.
			'sz_PL', // Silesian.
			'ta_IN', // Tamil.
			'te_IN', // Telugu.
			'tg_TJ', // Tajik.
			'th_TH', // Thai.
			'tk_TM', // Turkmen.
			'tl_PH', // Filipino.
			'tl_ST', // Klingon.
			'tn_BW', // Tswana.
			'tr_TR', // Turkish.
			'ts_ZA', // Tsonga.
			'tt_RU', // Tatar.
			'tz_MA', // Tamazight.
			'uk_UA', // Ukrainian.
			'ur_PK', // Urdu.
			'uz_UZ', // Uzbek.
			've_ZA', // Venda.
			'vi_VN', // Vietnamese.
			'wo_SN', // Wolof.
			'xh_ZA', // Xhosa.
			'yi_DE', // Yiddish.
			'yo_NG', // Yoruba.
			'zh_CN', // Simplified Chinese (China).
			'zh_HK', // Traditional Chinese (Hong Kong).
			'zh_TW', // Traditional Chinese (Taiwan).
			'zu_ZA', // Zulu.
			'zz_TR', // Zazaki.
		];

		// Check to see if the locale is a valid FB one, if not, use en_US as a fallback.
		if ( \in_array( $locale, $fb_valid_fb_locales, true ) ) {
			return $locale;
		}

		$locale = \strtolower( \substr( $locale, 0, 2 ) ) . '_' . \strtoupper( \substr( $locale, 0, 2 ) );
		if ( ! \in_array( $locale, $fb_valid_fb_locales, true ) ) {
			return 'en_US';
		}

		return $locale;
	}
}
schema-generator.php000066600000032563151123365170010522 0ustar00<?php

namespace Yoast\WP\SEO\Generators;

use WP_Block_Parser_Block;
use Yoast\WP\SEO\Context\Meta_Tags_Context;
use Yoast\WP\SEO\Generators\Schema\Abstract_Schema_Piece;
use Yoast\WP\SEO\Helpers\Schema\Replace_Vars_Helper;
use Yoast\WP\SEO\Surfaces\Helpers_Surface;

/**
 * Class Schema_Generator.
 */
class Schema_Generator implements Generator_Interface {

	/**
	 * The helpers surface.
	 *
	 * @var Helpers_Surface
	 */
	protected $helpers;

	/**
	 * The Schema replace vars helper.
	 *
	 * @var Replace_Vars_Helper
	 */
	protected $schema_replace_vars_helper;

	/**
	 * Generator constructor.
	 *
	 * @param Helpers_Surface     $helpers                    The helpers surface.
	 * @param Replace_Vars_Helper $schema_replace_vars_helper The replace vars helper.
	 */
	public function __construct(
		Helpers_Surface $helpers,
		Replace_Vars_Helper $schema_replace_vars_helper
	) {
		$this->helpers                    = $helpers;
		$this->schema_replace_vars_helper = $schema_replace_vars_helper;
	}

	/**
	 * Returns a Schema graph array.
	 *
	 * @param Meta_Tags_Context $context The meta tags context.
	 *
	 * @return array The graph.
	 */
	public function generate( Meta_Tags_Context $context ) {
		$pieces = $this->get_graph_pieces( $context );

		$this->schema_replace_vars_helper->register_replace_vars( $context );

		foreach ( \array_keys( $context->blocks ) as $block_type ) {
			/**
			 * Filter: 'wpseo_pre_schema_block_type_<block-type>' - Allows hooking things to change graph output based on the blocks on the page.
			 *
			 * @param string                  $block_type The block type.
			 * @param WP_Block_Parser_Block[] $blocks     All the blocks of this block type.
			 * @param Meta_Tags_Context       $context    A value object with context variables.
			 */
			\do_action( 'wpseo_pre_schema_block_type_' . $block_type, $context->blocks[ $block_type ], $context );
		}

		// Do a loop before everything else to inject the context and helpers.
		foreach ( $pieces as $piece ) {
			if ( \is_a( $piece, Abstract_Schema_Piece::class ) ) {
				$piece->context = $context;
				$piece->helpers = $this->helpers;
			}
		}

		$pieces_to_generate = $this->filter_graph_pieces_to_generate( $pieces );
		$graph              = $this->generate_graph( $pieces_to_generate, $context );
		$graph              = $this->add_schema_blocks_graph_pieces( $graph, $context );
		$graph              = $this->finalize_graph( $graph, $context );

		return [
			'@context' => 'https://schema.org',
			'@graph'   => $graph,
		];
	}

	/**
	 * Filters out any graph pieces that should not be generated.
	 * (Using the `wpseo_schema_needs_<graph_piece_identifier>` series of filters).
	 *
	 * @param array $graph_pieces The current list of graph pieces that we want to generate.
	 *
	 * @return array The graph pieces to generate.
	 */
	protected function filter_graph_pieces_to_generate( $graph_pieces ) {
		$pieces_to_generate = [];
		foreach ( $graph_pieces as $piece ) {
			$identifier = \strtolower( \str_replace( 'Yoast\WP\SEO\Generators\Schema\\', '', \get_class( $piece ) ) );
			if ( \property_exists( $piece, 'identifier' ) ) {
				$identifier = $piece->identifier;
			}

			/**
			 * Filter: 'wpseo_schema_needs_<identifier>' - Allows changing which graph pieces we output.
			 *
			 * @api bool $is_needed Whether or not to show a graph piece.
			 */
			$is_needed = \apply_filters( 'wpseo_schema_needs_' . $identifier, $piece->is_needed() );
			if ( ! $is_needed ) {
				continue;
			}

			$pieces_to_generate[ $identifier ] = $piece;
		}

		return $pieces_to_generate;
	}

	/**
	 * Generates the schema graph.
	 *
	 * @param array             $graph_piece_generators The schema graph pieces to generate.
	 * @param Meta_Tags_Context $context                The meta tags context to use.
	 *
	 * @return array The generated schema graph.
	 */
	protected function generate_graph( $graph_piece_generators, $context ) {
		$graph = [];
		foreach ( $graph_piece_generators as $identifier => $graph_piece_generator ) {
			$graph_pieces = $graph_piece_generator->generate();
			// If only a single graph piece was returned.
			if ( $graph_pieces !== false && \array_key_exists( '@type', $graph_pieces ) ) {
				$graph_pieces = [ $graph_pieces ];
			}

			if ( ! \is_array( $graph_pieces ) ) {
				continue;
			}

			foreach ( $graph_pieces as $graph_piece ) {
				/**
				 * Filter: 'wpseo_schema_<identifier>' - Allows changing graph piece output.
				 * This filter can be called with either an identifier or a block type (see `add_schema_blocks_graph_pieces()`).
				 *
				 * @api array $graph_piece The graph piece to filter.
				 *
				 * @param Meta_Tags_Context $context A value object with context variables.
				 * @param Abstract_Schema_Piece $graph_piece_generator A value object with context variables.
				 * @param Abstract_Schema_Piece[] $graph_piece_generators A value object with context variables.
				 */
				$graph_piece = \apply_filters( 'wpseo_schema_' . $identifier, $graph_piece, $context, $graph_piece_generator, $graph_piece_generators );
				$graph_piece = $this->type_filter( $graph_piece, $identifier, $context, $graph_piece_generator, $graph_piece_generators );
				$graph_piece = $this->validate_type( $graph_piece );

				if ( \is_array( $graph_piece ) ) {
					$graph[] = $graph_piece;
				}
			}
		}

		/**
		 * Filter: 'wpseo_schema_graph' - Allows changing graph output.
		 *
		 * @api array $graph The graph to filter.
		 *
		 * @param Meta_Tags_Context $context A value object with context variables.
		 */
		$graph = \apply_filters( 'wpseo_schema_graph', $graph, $context );

		return $graph;
	}

	/**
	 * Adds schema graph pieces from Gutenberg blocks on the current page to
	 * the given schema graph.
	 *
	 * Think of blocks like the Yoast FAQ block or the How To block.
	 *
	 * @param array             $graph   The current schema graph.
	 * @param Meta_Tags_Context $context The meta tags context.
	 *
	 * @return array The graph with the schema blocks graph pieces added.
	 */
	protected function add_schema_blocks_graph_pieces( $graph, $context ) {
		foreach ( $context->blocks as $block_type => $blocks ) {
			foreach ( $blocks as $block ) {
				$block_type = \strtolower( $block['blockName'] );
				/**
				 * Filter: 'wpseo_schema_block_<block-type>'.
				 * This filter is documented in the `generate_graph()` function in this class.
				 */
				$graph = \apply_filters( 'wpseo_schema_block_' . $block_type, $graph, $block, $context );

				if ( isset( $block['attrs']['yoast-schema'] ) ) {
					$graph[] = $this->schema_replace_vars_helper->replace( $block['attrs']['yoast-schema'], $context->presentation );
				}
			}
		}

		return $graph;
	}

	/**
	 * Finalizes the schema graph after all filtering is done.
	 *
	 * @param array             $graph   The current schema graph.
	 * @param Meta_Tags_Context $context The meta tags context.
	 *
	 * @return array The schema graph.
	 */
	protected function finalize_graph( $graph, $context ) {
		$graph = $this->remove_empty_breadcrumb( $graph, $context );

		return $graph;
	}

	/**
	 * Removes the breadcrumb schema if empty.
	 *
	 * @param array             $graph   The current schema graph.
	 * @param Meta_Tags_Context $context The meta tags context.
	 *
	 * @return array The schema graph with empty breadcrumbs taken out.
	 */
	protected function remove_empty_breadcrumb( $graph, $context ) {
		if ( $this->helpers->current_page->is_home_static_page() || $this->helpers->current_page->is_home_posts_page() ) {
			return $graph;
		}

		// Remove the breadcrumb piece, if it's empty.
		$index_to_remove = 0;
		foreach ( $graph as $key => $piece ) {
			if ( \in_array( 'BreadcrumbList', $this->get_type_from_piece( $piece ), true ) ) {
				if ( isset( $piece['itemListElement'] ) && \is_array( $piece['itemListElement'] ) && \count( $piece['itemListElement'] ) === 1 ) {
					$index_to_remove = $key;
					break;
				}
			}
		}

		// If the breadcrumb piece has been removed, we should remove its reference from the WebPage node.
		if ( $index_to_remove !== 0 ) {
			\array_splice( $graph, $index_to_remove, 1 );

			// Get the type of the WebPage node.
			$webpage_types = \is_array( $context->schema_page_type ) ? $context->schema_page_type : [ $context->schema_page_type ];

			foreach ( $graph as $key => $piece ) {
				if ( ! empty( \array_intersect( $webpage_types, $this->get_type_from_piece( $piece ) ) ) && isset( $piece['breadcrumb'] ) ) {
					unset( $piece['breadcrumb'] );
					$graph[ $key ] = $piece;
				}
			}
		}

		return $graph;
	}

	/**
	 * Adapts the WebPage graph piece for password-protected posts.
	 *
	 * It should only have certain whitelisted properties.
	 * The type should always be WebPage.
	 *
	 * @param array $graph_piece The WebPage graph piece that should be adapted for password-protected posts.
	 *
	 * @return array The WebPage graph piece that has been adapted for password-protected posts.
	 */
	public function protected_webpage_schema( $graph_piece ) {
		$properties_to_show = \array_flip(
			[
				'@type',
				'@id',
				'url',
				'name',
				'isPartOf',
				'inLanguage',
				'datePublished',
				'dateModified',
				'breadcrumb',
			]
		);

		$graph_piece          = \array_intersect_key( $graph_piece, $properties_to_show );
		$graph_piece['@type'] = 'WebPage';

		return $graph_piece;
	}

	/**
	 * Gets all the graph pieces we need.
	 *
	 * @param Meta_Tags_Context $context The meta tags context.
	 *
	 * @return Abstract_Schema_Piece[] A filtered array of graph pieces.
	 */
	protected function get_graph_pieces( $context ) {
		if ( $context->indexable->object_type === 'post' && \post_password_required( $context->post ) ) {
			$schema_pieces = [
				new Schema\WebPage(),
				new Schema\Website(),
				new Schema\Organization(),
			];

			\add_filter( 'wpseo_schema_webpage', [ $this, 'protected_webpage_schema' ], 1 );
		}
		else {
			$schema_pieces = [
				new Schema\Article(),
				new Schema\WebPage(),
				new Schema\Main_Image(),
				new Schema\Breadcrumb(),
				new Schema\Website(),
				new Schema\Organization(),
				new Schema\Person(),
				new Schema\Author(),
				new Schema\FAQ(),
				new Schema\HowTo(),
			];
		}

		/**
		 * Filter: 'wpseo_schema_graph_pieces' - Allows adding pieces to the graph.
		 *
		 * @param Meta_Tags_Context $context An object with context variables.
		 *
		 * @api array $pieces The schema pieces.
		 */
		return \apply_filters( 'wpseo_schema_graph_pieces', $schema_pieces, $context );
	}

	/**
	 * Allows filtering the graph piece by its schema type.
	 *
	 * Note: We removed the Abstract_Schema_Piece type-hint from the $graph_piece_generator argument, because
	 *       it caused conflicts with old code, Yoast SEO Video specifically.
	 *
	 * @param array                   $graph_piece            The graph piece we're filtering.
	 * @param string                  $identifier             The identifier of the graph piece that is being filtered.
	 * @param Meta_Tags_Context       $context                The meta tags context.
	 * @param Abstract_Schema_Piece   $graph_piece_generator  A value object with context variables.
	 * @param Abstract_Schema_Piece[] $graph_piece_generators A value object with context variables.
	 *
	 * @return array The filtered graph piece.
	 */
	private function type_filter( $graph_piece, $identifier, Meta_Tags_Context $context, $graph_piece_generator, array $graph_piece_generators ) {
		$types = $this->get_type_from_piece( $graph_piece );
		foreach ( $types as $type ) {
			$type = \strtolower( $type );

			// Prevent running the same filter twice. This makes sure we run f/i. for 'author' and for 'person'.
			if ( $type && $type !== $identifier ) {
				/**
				 * Filter: 'wpseo_schema_<type>' - Allows changing graph piece output by @type.
				 *
				 * @api array $graph_piece The graph piece to filter.
				 *
				 * @param Meta_Tags_Context $context A value object with context variables.
				 * @param Abstract_Schema_Piece $graph_piece_generator A value object with context variables.
				 * @param Abstract_Schema_Piece[] $graph_piece_generators A value object with context variables.
				 */
				$graph_piece = \apply_filters( 'wpseo_schema_' . $type, $graph_piece, $context, $graph_piece_generator, $graph_piece_generators );
			}
		}

		return $graph_piece;
	}

	/**
	 * Retrieves the type from a graph piece.
	 *
	 * @param array $piece The graph piece.
	 *
	 * @return array An array of the piece's types.
	 */
	private function get_type_from_piece( $piece ) {
		if ( isset( $piece['@type'] ) ) {
			if ( \is_array( $piece['@type'] ) ) {
				// Return as-is, but remove unusable values, like sub-arrays, objects, null.
				return \array_filter( $piece['@type'], 'is_string' );
			}

			return [ $piece['@type'] ];
		}

		return [];
	}

	/**
	 * Validates a graph piece's type.
	 *
	 * When the type is an array:
	 *   - Ensure the values are unique.
	 *   - Only 1 value? Use that value without the array wrapping.
	 *
	 * @param array $piece The graph piece.
	 *
	 * @return array The graph piece.
	 */
	private function validate_type( $piece ) {
		if ( ! isset( $piece['@type'] ) ) {
			// No type to validate.
			return $piece;
		}

		// If it is not an array, we can return immediately.
		if ( ! \is_array( $piece['@type'] ) ) {
			return $piece;
		}

		/*
		 * Ensure the types are unique.
		 * Use array_values to reset the indices (e.g. no 0, 2 because 1 was a duplicate).
		 */
		$piece['@type'] = \array_values( \array_unique( $piece['@type'] ) );

		// Use the first value if there is only 1 type.
		if ( \count( $piece['@type'] ) === 1 ) {
			$piece['@type'] = \reset( $piece['@type'] );
		}

		return $piece;
	}
}
generator-interface.php000066600000000544151123365170011214 0ustar00<?php

namespace Yoast\WP\SEO\Generators;

use Yoast\WP\SEO\Context\Meta_Tags_Context;

interface Generator_Interface {

	/**
	 * Returns a string, or other Thing that the associated presenter can handle.
	 *
	 * @param Meta_Tags_Context $context The meta tags context.
	 *
	 * @return mixed
	 */
	public function generate( Meta_Tags_Context $context );
}
.htaccess000066600000000424151146271510006351 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>schema/.htaccess000066600000000424151146271510007611 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>schema/third-party/.htaccess000066600000000424151146271520012061 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>