$is_paired, ] ); self::$support_added_via_option = $is_paired ? self::TRANSITIONAL_MODE_SLUG : self::STANDARD_MODE_SLUG; } elseif ( AMP_Validation_Manager::is_theme_support_forced() ) { self::$support_added_via_option = self::STANDARD_MODE_SLUG; 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 [ self::PAIRED_FLAG => false, ]; } if ( ! isset( $support[0] ) || ! is_array( $support[0] ) ) { return []; } return $support[0]; } /** * Gets whether the parent or child theme supports Reader Mode. * * True if the theme does not call add_theme_support( 'amp' ) at all, * and it has an amp/ directory for templates. * * @return bool Whether the theme supports Reader Mode. */ public static function supports_reader_mode() { return ( ! self::get_support_mode_added_via_theme() && ( is_dir( trailingslashit( get_template_directory() ) . self::READER_MODE_TEMPLATE_DIRECTORY ) || is_dir( trailingslashit( get_stylesheet_directory() ) . self::READER_MODE_TEMPLATE_DIRECTORY ) ) ); } /** * 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( [ $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() || is_singular( AMP_Story_Post_Type::POST_TYPE_SLUG ) ) { /* * When AMP-first/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", [ __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 = [ 'errors' => [], '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, [ 'errors' => [ 'no_query_available' ] ] ); } $theme_support_args = self::get_theme_support_args(); if ( false === $theme_support_args ) { return array_merge( $default_response, [ 'errors' => [ '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, [ 'errors' => [ 'no-post-support' ], 'supported' => false, 'immutable' => true, ] ); } $response = array_merge( $default_response, [ '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 = []; $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( [ $query, $callback ] ); } elseif ( is_callable( $callback ) ) { $is_match = $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 ] = [ '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']; /* * If the parent is not amongst the supportable templates, then something is off in terms of hierarchy. * Either the matching is off-track, or the template is badly configured. */ if ( ! array_key_exists( $parent, $supportable_templates ) ) { _doing_it_wrong( __METHOD__, esc_html( sprintf( /* translators: %s: amp_supportable_templates */ __( 'An expected parent was not found. Did you filter %s to not honor the template hierarchy?', 'amp' ), 'amp_supportable_templates' ) ), '1.4' ); break; } $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 = [ '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 = []; 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 = [ $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, [ 'supported' => true, ] ); } return array_merge( $default_response, [ 'errors' => [ '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 = [ 'is_singular' => [ 'label' => __( 'Singular', 'amp' ), 'description' => __( 'Required for the above content types.', 'amp' ), ], ]; if ( 'page' === get_option( 'show_on_front' ) ) { $templates['is_front_page'] = [ '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'] = [ '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'] = [ 'label' => __( 'Homepage', 'amp' ), ]; } $templates = array_merge( $templates, [ 'is_archive' => [ 'label' => __( 'Archives', 'amp' ), ], 'is_author' => [ 'label' => __( 'Author', 'amp' ), 'parent' => 'is_archive', ], 'is_date' => [ 'label' => __( 'Date', 'amp' ), 'parent' => 'is_archive', ], 'is_search' => [ 'label' => __( 'Search', 'amp' ), ], 'is_404' => [ 'label' => __( 'Not Found (404)', 'amp' ), ], ] ); if ( taxonomy_exists( 'category' ) ) { $templates['is_category'] = [ 'label' => get_taxonomy( 'category' )->labels->name, 'parent' => 'is_archive', ]; } if ( taxonomy_exists( 'post_tag' ) ) { $templates['is_tag'] = [ 'label' => get_taxonomy( 'post_tag' )->labels->name, 'parent' => 'is_archive', ]; } $taxonomy_args = [ '_builtin' => false, 'public' => true, ]; foreach ( get_taxonomies( $taxonomy_args, 'objects' ) as $taxonomy ) { $templates[ sprintf( 'is_tax[%s]', $taxonomy->name ) ] = [ 'label' => $taxonomy->labels->name, 'parent' => 'is_archive', 'callback' => static function ( WP_Query $query ) use ( $taxonomy ) { return $query->is_tax( $taxonomy->name ); }, ]; } $post_type_args = [ 'has_archive' => true, 'public' => true, ]; foreach ( get_post_types( $post_type_args, 'objects' ) as $post_type ) { $templates[ sprintf( 'is_post_type_archive[%s]', $post_type->name ) ] = [ 'label' => $post_type->labels->archives, 'parent' => 'is_archive', 'callback' => static 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 = []; 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() { // Remove core actions which are invalid AMP. remove_action( 'wp_head', 'wp_post_preview_js', 1 ); remove_action( 'wp_head', 'wp_oembed_add_host_js' ); // Replace JS-based emoji with PHP-based, if the JS-based emoji replacement was not already removed. if ( has_action( 'wp_head', 'print_emoji_detection_script' ) ) { remove_action( 'wp_head', 'print_emoji_detection_script', 7 ); remove_action( 'wp_print_styles', 'print_emoji_styles' ); add_action( 'wp_print_styles', [ __CLASS__, 'print_emoji_styles' ] ); add_filter( 'the_title', 'wp_staticize_emoji' ); add_filter( 'the_excerpt', 'wp_staticize_emoji' ); add_filter( 'the_content', 'wp_staticize_emoji' ); add_filter( 'comment_text', 'wp_staticize_emoji' ); add_filter( 'widget_text', 'wp_staticize_emoji' ); } // @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', static function() { return 'amp'; } ); add_filter( 'wp_audio_shortcode_library', static function() { return 'amp'; } ); // Don't show loading indicator on custom logo since it makes most sense for larger images. add_filter( 'get_custom_logo', static function( $html ) { return preg_replace( '/(?<=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 = []; 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( [ 'content_max_width' => ! empty( $content_width ) ? $content_width : AMP_Post_Template::CONTENT_MAX_WIDTH, // Back-compat. ], $args ) ); if ( ! $embed_handler instanceof 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; } /** * Amend the comment form with the redirect_to field to persist the AMP page after submission. */ 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'], JSON_UNESCAPED_UNICODE ) ), wp_json_encode( $args['title_reply'], JSON_UNESCAPED_UNICODE ) ); $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 = [ $state_id => [ 'replyToName' => $comment->comment_author, 'values' => [ '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 ) { if ( empty( $text ) ) { $text = __( 'Click here to cancel reply.', 'default' ); } $state_id = self::get_comment_form_state_id( get_the_ID() ); $tap_state = [ $state_id => [ 'replyToName' => '', 'values' => [ '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, JSON_UNESCAPED_UNICODE ) ) ), esc_html( $text ) ); } /** * Configure the admin bar for AMP. * * @since 1.0 */ public static function init_admin_bar() { add_filter( 'style_loader_tag', [ __CLASS__, 'filter_admin_bar_style_loader_tag' ], 10, 2 ); add_filter( 'script_loader_tag', [ __CLASS__, 'filter_admin_bar_script_loader_tag' ], 10, 2 ); // Inject the data-ampdevmode attribute into the admin bar bump style. See \WP_Admin_Bar::initialize(). if ( current_theme_supports( 'admin-bar' ) ) { $admin_bar_args = get_theme_support( 'admin-bar' ); $header_callback = $admin_bar_args[0]['callback']; } else { $header_callback = '_admin_bar_bump_cb'; } remove_action( 'wp_head', $header_callback ); if ( '__return_false' !== $header_callback ) { ob_start(); $header_callback(); $style = ob_get_clean(); $data = trim( preg_replace( '#]*>(.*)#is', '$1', $style ) ); // See wp_add_inline_style(). // Override AMP's position:relative on the body for the sake of the AMP viewer, which is not relevant an an Admin Bar context. if ( amp_is_dev_mode() ) { $data .= 'html:not(#_) > body { position:unset !important; }'; } wp_add_inline_style( 'admin-bar', $data ); } // Emulate customize support script in PHP, to assume Customizer. add_action( 'admin_bar_menu', static function() { remove_action( 'wp_before_admin_bar_render', 'wp_customize_support_script' ); }, 41 ); add_filter( 'body_class', static function( $body_classes ) { return array_merge( array_diff( $body_classes, [ 'no-customize-support' ] ), [ 'customize-support' ] ); } ); } /** * Recursively determine if a given dependency depends on another. * * @since 1.3 * * @param WP_Dependencies $dependencies Dependencies. * @param string $current_handle Current handle. * @param string $dependency_handle Dependency handle. * @return bool Whether the current handle is a dependency of the dependency handle. */ protected static function has_dependency( WP_Dependencies $dependencies, $current_handle, $dependency_handle ) { if ( $current_handle === $dependency_handle ) { return true; } if ( ! isset( $dependencies->registered[ $current_handle ] ) ) { return false; } foreach ( $dependencies->registered[ $current_handle ]->deps as $handle ) { if ( self::has_dependency( $dependencies, $handle, $dependency_handle ) ) { return true; } } return false; } /** * Add data-ampdevmode attribute to any enqueued style that depends on the admin-bar. * * @since 1.3 * * @param string $tag The link tag for the enqueued style. * @param string $handle The style's registered handle. * @return string Tag. */ public static function filter_admin_bar_style_loader_tag( $tag, $handle ) { if ( 'dashicons' === $handle ) { // Conditionally include Dashicons in dev mode only if was included because it is a dependency of admin-bar. $needs_dev_mode = true; foreach ( wp_styles()->queue as $queued_handle ) { if ( // If a theme or plugin directly enqueued dashicons, then it is not added via admin-bar dependency and it is not part of dev mode. 'dashicons' === $queued_handle || // If a stylesheet has dashicons as a dependency without also having admin-bar as a dependency, then no dev mode. ( self::has_dependency( wp_styles(), $queued_handle, 'dashicons' ) && ! self::has_dependency( wp_styles(), $queued_handle, 'admin-bar' ) ) ) { $needs_dev_mode = false; break; } } } else { $needs_dev_mode = self::has_dependency( wp_styles(), $handle, 'admin-bar' ); } if ( $needs_dev_mode ) { $tag = preg_replace( '/(?<=)/i', ' ' . AMP_Rule_Spec::DEV_MODE_ATTRIBUTE, $tag ); } return $tag; } /** * Add data-ampdevmode attribute to any enqueued script that depends on the admin-bar. * * @since 1.3 * * @param string $tag The `