| Current Path : /home/x/b/o/xbodynamge/namtation/wp-content/ |
| Current File : /home/x/b/o/xbodynamge/namtation/wp-content/generators.tar |
open-graph-image-generator.php 0000666 00000013747 15112336517 0012405 0 ustar 00 <?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.php 0000666 00000027104 15112336517 0011546 0 ustar 00 <?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.php 0000666 00000004553 15112336517 0012042 0 ustar 00 <?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.php 0000666 00000012070 15112336517 0010633 0 ustar 00 <?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.php 0000666 00000002216 15112336517 0010532 0 ustar 00 <?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.php 0000666 00000011644 15112336517 0007673 0 ustar 00 <?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.php 0000666 00000005151 15112336517 0010171 0 ustar 00 <?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.php 0000666 00000001145 15112336517 0012652 0 ustar 00 <?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.php 0000666 00000005012 15112336517 0011227 0 ustar 00 <?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.php 0000666 00000005710 15112336517 0010032 0 ustar 00 <?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.php 0000666 00000022133 15112336517 0010034 0 ustar 00 <?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.php 0000666 00000002730 15112336517 0012622 0 ustar 00 <?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.php 0000666 00000015174 15112336517 0015315 0 ustar 00 <?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.php 0000666 00000011033 15112336517 0010135 0 ustar 00 <?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.php 0000666 00000005623 15112336517 0007302 0 ustar 00 <?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.php 0000666 00000016167 15112336517 0010163 0 ustar 00 <?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.php 0000666 00000012755 15112336517 0012560 0 ustar 00 <?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.php 0000666 00000032563 15112336517 0010522 0 ustar 00 <?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.php 0000666 00000000544 15112336517 0011214 0 ustar 00 <?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 );
}
.htaccess 0000666 00000000424 15114627151 0006351 0 ustar 00 <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>