You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

439 lines
13 KiB

<?php
/**
* YoastSEO_AMP_Glue plugin file.
*
* @package YoastSEO_AMP_Glue\Frontend
* @author Joost de Valk
* @copyright 2016 Yoast BV
* @license GPL-2.0+
*/
if ( ! class_exists( 'YoastSEO_AMP_Frontend' ) ) {
/**
* This class improves upon the AMP output by the default WordPress AMP plugin using Yoast SEO metadata.
*/
class YoastSEO_AMP_Frontend {
/**
* WPSEO_Frontend singleton instance.
*
* @var WPSEO_Frontend
*/
private $front;
/**
* YoastSEO_AMP_Glue options.
*
* @var array
*/
private $options;
/**
* All WPSEO options.
*
* @var array
*/
private $wpseo_options;
/**
* YoastSEO_AMP_Frontend constructor.
*/
public function __construct() {
$this->set_options();
add_action( 'amp_init', array( $this, 'post_types' ) );
add_action( 'amp_post_template_css', array( $this, 'additional_css' ) );
add_action( 'amp_post_template_head', array( $this, 'extra_head' ) );
add_action( 'amp_post_template_footer', array( $this, 'extra_footer' ) );
add_filter( 'amp_post_template_data', array( $this, 'fix_amp_post_data' ) );
add_filter( 'amp_post_template_metadata', array( $this, 'fix_amp_post_metadata' ), 10, 2 );
add_filter( 'amp_post_template_analytics', array( $this, 'analytics' ) );
add_filter( 'amp_content_sanitizers', array( $this, 'add_sanitizer' ) );
}
/**
* Retrieve the plugin options and set the relevant properties.
*
* @return void
*/
private function set_options() {
$this->wpseo_options = WPSEO_Options::get_all();
$this->options = YoastSEO_AMP_Options::get();
}
/**
* Adds the blacklist sanitizer to the array of available sanitizers.
*
* @param array $sanitizers The current list of sanitizers.
*
* @return array The new array of sanitizers.
*/
public function add_sanitizer( $sanitizers ) {
require_once 'blacklist-sanitizer.php';
$sanitizers['Yoast_AMP_Blacklist_Sanitizer'] = array();
return $sanitizers;
}
/**
* Outputs the analytics tracking, if it has been set.
*
* @param array $analytics The available analytics options.
*
* @return array The analytics tracking code to output.
*/
public function analytics( $analytics ) {
// If Monster Insights is outputting analytics, don't do anything.
if ( ! empty( $analytics['monsterinsights-googleanalytics'] ) ) {
// Clear analytics-extra options because Monster Insights is taking care of everything.
$this->options['analytics-extra'] = '';
return $analytics;
}
if ( ! empty( $this->options['analytics-extra'] ) ) {
return $analytics;
}
if ( ! class_exists( 'Yoast_GA_Options' ) || Yoast_GA_Options::instance()->get_tracking_code() === null ) {
return $analytics;
}
$tracking_code = Yoast_GA_Options::instance()->get_tracking_code();
$analytics['yst-googleanalytics'] = array(
'type' => 'googleanalytics',
'attributes' => array(),
'config_data' => array(
'vars' => array(
'account' => $tracking_code,
),
'triggers' => array(
'trackPageview' => array(
'on' => 'visible',
'request' => 'pageview',
),
),
),
);
return $analytics;
}
/**
* Enables AMP for all the post types we want it for.
*
* @return void
*/
public function post_types() {
$post_types = get_post_types( array( 'public' => true ), 'objects' );
if ( is_array( $post_types ) && $post_types !== array() ) {
foreach ( $post_types as $post_type ) {
$post_type_name = $post_type->name;
if ( ! isset( $this->options[ 'post_types-' . $post_type_name . '-amp' ] ) ) {
continue;
}
// If AMP page support is not present, don't allow enabling it here.
if ( 'page' === $post_type_name && ! post_type_supports( 'page', AMP_QUERY_VAR ) ) {
continue;
}
if ( $this->options[ 'post_types-' . $post_type_name . '-amp' ] === 'on' ) {
add_post_type_support( $post_type_name, AMP_QUERY_VAR );
continue;
}
if ( 'post' === $post_type_name ) {
add_action( 'wp', array( $this, 'disable_amp_for_posts' ) );
continue;
}
remove_post_type_support( $post_type_name, AMP_QUERY_VAR );
}
}
}
/**
* Disables AMP for posts specifically.
*
* {@internal Runs later because of AMP plugin internals.}
*
* @return void
*/
public function disable_amp_for_posts() {
remove_post_type_support( 'post', AMP_QUERY_VAR );
}
/**
* Transforms the site's canonical URL and site icon URL and to be AMP compliant.
*
* Also ensures that the proper analytics script is loaded (if applicable).
*
* @param array $data The current post data.
*
* @return array The transformed post data.
*/
public function fix_amp_post_data( $data ) {
if ( ! $this->front ) {
$this->front = WPSEO_Frontend::get_instance();
}
$data['canonical_url'] = $this->front->canonical( false );
if ( ! empty( $this->options['amp_site_icon'] ) ) {
$data['site_icon_url'] = $this->options['amp_site_icon'];
}
// If we are loading extra analytics, we need to load the module too.
if ( ! empty( $this->options['analytics-extra'] ) ) {
$data['amp_component_scripts']['amp-analytics'] = 'https://cdn.ampproject.org/v0/amp-analytics-0.1.js';
}
return $data;
}
/**
* Transforms the site's organization object, site description and post image to be AMP compliant.
*
* @param array $metadata The meta data to transform.
* @param WP_Post $post The post to transform the meta data for.
*
* @return array The transformed post meta data.
*/
public function fix_amp_post_metadata( $metadata, $post ) {
if ( ! $this->front ) {
$this->front = WPSEO_Frontend::get_instance();
}
$this->build_organization_object( $metadata );
$desc = $this->front->metadesc( false );
if ( $desc ) {
$metadata['description'] = $desc;
}
$image = isset( $metadata['image'] ) ? $metadata['image'] : null;
$metadata['image'] = $this->get_image( $post, $image );
$metadata['@type'] = $this->get_post_schema_type( $post );
return $metadata;
}
/**
* Adds additional CSS to the AMP output.
*
* @return void
*/
public function additional_css() {
require 'views/additional-css.php';
$selectors = $this->get_class_selectors();
$css_builder = new YoastSEO_AMP_CSS_Builder();
$css_builder->add_option( 'header-color', $selectors['header-color'], 'background' );
$css_builder->add_option( 'headings-color', $selectors['headings-color'], 'color' );
$css_builder->add_option( 'text-color', $selectors['text-color'], 'color' );
$css_builder->add_option( 'blockquote-bg-color', $selectors['blockquote-bg-color'], 'background-color' );
$css_builder->add_option( 'blockquote-border-color', $selectors['blockquote-border-color'], 'border-color' );
$css_builder->add_option( 'blockquote-text-color', $selectors['blockquote-text-color'], 'color' );
$css_builder->add_option( 'link-color', $selectors['link-color'], 'color' );
$css_builder->add_option( 'link-color-hover', $selectors['link-color-hover'], 'color' );
$css_builder->add_option( 'meta-color', $selectors['meta-color'], 'color' );
echo $css_builder->build();
if ( ! empty( $this->options['extra-css'] ) ) {
$safe_text = strip_tags( $this->options['extra-css'] );
$safe_text = wp_check_invalid_utf8( $safe_text );
$safe_text = _wp_specialchars( $safe_text, ENT_NOQUOTES );
echo $safe_text;
}
}
/**
* Outputs extra code in the head, if set.
*
* @return void
*/
public function extra_head() {
$options = WPSEO_Options::get_option( 'wpseo_social' );
if ( $options['twitter'] === true ) {
WPSEO_Twitter::get_instance();
}
if ( $options['opengraph'] === true ) {
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- WPSEO global var.
$GLOBALS['wpseo_og'] = new WPSEO_OpenGraph();
}
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPSEO hook.
do_action( 'wpseo_opengraph' );
echo strip_tags( $this->options['extra-head'], '<link><meta>' );
}
/**
* Outputs analytics code in the footer, if set.
*
* @return void
*/
public function extra_footer() {
echo $this->options['analytics-extra'];
}
/**
* Builds the organization object if needed.
*
* @param array $metadata The data to base the organization object on.
*
* @return void
*/
private function build_organization_object( &$metadata ) {
// While it's using the blog name, it's actually outputting the company name.
if ( ! empty( $this->wpseo_options['company_name'] ) ) {
$metadata['publisher']['name'] = $this->wpseo_options['company_name'];
}
// The logo needs to be 600px wide max, 60px high max.
$logo = $this->get_image_object( $this->wpseo_options['company_logo'], array( 600, 60 ) );
if ( is_array( $logo ) ) {
$metadata['publisher']['logo'] = $logo;
}
}
/**
* Builds an image object array from an image URL.
*
* @param string $image_url Image URL to build URL for.
* @param string|array $size Optional. Image size. Accepts any valid image size, or an array of width
* and height values in pixels (in that order). Default 'full'.
*
* @return array|false The image object array or false if the image URL is empty.
*/
private function get_image_object( $image_url, $size = 'full' ) {
if ( empty( $image_url ) ) {
return false;
}
$image_id = attachment_url_to_postid( $image_url );
$image_src = wp_get_attachment_image_src( $image_id, $size );
if ( is_array( $image_src ) ) {
return array(
'@type' => 'ImageObject',
'url' => $image_src[0],
'width' => $image_src[1],
'height' => $image_src[2],
);
}
return false;
}
/**
* Retrieves the Schema.org image for the passed post.
*
* If an OpenGraph image is available for the post, that one will be used. Otherwise, the default image is used.
* If neither exist, the passed image is used instead.
*
* @param WP_Post $post The post to retrieve the image for.
* @param string|string[]|array|array[]|null $image The currently set post image(s). Can be either a URL string,
* an array of URL strings, an array as a single ImageObject,
* or an array of multiple ImageObject arrays. Null if none set.
*
* @return string|string[]|array|array[]|null The Schema.org-compliant image for the post.
*/
private function get_image( $post, $image ) {
$og_image = $this->get_image_object( WPSEO_Meta::get_value( 'opengraph-image', $post->ID ) );
if ( is_array( $og_image ) ) {
return $og_image;
}
// Posts without an image fail validation in Google, leading to Search Console errors.
if ( empty( $image ) && ! empty( $this->options['default_image'] ) ) {
$default_image = $this->get_image_object( $this->options['default_image'] );
if ( is_array( $default_image ) ) {
return $default_image;
}
}
return $image;
}
/**
* Gets the Schema.org type for the post, based on the post type.
*
* @param WP_Post $post The post to retrieve the data for.
*
* @return string The Schema.org type.
*/
private function get_post_schema_type( $post ) {
$type = 'WebPage';
if ( 'post' === $post->post_type ) {
$type = 'Article';
}
/**
* Filter: 'yoastseo_amp_schema_type' - Allow changing the Schema.org type for the post.
*
* @api string $type The Schema.org type for the $post.
*
* @param WP_Post $post
*/
$type = apply_filters( 'yoastseo_amp_schema_type', $type, $post );
return $type;
}
/**
* Gets the class names used by the AMP plugin.
*
* The AMP plugin changed the class names for a number of selectors between releases.
* This method makes sure the correct CSS class name is used depending on the used version of the AMP plugin.
*
* @return array The version dependent class names.
*/
private function get_class_selectors() {
$selectors = array(
'header-color' => 'nav.amp-wp-title-bar',
'headings-color' => '.amp-wp-title, h2, h3, h4',
'text-color' => '.amp-wp-content',
'blockquote-bg-color' => '.amp-wp-content blockquote',
'blockquote-border-color' => '.amp-wp-content blockquote',
'blockquote-text-color' => '.amp-wp-content blockquote',
'link-color' => 'a, a:active, a:visited',
'link-color-hover' => 'a:hover, a:focus',
'meta-color' => '.amp-wp-meta li, .amp-wp-meta li a',
);
// CSS classnames have been changed in version 0.4.0.
if ( version_compare( AMP__VERSION, '0.4.0', '>=' ) ) {
$selectors_v4 = array(
'header-color' => 'header.amp-wp-header, html',
'text-color' => 'div.amp-wp-article',
'blockquote-bg-color' => '.amp-wp-article-content blockquote',
'blockquote-border-color' => '.amp-wp-article-content blockquote',
'blockquote-text-color' => '.amp-wp-article-content blockquote',
'meta-color' => '.amp-wp-meta, .amp-wp-meta a',
);
$selectors = array_merge( $selectors, $selectors_v4 );
}
return $selectors;
}
}
}