( 'paired' === $theme_support_option ),
) );
self::$support_added_via_option = true;
} elseif ( AMP_Validation_Manager::is_theme_support_forced() ) {
add_theme_support( self::SLUG );
}
}
/**
* Get the theme support args.
*
* This avoids having to repeatedly call `get_theme_support()`, check the args, shift an item off the array, and so on.
*
* @since 1.0
*
* @return array|false Theme support args, or false if theme support is not present.
*/
public static function get_theme_support_args() {
if ( ! current_theme_supports( self::SLUG ) ) {
return false;
}
$support = get_theme_support( self::SLUG );
if ( true === $support ) {
return array(
'paired' => false,
);
}
if ( ! isset( $support[0] ) || ! is_array( $support[0] ) ) {
return array();
}
return $support[0];
}
/**
* Finish initialization once query vars are set.
*
* @since 0.7
*/
public static function finish_init() {
if ( ! is_amp_endpoint() ) {
// Redirect to AMP-less variable if AMP is not available for this URL and yet the query var is present.
if ( isset( $_GET[ amp_get_slug() ] ) ) { // WPCS: csrf ok.
self::redirect_ampless_url();
}
amp_add_frontend_actions();
return;
}
self::ensure_proper_amp_location();
$theme_support = self::get_theme_support_args();
if ( ! empty( $theme_support['template_dir'] ) ) {
self::add_amp_template_filters();
}
self::add_hooks();
self::$sanitizer_classes = amp_get_content_sanitizers();
self::$sanitizer_classes = AMP_Validation_Manager::filter_sanitizer_args( self::$sanitizer_classes );
self::$embed_handlers = self::register_content_embed_handlers();
self::$sanitizer_classes['AMP_Embed_Sanitizer']['embed_handlers'] = self::$embed_handlers;
foreach ( self::$sanitizer_classes as $sanitizer_class => $args ) {
if ( method_exists( $sanitizer_class, 'add_buffering_hooks' ) ) {
call_user_func( array( $sanitizer_class, 'add_buffering_hooks' ), $args );
}
}
}
/**
* Ensure that the current AMP location is correct.
*
* @since 1.0
*
* @param bool $exit Whether to exit after redirecting.
* @return bool Whether redirection was done. Naturally this is irrelevant if $exit is true.
*/
public static function ensure_proper_amp_location( $exit = true ) {
$has_query_var = false !== get_query_var( amp_get_slug(), false ); // May come from URL param or endpoint slug.
$has_url_param = isset( $_GET[ amp_get_slug() ] ); // WPCS: CSRF OK.
if ( amp_is_canonical() ) {
/*
* When AMP native/canonical, then when there is an /amp/ endpoint or ?amp URL param,
* then a redirect needs to be done to the URL without any AMP indicator in the URL.
*/
if ( $has_query_var || $has_url_param ) {
return self::redirect_ampless_url( $exit );
}
} else {
/*
* When in AMP paired mode *with* theme support, then the proper AMP URL has the 'amp' URL param
* and not the /amp/ endpoint. The URL param is now the exclusive way to mark AMP in paired mode
* when amp theme support present. This is important for plugins to be able to reliably call
* is_amp_endpoint() before the parse_query action.
*/
if ( $has_query_var && ! $has_url_param ) {
$old_url = amp_get_current_url();
$new_url = add_query_arg( amp_get_slug(), '', amp_remove_endpoint( $old_url ) );
if ( $old_url !== $new_url ) {
wp_safe_redirect( $new_url, 302 );
// @codeCoverageIgnoreStart
if ( $exit ) {
exit;
}
return true;
// @codeCoverageIgnoreEnd
}
}
}
return false;
}
/**
* Redirect to non-AMP version of the current URL, such as because AMP is canonical or there are unaccepted validation errors.
*
* If the current URL is already AMP-less then do nothing.
*
* @since 0.7
* @since 1.0 Added $exit param.
* @since 1.0 Renamed from redirect_canonical_amp().
*
* @param bool $exit Whether to exit after redirecting.
* @return bool Whether redirection was done. Naturally this is irrelevant if $exit is true.
*/
public static function redirect_ampless_url( $exit = true ) {
$current_url = amp_get_current_url();
$ampless_url = amp_remove_endpoint( $current_url );
if ( $ampless_url === $current_url ) {
return false;
}
/*
* Temporary redirect because AMP URL may return when blocking validation errors
* occur or when a non-canonical AMP theme is used.
*/
wp_safe_redirect( $ampless_url, 302 );
// @codeCoverageIgnoreStart
if ( $exit ) {
exit;
}
return true;
// @codeCoverageIgnoreEnd
}
/**
* Determines whether paired mode is available.
*
* When 'amp' theme support has not been added or canonical mode is enabled, then this returns false.
*
* @since 0.7
*
* @see amp_is_canonical()
* @return bool Whether available.
*/
public static function is_paired_available() {
if ( ! current_theme_supports( self::SLUG ) ) {
return false;
}
if ( amp_is_canonical() ) {
return false;
}
$availability = self::get_template_availability();
return $availability['supported'];
}
/**
* Determine whether the user is in the Customizer preview iframe.
*
* @since 0.7
*
* @return bool Whether in Customizer preview iframe.
*/
public static function is_customize_preview_iframe() {
global $wp_customize;
return is_customize_preview() && $wp_customize->get_messenger_channel();
}
/**
* Register filters for loading AMP-specific templates.
*/
public static function add_amp_template_filters() {
foreach ( self::$template_types as $template_type ) {
add_filter( "{$template_type}_template_hierarchy", array( __CLASS__, 'filter_amp_template_hierarchy' ) );
}
}
/**
* Determine template availability of AMP for the given query.
*
* This is not intended to return whether AMP is available for a _specific_ post. For that, use `post_supports_amp()`.
*
* @since 1.0
* @global WP_Query $wp_query
* @see post_supports_amp()
*
* @param WP_Query|WP_Post|null $query Query or queried post. If null then the global query will be used.
* @return array {
* Template availability.
*
* @type bool $supported Whether the template is supported in AMP.
* @type bool|null $immutable Whether the supported status is known to be unchangeable.
* @type string|null $template The ID of the matched template (conditional), such as 'is_singular', or null if nothing was matched.
* @type string[] $errors List of the errors or reasons for why the template is not available.
* }
*/
public static function get_template_availability( $query = null ) {
global $wp_query;
if ( ! $query ) {
$query = $wp_query;
} elseif ( $query instanceof WP_Post ) {
$post = $query;
$query = new WP_Query();
if ( 'page' === $post->post_type ) {
$query->set( 'page_id', $post->ID );
} else {
$query->set( 'p', $post->ID );
}
$query->queried_object = $post;
$query->queried_object_id = $post->ID;
$query->parse_query_vars();
}
$default_response = array(
'errors' => array(),
'supported' => false,
'immutable' => null,
'template' => null,
);
if ( ! ( $query instanceof WP_Query ) ) {
_doing_it_wrong( __METHOD__, esc_html__( 'No WP_Query available.', 'amp' ), '1.0' );
return array_merge(
$default_response,
array( 'errors' => array( 'no_query_available' ) )
);
}
$theme_support_args = self::get_theme_support_args();
if ( false === $theme_support_args ) {
return array_merge(
$default_response,
array( 'errors' => array( 'no_theme_support' ) )
);
}
// Support available_callback from 0.7, though it is deprecated.
if ( isset( $theme_support_args['available_callback'] ) && is_callable( $theme_support_args['available_callback'] ) ) {
/**
* Queried object.
*
* @var WP_Post $queried_object
*/
$queried_object = $query->get_queried_object();
if ( ( is_singular() || $query->is_posts_page ) && ! post_supports_amp( $queried_object ) ) {
return array_merge(
$default_response,
array(
'errors' => array( 'no-post-support' ),
'supported' => false,
'immutable' => true,
)
);
}
$response = array_merge(
$default_response,
array(
'supported' => call_user_func( $theme_support_args['available_callback'] ),
'immutable' => true,
)
);
if ( ! $response['supported'] ) {
$response['errors'][] = 'available_callback';
}
return $response;
}
$all_templates_supported_by_theme_support = false;
if ( isset( $theme_support_args['templates_supported'] ) ) {
$all_templates_supported_by_theme_support = 'all' === $theme_support_args['templates_supported'];
}
$all_templates_supported = (
$all_templates_supported_by_theme_support || AMP_Options_Manager::get_option( 'all_templates_supported' )
);
// Make sure global $wp_query is set in case of conditionals that unfortunately look at global scope.
$prev_query = $wp_query;
$wp_query = $query; // WPCS: override ok.
$matching_templates = array();
$supportable_templates = self::get_supportable_templates();
foreach ( $supportable_templates as $id => $supportable_template ) {
if ( empty( $supportable_template['callback'] ) ) {
$callback = $id;
} else {
$callback = $supportable_template['callback'];
}
// If the callback is a method on the query, then call the method on the query itself.
if ( is_string( $callback ) && 'is_' === substr( $callback, 0, 3 ) && method_exists( $query, $callback ) ) {
$is_match = call_user_func( array( $query, $callback ) );
} elseif ( is_callable( $callback ) ) {
$is_match = call_user_func( $callback, $query );
} else {
/* translators: %s: the supportable template ID. */
_doing_it_wrong( __FUNCTION__, esc_html( sprintf( __( 'Supportable template "%s" does not have a callable callback.', 'amp' ), $id ) ), '1.0' );
$is_match = false;
}
if ( $is_match ) {
$matching_templates[ $id ] = array(
'template' => $id,
'supported' => ! empty( $supportable_template['supported'] ),
'immutable' => ! empty( $supportable_template['immutable'] ),
);
}
}
// Restore previous $wp_query (if any).
$wp_query = $prev_query; // WPCS: override ok.
// Make sure children override their parents.
$matching_template_ids = array_keys( $matching_templates );
foreach ( array_diff( array_keys( $supportable_templates ), $matching_template_ids ) as $template_id ) {
unset( $supportable_templates[ $template_id ] );
}
foreach ( $matching_template_ids as $id ) {
$has_children = false;
foreach ( $supportable_templates as $other_id => $supportable_template ) {
if ( $other_id === $id ) {
continue;
}
if ( isset( $supportable_template['parent'] ) && $id === $supportable_template['parent'] ) {
$has_children = true;
break;
}
}
// Delete all matching parent templates since the child will override them.
if ( ! $has_children ) {
$supportable_template = $supportable_templates[ $id ];
while ( ! empty( $supportable_template['parent'] ) ) {
$parent = $supportable_template['parent'];
$supportable_template = $supportable_templates[ $parent ];
// Let the child supported status override the parent's supported status.
unset( $matching_templates[ $parent ] );
}
}
}
// The is_home() condition is the default so discard it if there are other matching templates.
if ( count( $matching_templates ) > 1 && isset( $matching_templates['is_home'] ) ) {
unset( $matching_templates['is_home'] );
}
/*
* If there are more than one matching templates, then something is probably not right.
* Template conditions need to be set up properly to prevent this from happening.
*/
if ( count( $matching_templates ) > 1 ) {
_doing_it_wrong( __METHOD__, esc_html__( 'Did not expect there to be more than one matching template. Did you filter amp_supportable_templates to not honor the template hierarchy?', 'amp' ), '1.0' );
}
$matching_template = array_shift( $matching_templates );
// If there aren't any matching templates left that are supported, then we consider it to not be available.
if ( ! $matching_template ) {
if ( $all_templates_supported ) {
return array_merge(
$default_response,
array(
'supported' => true,
)
);
} else {
return array_merge(
$default_response,
array( 'errors' => array( 'no_matching_template' ) )
);
}
}
$matching_template = array_merge( $default_response, $matching_template );
// If there aren't any matching templates left that are supported, then we consider it to not be available.
if ( empty( $matching_template['supported'] ) ) {
$matching_template['errors'][] = 'template_unsupported';
}
// For singular queries, post_supports_amp() is given the final say.
if ( $query->is_singular() || $query->is_posts_page ) {
/**
* Queried object.
*
* @var WP_Post $queried_object
*/
$queried_object = $query->get_queried_object();
if ( $queried_object instanceof WP_Post ) {
$support_errors = AMP_Post_Type_Support::get_support_errors( $queried_object );
if ( ! empty( $support_errors ) ) {
$matching_template['errors'] = array_merge( $matching_template['errors'], $support_errors );
$matching_template['supported'] = false;
}
}
}
return $matching_template;
}
/**
* Get the templates which can be supported.
*
* @return array Supportable templates.
*/
public static function get_supportable_templates() {
$templates = array(
'is_singular' => array(
'label' => __( 'Singular', 'amp' ),
'description' => __( 'Required for the above content types.', 'amp' ),
),
);
if ( 'page' === get_option( 'show_on_front' ) ) {
$templates['is_front_page'] = array(
'label' => __( 'Homepage', 'amp' ),
'parent' => 'is_singular',
);
if ( AMP_Post_Meta_Box::DISABLED_STATUS === get_post_meta( get_option( 'page_on_front' ), AMP_Post_Meta_Box::STATUS_POST_META_KEY, true ) ) {
/* translators: %s: the URL to the edit post screen. */
$templates['is_front_page']['description'] = sprintf( __( 'Currently disabled at the page level.', 'amp' ), esc_url( get_edit_post_link( get_option( 'page_on_front' ) ) ) );
}
// In other words, same as is_posts_page, *but* it not is_singular.
$templates['is_home'] = array(
'label' => __( 'Blog', 'amp' ),
);
if ( AMP_Post_Meta_Box::DISABLED_STATUS === get_post_meta( get_option( 'page_for_posts' ), AMP_Post_Meta_Box::STATUS_POST_META_KEY, true ) ) {
/* translators: %s: the URL to the edit post screen. */
$templates['is_home']['description'] = sprintf( __( 'Currently disabled at the page level.', 'amp' ), esc_url( get_edit_post_link( get_option( 'page_for_posts' ) ) ) );
}
} else {
$templates['is_home'] = array(
'label' => __( 'Homepage', 'amp' ),
);
}
$templates = array_merge(
$templates,
array(
'is_archive' => array(
'label' => __( 'Archives', 'amp' ),
),
'is_author' => array(
'label' => __( 'Author', 'amp' ),
'parent' => 'is_archive',
),
'is_date' => array(
'label' => __( 'Date', 'amp' ),
'parent' => 'is_archive',
),
'is_search' => array(
'label' => __( 'Search', 'amp' ),
),
'is_404' => array(
'label' => __( 'Not Found (404)', 'amp' ),
),
)
);
if ( taxonomy_exists( 'category' ) ) {
$templates['is_category'] = array(
'label' => get_taxonomy( 'category' )->labels->name,
'parent' => 'is_archive',
);
}
if ( taxonomy_exists( 'post_tag' ) ) {
$templates['is_tag'] = array(
'label' => get_taxonomy( 'post_tag' )->labels->name,
'parent' => 'is_archive',
);
}
$taxonomy_args = array(
'_builtin' => false,
'publicly_queryable' => true,
);
foreach ( get_taxonomies( $taxonomy_args, 'objects' ) as $taxonomy ) {
$templates[ sprintf( 'is_tax[%s]', $taxonomy->name ) ] = array(
'label' => $taxonomy->labels->name,
'parent' => 'is_archive',
'callback' => function ( WP_Query $query ) use ( $taxonomy ) {
return $query->is_tax( $taxonomy->name );
},
);
}
$post_type_args = array(
'has_archive' => true,
'publicly_queryable' => true,
);
foreach ( get_post_types( $post_type_args, 'objects' ) as $post_type ) {
$templates[ sprintf( 'is_post_type_archive[%s]', $post_type->name ) ] = array(
'label' => $post_type->labels->archives,
'parent' => 'is_archive',
'callback' => function ( WP_Query $query ) use ( $post_type ) {
return $query->is_post_type_archive( $post_type->name );
},
);
}
/**
* Filters list of supportable templates.
*
* A theme or plugin can force a given template to be supported or not by preemptively
* setting the 'supported' flag for a given template. Otherwise, if the flag is undefined
* then the user will be able to toggle it themselves in the admin. Each array item should
* have a key that corresponds to a template conditional function. If the key is such a
* function, then the key is used to evaluate whether the given template entry is a match.
* Otherwise, a supportable template item can include a callback value which is used instead.
* Each item needs a 'label' value. Additionally, if the supportable template is a subset of
* another condition (e.g. is_singular > is_single) then this relationship needs to be
* indicated via the 'parent' value.
*
* @since 1.0
*
* @param array $templates Supportable templates.
*/
$templates = apply_filters( 'amp_supportable_templates', $templates );
$theme_support_args = self::get_theme_support_args();
$theme_supported_templates = array();
if ( isset( $theme_support_args['templates_supported'] ) ) {
$theme_supported_templates = $theme_support_args['templates_supported'];
}
$supported_templates = AMP_Options_Manager::get_option( 'supported_templates' );
foreach ( $templates as $id => &$template ) {
// Capture user-elected support from options. This allows us to preserve the original user selection through programmatic overrides.
$template['user_supported'] = in_array( $id, $supported_templates, true );
// Consider supported templates from theme support args.
if ( ! isset( $template['supported'] ) ) {
if ( 'all' === $theme_supported_templates ) {
$template['supported'] = true;
} elseif ( is_array( $theme_supported_templates ) && isset( $theme_supported_templates[ $id ] ) ) {
$template['supported'] = $theme_supported_templates[ $id ];
}
}
// Make supported state immutable if it was programmatically set.
$template['immutable'] = isset( $template['supported'] );
// Set supported state from user preference.
if ( ! $template['immutable'] ) {
$template['supported'] = AMP_Options_Manager::get_option( 'all_templates_supported' ) || $template['user_supported'];
}
}
return $templates;
}
/**
* Register hooks.
*/
public static function add_hooks() {
// Let the AMP plugin manage service worker streaming in the PWA plugin.
remove_action( 'template_redirect', 'WP_Service_Worker_Navigation_Routing_Component::start_output_buffering_stream_fragment', PHP_INT_MAX );
// Remove core actions which are invalid AMP.
remove_action( 'wp_head', 'wp_post_preview_js', 1 );
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'wp_print_styles', 'print_emoji_styles' );
remove_action( 'wp_head', 'wp_oembed_add_host_js' );
// @todo The wp_mediaelement_fallback() should still run to be injected inside of the audio/video generated by wp_audio_shortcode()/wp_video_shortcode() respectively.
// Prevent MediaElement.js scripts/styles from being enqueued.
add_filter( 'wp_video_shortcode_library', function() {
return 'amp';
} );
add_filter( 'wp_audio_shortcode_library', function() {
return 'amp';
} );
// Don't show loading indicator on custom logo since it makes most sense for larger images.
add_filter( 'get_custom_logo', function( $html ) {
return preg_replace( '/(?<= style[amp-boilerplate] and noscript > style[amp-boilerplate])
* in their head tag." {@link https://www.ampproject.org/docs/fundamentals/spec#required-markup AMP Required markup}
*
* After "Specify the tag for your favicon.", then
* "Specify any custom styles by using the ';
}, 0 );
add_action( 'wp_head', function() {
echo amp_get_boilerplate_code(); // WPCS: xss ok.
}, PHP_INT_MAX );
add_action( 'wp_head', 'amp_add_generator_metadata', 20 );
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'dequeue_customize_preview_scripts' ), 1000 );
add_filter( 'customize_partial_render', array( __CLASS__, 'filter_customize_partial_render' ) );
add_action( 'wp_footer', 'amp_print_analytics' );
/*
* Disable admin bar because admin-bar.css (28K) and Dashicons (48K) alone
* combine to surpass the 50K limit imposed for the amp-custom style.
*/
if ( AMP_Options_Manager::get_option( 'disable_admin_bar' ) ) {
add_filter( 'show_admin_bar', '__return_false', 100 );
} else {
add_action( 'admin_bar_init', array( __CLASS__, 'init_admin_bar' ) );
}
/*
* Start output buffering at very low priority for sake of plugins and themes that use template_redirect
* instead of template_include.
*/
$priority = defined( 'PHP_INT_MIN' ) ? PHP_INT_MIN : ~PHP_INT_MAX; // phpcs:ignore PHPCompatibility.PHP.NewConstants.php_int_minFound
add_action( 'template_redirect', array( __CLASS__, 'start_output_buffering' ), $priority );
// Commenting hooks.
add_filter( 'wp_list_comments_args', array( __CLASS__, 'set_comments_walker' ), PHP_INT_MAX );
add_filter( 'comment_form_defaults', array( __CLASS__, 'filter_comment_form_defaults' ) );
add_filter( 'comment_reply_link', array( __CLASS__, 'filter_comment_reply_link' ), 10, 4 );
add_filter( 'cancel_comment_reply_link', array( __CLASS__, 'filter_cancel_comment_reply_link' ), 10, 3 );
add_action( 'comment_form', array( __CLASS__, 'amend_comment_form' ), 100 );
remove_action( 'comment_form', 'wp_comment_form_unfiltered_html_nonce' );
add_filter( 'wp_kses_allowed_html', array( __CLASS__, 'whitelist_layout_in_wp_kses_allowed_html' ), 10 );
add_filter( 'get_header_image_tag', array( __CLASS__, 'amend_header_image_with_video_header' ), PHP_INT_MAX );
add_action( 'wp_print_footer_scripts', function() {
wp_dequeue_script( 'wp-custom-header' );
}, 0 );
add_action( 'wp_enqueue_scripts', function() {
wp_dequeue_script( 'comment-reply' ); // Handled largely by AMP_Comments_Sanitizer and *reply* methods in this class.
} );
// @todo Add character conversion.
}
/**
* Register/override widgets.
*
* @global WP_Widget_Factory
* @return void
*/
public static function register_widgets() {
global $wp_widget_factory;
foreach ( $wp_widget_factory->widgets as $registered_widget ) {
$registered_widget_class_name = get_class( $registered_widget );
if ( ! preg_match( '/^WP_Widget_(.+)$/', $registered_widget_class_name, $matches ) ) {
continue;
}
$amp_class_name = 'AMP_Widget_' . $matches[1];
if ( ! class_exists( $amp_class_name ) || is_a( $amp_class_name, $registered_widget_class_name ) ) {
continue;
}
unregister_widget( $registered_widget_class_name );
register_widget( $amp_class_name );
}
}
/**
* Register content embed handlers.
*
* This was copied from `AMP_Content::register_embed_handlers()` due to being a private method
* and due to `AMP_Content` not being well suited for use in AMP canonical.
*
* @see AMP_Content::register_embed_handlers()
* @global int $content_width
* @return AMP_Base_Embed_Handler[] Handlers.
*/
public static function register_content_embed_handlers() {
global $content_width;
$embed_handlers = array();
foreach ( amp_get_content_embed_handlers() as $embed_handler_class => $args ) {
/**
* Embed handler.
*
* @type AMP_Base_Embed_Handler $embed_handler
*/
$embed_handler = new $embed_handler_class( array_merge(
array(
'content_max_width' => ! empty( $content_width ) ? $content_width : AMP_Post_Template::CONTENT_MAX_WIDTH, // Back-compat.
),
$args
) );
if ( ! is_subclass_of( $embed_handler, 'AMP_Base_Embed_Handler' ) ) {
/* translators: %s is embed handler */
_doing_it_wrong( __METHOD__, esc_html( sprintf( __( 'Embed Handler (%s) must extend `AMP_Embed_Handler`', 'amp' ), $embed_handler_class ) ), '0.1' );
continue;
}
$embed_handler->register_embed();
$embed_handlers[] = $embed_handler;
}
return $embed_handlers;
}
/**
* Add the comments template placeholder marker
*
* @param array $args the args for the comments list..
* @return array Args to return.
*/
public static function set_comments_walker( $args ) {
$amp_walker = new AMP_Comment_Walker();
$args['walker'] = $amp_walker;
return $args;
}
/**
* Adds the form submit success and fail templates.
*/
public static function amend_comment_form() {
?>
{{{message}}}
{{{error}}}