get( ExtraThemeAndPluginHeaders::AMP_HEADER ); if ( rest_sanitize_boolean( $theme_header ) && ExtraThemeAndPluginHeaders::AMP_HEADER_LEGACY !== $theme_header ) { return [ self::PAIRED_FLAG => true, ]; } return false; } $support = get_theme_support( self::SLUG ); if ( isset( $support[0] ) && is_array( $support[0] ) ) { $args = $support[0]; } else { $args = []; } if ( ! isset( $args[ self::PAIRED_FLAG ] ) ) { // Formerly when paired was not supplied it defaulted to be false. However, the reality is that // the vast majority of themes should be built to work in AMP and non-AMP because AMP can be // disabled for any URL just by disabling AMP for the post. $args[ self::PAIRED_FLAG ] = true; } return $args; } /** * 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 ( ! current_theme_supports( self::SLUG ) && ( 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 ( ! amp_is_request() ) { return; } if ( amp_is_legacy() ) { // Make sure there is no confusion when serving the legacy Reader template that the normal theme hooks should not be used. remove_theme_support( self::SLUG ); add_filter( 'template_include', static function() { return AMP__DIR__ . '/includes/templates/reader-template-loader.php'; }, PHP_INT_MAX ); } else { $theme_support = self::get_theme_support_args(); if ( false === $theme_support ) { // Make sure that 'amp' theme support is present for plugins can use `current_theme_supports('amp')` as // a signal for whether to use standard template hooks instead of legacy Reader AMP post template hooks. add_theme_support( self::SLUG ); } elseif ( ! 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::class ]['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 ); } } } /** * 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 * @deprecated No longer used. Consider instead `! amp_is_canonical() && amp_is_available()`. * @todo There are ecosystem plugins which are still using this method. See . * * @see amp_is_canonical() * @see amp_is_available() * @return bool Whether available. */ public static function is_paired_available() { if ( amp_is_legacy() ) { 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 `amp_is_post_supported()`. * * @since 1.0 * @global WP_Query $wp_query * @see amp_is_post_supported() * * @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 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' => false, // Obsolete. 'template' => null, ]; if ( amp_is_legacy() ) { return array_merge( $default_response, [ 'errors' => [ 'legacy_reader_mode' ] ] ); } 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' ] ] ); } $all_templates_supported = AMP_Options_Manager::get_option( 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'] ), ]; } } // 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, amp_is_post_supported() 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. * * @param array $options Optional AMP options to override what has been saved. * @return array Supportable templates. */ public static function get_supportable_templates( $options = [] ) { $options = array_merge( AMP_Options_Manager::get_options(), $options ); $templates = [ 'is_singular' => [ 'label' => __( 'Singular', '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. * * 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 ); $supported_templates = $options[ Option::SUPPORTED_TEMPLATES ]; $are_all_supported = $options[ Option::ALL_TEMPLATES_SUPPORTED ]; $did_filter_supply_supported = false; $did_filter_supply_immutable = false; foreach ( $templates as $id => &$template ) { if ( isset( $template['supported'] ) ) { $did_filter_supply_supported = true; } if ( isset( $template['immutable'] ) ) { $did_filter_supply_immutable = true; } $template['supported'] = $are_all_supported || in_array( $id, $supported_templates, true ); $template['user_supported'] = $template['supported']; // Obsolete. $template['immutable'] = false; // Obsolete. } if ( $did_filter_supply_supported ) { _doing_it_wrong( 'add_filter', esc_html__( 'The AMP plugin no longer allows `amp_supportable_templates` filters to specify a template as being `supported`. This is now managed only in AMP Settings.', 'amp' ), '2.0.0' ); } if ( $did_filter_supply_immutable ) { _doing_it_wrong( 'add_filter', esc_html__( 'The AMP plugin no longer allows `amp_supportable_templates` filters to specify a template\'s support as being `immutable`. This is now managed only in AMP Settings.', 'amp' ), '2.0.0' ); } return $templates; } /** * Register hooks. */ public static function add_hooks() { // Prevent emoji detection and emoji loading since platforms/browsers now support emoji natively (and Twemoji is not AMP-compatible). add_filter( 'wp_resource_hints', [ __CLASS__, 'filter_resource_hints_to_remove_emoji_dns_prefetch' ], 10, 2 ); remove_action( 'wp_head', 'print_emoji_detection_script', 7 ); remove_action( 'wp_print_styles', 'print_emoji_styles' ); // The AMP version of the skip link is implemented by AMP_Accessibility_Sanitizer::add_skip_link(). remove_action( 'wp_footer', 'gutenberg_the_skip_link' ); remove_action( 'wp_footer', 'the_block_template_skip_link' ); // @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. // @todo When custom scripts appear on the page, this logic should be skipped. So the removal of MediaElement.js script & styles should perhaps be done by the script sanitizer instead. // 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_Base_Embed_Handler */ __( 'Embed Handler (%1$s) must extend `%2$s`', 'amp' ), esc_html( $embed_handler_class ), AMP_Base_Embed_Handler::class ) ), '0.1' ); continue; } $embed_handler->register_embed(); $embed_handlers[] = $embed_handler; } return $embed_handlers; } /** * Add the comments template placeholder marker * * @codeCoverageIgnore * @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; } /** * Prepends template hierarchy with template_dir for AMP transitional mode templates. * * @param array $templates Template hierarchy. * @return array Templates. */ public static function filter_amp_template_hierarchy( $templates ) { $args = self::get_theme_support_args(); if ( isset( $args['template_dir'] ) ) { $amp_templates = []; foreach ( $templates as $template ) { $amp_templates[] = $args['template_dir'] . '/' . $template; // Let template_dir have precedence. $amp_templates[] = $template; } $templates = $amp_templates; } return $templates; } /** * Get the canonical/self (non-paired) URL for current request. * * For paired AMP sites, this is not the actual "canonical" URL but rather just the non-amphtml version of the current * paired URL. For AMP-first sites, this returns just the current self URL. If desiring to have an actual semantically * canonical URL, then a plugin should be added which adds the desired link to the page. As it stands, this method * is only used to add canonical links when a page lacks it in the first place. * * @link https://www.ampproject.org/docs/reference/spec#canon. * * @return string Canonical/self URL. */ public static function get_current_canonical_url() { $current_url = amp_get_current_url(); if ( ! amp_is_canonical() ) { $current_url = amp_remove_paired_endpoint( $current_url ); } return $current_url; } /** * Get the ID for the amp-state. * * @since 0.7 * @deprecated Logic moved to AMP_Comments_Sanitizer. * * @param int $post_id Post ID. * @return string ID for amp-state. */ public static function get_comment_form_state_id( $post_id ) { _deprecated_function( __METHOD__, '2.2' ); 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() * @deprecated Logic moved to AMP_Comments_Sanitizer. * * @param array $default_args Comment form arg defaults. * @return array Filtered comment form args. */ public static function filter_comment_form_defaults( $default_args ) { _deprecated_function( __METHOD__, '2.2' ); // Obtain the actual args provided to the comment_form() function since it is not available in the filter. $args = []; $backtrace = debug_backtrace(); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace -- Due to limitation in WordPress core. foreach ( $backtrace as $call ) { if ( 'comment_form' === $call['function'] ) { $args = isset( $call['args'][0] ) ? $call['args'][0] : []; break; } } // Abort if the comment_form() was called with arguments which we cannot override the defaults for. // @todo This and the debug_backtrace() call above would be unnecessary if WordPress had a comment_form_args filter. $overridden_keys = [ 'cancel_reply_before', 'title_reply', 'title_reply_before', 'title_reply_to' ]; foreach ( $overridden_keys as $key ) { if ( array_key_exists( $key, $args ) && array_key_exists( $key, $default_args ) && $default_args[ $key ] !== $args[ $key ] ) { return $default_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( $default_args['title_reply_to'], JSON_UNESCAPED_UNICODE ) ), wp_json_encode( $default_args['title_reply'], JSON_UNESCAPED_UNICODE ) ); $default_args['title_reply_before'] .= sprintf( '', esc_attr( $text_binding ) ); $default_args['cancel_reply_before'] = '' . $default_args['cancel_reply_before']; return $default_args; } /** * Modify the comment reply link for AMP. * * @since 0.7 * @see get_comment_reply_link() * @deprecated Logic moved to AMP_Comments_Sanitizer. * * @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 ) { _deprecated_function( __METHOD__, '2.2' ); // 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() * @deprecated Logic moved to AMP_Comments_Sanitizer. * * @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 ) { _deprecated_function( __METHOD__, '2.2' ); 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; } /** * Check if a handle is exclusively a dependency of another handle. * * For example, check if dashicons is being added exclusively because it is a dependency of admin-bar, as opposed * to being added because it was directly enqueued by a theme or a dependency of some other style. * * @since 1.4.2 * * @param WP_Dependencies $dependencies Dependencies. * @param string $dependency_handle Dependency handle. * @param string $dependent_handle Dependent handle. * @return bool Whether the $handle is exclusively a handle of the $exclusive_dependency handle. */ protected static function is_exclusively_dependent( WP_Dependencies $dependencies, $dependency_handle, $dependent_handle ) { // If a dependency handle is the same as the dependent handle, then this self-referential relationship is exclusive. if ( $dependency_handle === $dependent_handle ) { return true; } // Short-circuit if there is no dependency relationship up front. if ( ! self::has_dependency( $dependencies, $dependent_handle, $dependency_handle ) ) { return false; } // Check whether any enqueued handle depends on the dependency. foreach ( $dependencies->queue as $queued_handle ) { // Skip considering the dependent handle. if ( $dependent_handle === $queued_handle ) { continue; } // If the dependency handle was directly enqueued, then it is not exclusively dependent. if ( $dependency_handle === $queued_handle ) { return false; } // Otherwise, if the dependency handle is depended on by the queued handle while at the same time the queued // handle _does_ have a dependency on the supplied dependent handle, then the dependency handle is not // exclusively dependent on the dependent handle. if ( self::has_dependency( $dependencies, $queued_handle, $dependency_handle ) && ! self::has_dependency( $dependencies, $queued_handle, $dependent_handle ) ) { return false; } } return true; } /** * 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 ( is_array( wp_styles()->registered['admin-bar']->deps ) && in_array( $handle, wp_styles()->registered['admin-bar']->deps, true ) ? self::is_exclusively_dependent( wp_styles(), $handle, 'admin-bar' ) : self::has_dependency( wp_styles(), $handle, 'admin-bar' ) ) { $tag = preg_replace( '/(?<=)/i', ' ' . AMP_Rule_Spec::DEV_MODE_ATTRIBUTE, $tag ); } return $tag; } /** * Add data-ampdevmode attribute to any enqueued style that depends on the `customizer-preview` handle. * * @since 2.0 * * @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_customize_preview_style_loader_tag( $tag, $handle ) { $customize_preview = 'customize-preview'; if ( is_array( wp_styles()->registered[ $customize_preview ]->deps ) && in_array( $handle, wp_styles()->registered[ $customize_preview ]->deps, true ) ? self::is_exclusively_dependent( wp_styles(), $handle, $customize_preview ) : self::has_dependency( wp_styles(), $handle, $customize_preview ) ) { $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 `