( '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. * Temporary redirect is used for admin users because implied transitional mode and template support can be * enabled by user ay any time, so they will be able to make AMP available for this URL and see the change * without wrestling with the redirect cache. */ if ( isset( $_GET[ amp_get_slug() ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended self::redirect_non_amp_url( current_user_can( 'manage_options' ) ? 302 : 301, true ); } 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() ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended 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. * Permanent redirect is used for unauthenticated users since switching between modes * should happen infrequently. For admin users, this is kept temporary to allow them * to not be hampered by browser remembering permanent redirects and preventing test. */ if ( $has_query_var || $has_url_param ) { return self::redirect_non_amp_url( current_user_can( 'manage_options' ) ? 302 : 301, $exit ); } } else { /* * When in AMP transitional 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 transitional 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 ) { // A temporary redirect is used for admin users to allow them to see changes between reader mode and transitional modes. wp_safe_redirect( $new_url, current_user_can( 'manage_options' ) ? 302 : 301 ); // @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 int $status Status code (301 or 302). * @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_non_amp_url( $status = 302, $exit = true ) { $current_url = amp_get_current_url(); $non_amp_url = amp_remove_endpoint( $current_url ); if ( $non_amp_url === $current_url ) { return false; } wp_safe_redirect( $non_amp_url, $status ); // @codeCoverageIgnoreStart if ( $exit ) { exit; } return true; // @codeCoverageIgnoreEnd } /** * Determines whether transitional 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 ) { // See get_query_template(). $template_type = preg_replace( '|[^a-z0-9-]+|', '', $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; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $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; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited // 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 ] ); } } } // If there is more than 1 matching template, 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'] ); } /* * When there is still more than one matching template, account for ambiguous cases, informed by the order in template-loader.php. * See . */ if ( count( $matching_templates ) > 1 ) { $template_conditional_priority_order = array( 'is_embed', 'is_404', 'is_search', 'is_front_page', 'is_home', 'is_post_type_archive', 'is_tax', 'is_attachment', 'is_single', 'is_page', 'is_singular', 'is_category', 'is_tag', 'is_author', 'is_date', 'is_archive', ); // Obtain the template conditionals for each matching template ID (e.g. 'is_post_type_archive[product]' => 'is_post_type_archive'). $template_conditional_id_mapping = array(); foreach ( array_keys( $matching_templates ) as $template_id ) { $template_conditional_id_mapping[ strtok( $template_id, '[' ) ] = $template_id; } // If there are any custom supportable templates, only consider them since they would override the conditional logic in core. $custom_template_conditions = array_diff( array_keys( $template_conditional_id_mapping ), $template_conditional_priority_order ); if ( ! empty( $custom_template_conditions ) ) { $matching_templates = wp_array_slice_assoc( $matching_templates, array_values( wp_array_slice_assoc( $template_conditional_id_mapping, $custom_template_conditions ) ) ); } else { /* * Otherwise, iterate over the template conditionals in the order they occur in the if/elseif/else conditional chain. * to then populate $matching_templates with just this one entry. */ foreach ( $template_conditional_priority_order as $template_conditional ) { if ( isset( $template_conditional_id_mapping[ $template_conditional ] ) ) { $template_id = $template_conditional_id_mapping[ $template_conditional ]; $matching_templates = array( $template_id => $matching_templates[ $template_id ], ); break; } } } } /* * 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( sprintf( /* translators: %s: amp_supportable_templates */ __( 'Did not expect there to be more than one matching template. Did you filter %s to not honor the template hierarchy?', 'amp' ), 'amp_supportable_templates' ) ), '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(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped }, PHP_INT_MAX ); add_action( 'wp_head', 'amp_add_generator_metadata', 20 ); add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ), 0 ); // Enqueue before theme's styles. 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.Constants.NewConstants.php_int_minFound add_action( 'template_redirect', array( __CLASS__, 'start_output_buffering' ), $priority ); // Commenting hooks. 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' ) ) { _doing_it_wrong( __METHOD__, esc_html( sprintf( /* translators: 1: embed handler. 2: AMP_Embed_Handler */ __( 'Embed Handler (%1$s) must extend `%2$s`', 'amp' ), esc_html( $embed_handler_class ), 'AMP_Embed_Handler' ) ), '0.1' ); continue; } $embed_handler->register_embed(); $embed_handlers[] = $embed_handler; } return $embed_handlers; } /** * Add the comments template placeholder marker * * @deprecated 1.1.0 This functionality was moved to AMP_Comments_Sanitizer * * @param array $args the args for the comments list. * @return array Args to return. */ public static function set_comments_walker( $args ) { _deprecated_function( __METHOD__, '1.1' ); $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() { ?>
query_vars; if ( ! $wp_rewrite->permalink_structure || empty( $wp->request ) ) { $url = home_url( '/' ); } else { $url = home_url( user_trailingslashit( $wp->request ) ); parse_str( $wp->matched_query, $matched_query_vars ); foreach ( $wp->query_vars as $key => $value ) { // Remove query vars that were matched in the rewrite rules for the request. if ( isset( $matched_query_vars[ $key ] ) ) { unset( $added_query_vars[ $key ] ); } } } } if ( ! empty( $added_query_vars ) ) { $url = add_query_arg( $added_query_vars, $url ); } return amp_remove_endpoint( $url ); } /** * Get the ID for the amp-state. * * @since 0.7 * * @param int $post_id Post ID. * @return string ID for amp-state. */ public static function get_comment_form_state_id( $post_id ) { return sprintf( 'commentform_post_%d', $post_id ); } /** * Filter comment form args to an element with [text] AMP binding wrap the title reply. * * @since 0.7 * @see comment_form() * * @param array $args Comment form args. * @return array Filtered comment form args. */ public static function filter_comment_form_defaults( $args ) { $state_id = self::get_comment_form_state_id( get_the_ID() ); $text_binding = sprintf( '%s.replyToName ? %s : %s', $state_id, str_replace( '%s', sprintf( '" + %s.replyToName + "', $state_id ), wp_json_encode( $args['title_reply_to'] ) ), wp_json_encode( $args['title_reply'] ) ); $args['title_reply_before'] .= sprintf( '', esc_attr( $text_binding ) ); $args['cancel_reply_before'] = '' . $args['cancel_reply_before']; return $args; } /** * Modify the comment reply link for AMP. * * @since 0.7 * @see get_comment_reply_link() * * @param string $link The HTML markup for the comment reply link. * @param array $args An array of arguments overriding the defaults. * @param WP_Comment $comment The object of the comment being replied. * @return string Comment reply link. */ public static function filter_comment_reply_link( $link, $args, $comment ) { // Continue to show default link to wp-login when user is not logged-in. if ( get_option( 'comment_registration' ) && ! is_user_logged_in() ) { return $args['before'] . $link . $args['after']; } $state_id = self::get_comment_form_state_id( get_the_ID() ); $tap_state = array( $state_id => array( 'replyToName' => $comment->comment_author, 'values' => array( 'comment_parent' => (string) $comment->comment_ID, ), ), ); // @todo Figure out how to support add_below. Instead of moving the form, what about letting the form get a fixed position? $link = sprintf( '%s', esc_attr( '#' . $args['respond_id'] ), esc_attr( sprintf( 'tap:AMP.setState( %s )', wp_json_encode( $tap_state, JSON_UNESCAPED_UNICODE ) ) ), esc_attr( sprintf( $args['reply_to_text'], $comment->comment_author ) ), $args['reply_text'] ); return $args['before'] . $link . $args['after']; } /** * Filters the cancel comment reply link HTML. * * @since 0.7 * @see get_cancel_comment_reply_link() * * @param string $formatted_link The HTML-formatted cancel comment reply link. * @param string $link Cancel comment reply link URL. * @param string $text Cancel comment reply link text. * @return string Cancel reply link. */ public static function filter_cancel_comment_reply_link( $formatted_link, $link, $text ) { unset( $formatted_link, $link ); if ( empty( $text ) ) { $text = __( 'Click here to cancel reply.', 'default' ); } $state_id = self::get_comment_form_state_id( get_the_ID() ); $tap_state = array( $state_id => array( 'replyToName' => '', 'values' => array( 'comment_parent' => '0', ), ), ); $respond_id = 'respond'; // Hard-coded in comment_form() and default value in get_comment_reply_link(). return sprintf( '%s', esc_url( remove_query_arg( 'replytocom' ) . '#' . $respond_id ), isset( $_GET['replytocom'] ) ? '' : ' hidden', // phpcs:ignore esc_attr( sprintf( '%s.values.comment_parent == "0"', self::get_comment_form_state_id( get_the_ID() ) ) ), esc_attr( sprintf( 'tap:AMP.setState( %s )', wp_json_encode( $tap_state ) ) ), esc_html( $text ) ); } /** * Configure the admin bar for AMP. * * @since 1.0 */ public static function init_admin_bar() { // Replace admin-bar.css in core with forked version which makes use of :focus-within among other change for AMP-compat. wp_styles()->registered['admin-bar']->src = amp_get_asset_url( 'css/admin-bar.css' ); wp_styles()->registered['admin-bar']->ver = AMP__VERSION; // Remove script which is almost entirely made obsolete by :focus-inside in the forked admin-bar.css. wp_dequeue_script( 'admin-bar' ); // Remove customize support script since not valid AMP. add_action( 'admin_bar_menu', function() { remove_action( 'wp_before_admin_bar_render', 'wp_customize_support_script' ); }, 41 ); // Emulate customize support script in PHP, to assume Customizer. add_filter( 'body_class', function( $body_classes ) { return array_merge( array_diff( $body_classes, array( 'no-customize-support' ) ), array( 'customize-support' ) ); } ); } /** * Ensure the markup exists as required by AMP and elements are in the optimal loading order. * * Ensure meta[charset], meta[name=viewport], and link[rel=canonical] exist, as the whitelist sanitizer * may have removed an illegal meta[http-equiv] or meta[name=viewport]. For a singular post, core only outputs a * canonical URL by default. Adds the preload links. * * @since 0.7 * @link https://www.ampproject.org/docs/reference/spec#required-markup * @link https://docs.google.com/document/d/169XUxtSSEJb16NfkrCr9y5lqhUR7vxXEAsNxBzg07fM/edit#heading=h.2ha259c3ffos * @todo All of this might be better placed inside of a sanitizer. * * @param DOMDocument $dom Document. * @param string[] $script_handles AMP script handles for components identified during output buffering. */ public static function ensure_required_markup( DOMDocument $dom, $script_handles = array() ) { /** * Elements. * * @var DOMElement $meta * @var DOMElement $script * @var DOMElement $link */ $xpath = new DOMXPath( $dom ); // Make sure the HEAD element is in the doc. $head = $dom->getElementsByTagName( 'head' )->item( 0 ); if ( ! $head ) { $head = $dom->createElement( 'head' ); $dom->documentElement->insertBefore( $head, $dom->documentElement->firstChild ); } // Ensure there is a schema.org script in the document. // @todo Consider applying the amp_schemaorg_metadata filter on the contents when a script is already present. $schema_org_meta_script = $xpath->query( '//script[ @type = "application/ld+json" ][ contains( ./text(), "schema.org" ) ]' )->item( 0 ); if ( ! $schema_org_meta_script ) { $script = $dom->createElement( 'script' ); $script->setAttribute( 'type', 'application/ld+json' ); $script->appendChild( $dom->createTextNode( wp_json_encode( amp_get_schemaorg_metadata() ) ) ); $head->appendChild( $script ); } // Ensure rel=canonical link. $links = array(); $link_elements = $head->getElementsByTagName( 'link' ); $rel_canonical = null; foreach ( $link_elements as $link ) { if ( $link->hasAttribute( 'rel' ) ) { $links[ $link->getAttribute( 'rel' ) ][] = $link; } } if ( empty( $links['canonical'] ) ) { $rel_canonical = AMP_DOM_Utils::create_node( $dom, 'link', array( 'rel' => 'canonical', 'href' => self::get_current_canonical_url(), ) ); $head->appendChild( $rel_canonical ); } /* * Ensure meta charset and meta viewport are present. * * "AMP is already quite restrictive about which markup is allowed in the section. However, * there are a few basic optimizations that you can apply. The key is to structure the section * in a way so that all render-blocking scripts and custom fonts load as fast as possible." * * "The first tag should be the meta charset tag, followed by any remaining meta tags." * * {@link https://docs.google.com/document/d/169XUxtSSEJb16NfkrCr9y5lqhUR7vxXEAsNxBzg07fM/edit#heading=h.2ha259c3ffos Optimize the AMP Runtime loading} */ $meta_charset = null; $meta_viewport = null; $meta_elements = array(); foreach ( $head->getElementsByTagName( 'meta' ) as $meta ) { if ( $meta->hasAttribute( 'charset' ) ) { // There will not be a meta[http-equiv] because the sanitizer removed it. $meta_charset = $meta; } elseif ( 'viewport' === $meta->getAttribute( 'name' ) ) { $meta_viewport = $meta; } else { $meta_elements[] = $meta; } } if ( ! $meta_charset ) { // Warning: This probably means the character encoding needs to be converted. $meta_charset = AMP_DOM_Utils::create_node( $dom, 'meta', array( 'charset' => 'utf-8', ) ); } else { $head->removeChild( $meta_charset ); // So we can move it. } $head->insertBefore( $meta_charset, $head->firstChild ); if ( ! $meta_viewport ) { $meta_viewport = AMP_DOM_Utils::create_node( $dom, 'meta', array( 'name' => 'viewport', 'content' => 'width=device-width', ) ); } else { $head->removeChild( $meta_viewport ); // So we can move it. } $head->insertBefore( $meta_viewport, $meta_charset->nextSibling ); $previous_node = $meta_viewport; foreach ( $meta_elements as $meta_element ) { $meta_element->parentNode->removeChild( $meta_element ); $head->insertBefore( $meta_element, $previous_node->nextSibling ); $previous_node = $meta_element; } $title = $head->getElementsByTagName( 'title' )->item( 0 ); if ( $title ) { $title->parentNode->removeChild( $title ); // So we can move it. $head->insertBefore( $title, $previous_node->nextSibling ); $previous_node = $title; } // @see https://github.com/ampproject/amphtml/blob/2fd30ca984bceac05905bd5b17f9e0010629d719/src/render-delaying-services.js#L39-L43 AMPHTML Render Delaying Services SERVICES definition. $render_delaying_extensions = array( 'amp-experiment', 'amp-dynamic-css-classes', 'amp-story', ); // Obtain the existing AMP scripts. $amp_scripts = array(); $ordered_scripts = array(); $head_scripts = array(); $runtime_src = wp_scripts()->registered['amp-runtime']->src; foreach ( $head->getElementsByTagName( 'script' ) as $script ) { // Note that prepare_response() already moved body scripts to head. $head_scripts[] = $script; } foreach ( $head_scripts as $script ) { $src = $script->getAttribute( 'src' ); if ( ! $src || 'https://cdn.ampproject.org/' !== substr( $src, 0, 27 ) ) { continue; } if ( $runtime_src === $src ) { $amp_scripts['amp-runtime'] = $script; } elseif ( $script->hasAttribute( 'custom-element' ) ) { $amp_scripts[ $script->getAttribute( 'custom-element' ) ] = $script; } elseif ( $script->hasAttribute( 'custom-template' ) ) { $amp_scripts[ $script->getAttribute( 'custom-template' ) ] = $script; } else { continue; } $script->parentNode->removeChild( $script ); // So we can move it. } // Create scripts for any components discovered from output buffering. foreach ( array_diff( $script_handles, array_keys( $amp_scripts ) ) as $missing_script_handle ) { if ( ! wp_script_is( $missing_script_handle, 'registered' ) ) { continue; } $attrs = array( 'src' => wp_scripts()->registered[ $missing_script_handle ]->src, 'async' => '', ); if ( 'amp-mustache' === $missing_script_handle ) { $attrs['custom-template'] = $missing_script_handle; } else { $attrs['custom-element'] = $missing_script_handle; } $amp_scripts[ $missing_script_handle ] = AMP_DOM_Utils::create_node( $dom, 'script', $attrs ); } /* phpcs:ignore Squiz.PHP.CommentedOutCode.Found * * "Next, preload the AMP runtime v0.js