diff --git a/amp.php b/amp.php index ef419328c96..18baaf95225 100644 --- a/amp.php +++ b/amp.php @@ -272,28 +272,67 @@ function amp_correct_query_when_is_front_page( WP_Query $query ) { } /** - * Whether this is in 'canonical mode.' + * Whether this is in 'canonical mode'. * - * Themes can register support for this with `add_theme_support( 'amp' )`. - * Then, this will change the plugin from 'paired mode,' and it won't use its own templates. - * Nor output frontend markup like the 'rel' link. If the theme registers support for AMP with: - * `add_theme_support( 'amp', array( 'template_dir' => 'my-amp-templates' ) )` - * it will retain 'paired mode. + * Themes can register support for this with `add_theme_support( 'amp' )`: * - * @return boolean Whether this is in AMP 'canonical mode'. + * add_theme_support( 'amp' ); + * + * This will serve templates in native AMP by default but the user would be able to change the template mode + * from native to paired in the admin. To force only native to be available, such as when you are using AMP components + * in your theme templates, do: + * + * add_theme_support( 'amp', array( + * 'mode' => 'native', + * ) ); + * + * If you want to force AMP to always be served on a given template, you can use the templates_supported arg, + * for example to always serve the Category template in AMP: + * + * add_theme_support( 'amp', array( + * 'templates_supported' => array( + * 'is_category' => true, + * ), + * ) ); + * + * Or if you want to force AMP to be used on all templates: + * + * add_theme_support( 'amp', array( + * 'templates_supported' => 'all', + * ) ); + * + * @see AMP_Theme_Support::read_theme_support() + * @return boolean Whether this is in AMP 'canonical' mode, that is whether it is native and there is not separate AMP URL current URL. */ function amp_is_canonical() { - $support = get_theme_support( 'amp' ); - if ( true === $support ) { - return true; + if ( ! current_theme_supports( 'amp' ) ) { + return false; } + + $mode = 'native'; + $support = get_theme_support( 'amp' ); if ( is_array( $support ) ) { - $args = array_shift( $support ); - if ( empty( $args['template_dir'] ) ) { - return true; + $args = array_shift( $support ); + $support = AMP_Options_Manager::get_option( 'theme_support' ); + + // If support is optional, look at DB option if mode is not explicitly set in theme support. + if ( ! empty( $args['optional'] ) ) { + if ( 'disabled' === $support ) { + return false; + } elseif ( ! isset( $args['mode'] ) ) { + return 'native' === $support; + } + } + + if ( isset( $args['mode'] ) ) { + $mode = $args['mode']; + } elseif ( 'disabled' !== $support ) { + $mode = $support; // Supplied via admin screen. + } elseif ( ! empty( $args['template_dir'] ) ) { + $mode = 'paired'; // If there is a template_dir, then paired mode is implied. } } - return false; + return 'native' === $mode; } /** @@ -426,6 +465,7 @@ function _amp_bootstrap_customizer() { /** * Redirects the old AMP URL to the new AMP URL. + * * If post slug is updated the amp page with old post slug will be redirected to the updated url. * * @since 0.5 diff --git a/includes/admin/class-amp-editor-blocks.php b/includes/admin/class-amp-editor-blocks.php index 7e880cea840..d2820e5e14c 100644 --- a/includes/admin/class-amp-editor-blocks.php +++ b/includes/admin/class-amp-editor-blocks.php @@ -43,6 +43,22 @@ public function init() { if ( function_exists( 'gutenberg_init' ) ) { add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_editor_assets' ) ); add_filter( 'wp_kses_allowed_html', array( $this, 'whitelist_block_atts_in_wp_kses_allowed_html' ), 10, 2 ); + + /* + * Dirty AMP is required when a site is in native mode but not all templates are being served + * as AMP. In particular, if a single post is using AMP-specific Gutenberg Blocks which make + * use of AMP components, and the singular template is served as AMP but the blog page is not, + * then the non-AMP blog page need to load the AMP runtime scripts so that the AMP components + * in the posts displayed there will be rendered properly. This is only relevant on native AMP + * sites because the AMP Gutenberg blocks are only made available in that mode; they are not + * presented in the Gutenberg inserter in paired mode. In general, using AMP components in + * non-AMP documents is still not officially supported, so it's occurrence is being minimized + * as much as possible. For more, see . + */ + if ( amp_is_canonical() ) { + add_filter( 'the_content', array( $this, 'tally_content_requiring_amp_scripts' ) ); + add_action( 'wp_print_footer_scripts', array( $this, 'print_dirty_amp_scripts' ) ); + } } } @@ -145,4 +161,32 @@ public function enqueue_block_editor_assets() { ) ) ) ); } + + /** + * Tally the AMP component scripts that are needed in a dirty AMP document. + * + * @param string $content Content. + * @return string Content (unmodified). + */ + public function tally_content_requiring_amp_scripts( $content ) { + if ( ! is_amp_endpoint() ) { + $pattern = sprintf( '/<(%s)\b.*?>/s', join( '|', $this->amp_blocks ) ); + if ( preg_match_all( $pattern, $content, $matches ) ) { + $this->content_required_amp_scripts = array_merge( + $this->content_required_amp_scripts, + $matches[1] + ); + } + } + return $content; + } + + /** + * Print AMP scripts required for AMP components used in a non-AMP document (dirty AMP). + */ + public function print_dirty_amp_scripts() { + if ( ! is_amp_endpoint() && ! empty( $this->content_required_amp_scripts ) ) { + wp_scripts()->do_items( $this->content_required_amp_scripts ); + } + } } diff --git a/includes/admin/class-amp-post-meta-box.php b/includes/admin/class-amp-post-meta-box.php index 55842b335ba..325e81e7b9d 100644 --- a/includes/admin/class-amp-post-meta-box.php +++ b/includes/admin/class-amp-post-meta-box.php @@ -143,12 +143,20 @@ public function enqueue_admin_assets() { array( 'jquery' ), AMP__VERSION ); + + if ( current_theme_supports( 'amp' ) ) { + $availability = AMP_Theme_Support::get_template_availability( $post ); + $support_errors = $availability['errors']; + } else { + $support_errors = AMP_Post_Type_Support::get_support_errors( $post ); + } + wp_add_inline_script( self::ASSETS_HANDLE, sprintf( 'ampPostMetaBox.boot( %s );', wp_json_encode( array( 'previewLink' => esc_url_raw( add_query_arg( amp_get_slug(), '', get_preview_post_link( $post ) ) ), 'canonical' => amp_is_canonical(), - 'enabled' => post_supports_amp( $post ), - 'canSupport' => count( AMP_Post_Type_Support::get_support_errors( $post ) ) === 0, + 'enabled' => empty( $support_errors ), + 'canSupport' => 0 === count( array_diff( $support_errors, array( 'post-status-disabled' ) ) ), 'statusInputName' => self::STATUS_INPUT_NAME, 'l10n' => array( 'ampPreviewBtnLabel' => __( 'Preview changes in AMP (opens in new window)', 'amp' ), @@ -170,23 +178,37 @@ public function render_status( $post ) { is_post_type_viewable( $post->post_type ) && current_user_can( 'edit_post', $post->ID ) - && - ! amp_is_canonical() ); if ( true !== $verify ) { return; } - $errors = AMP_Post_Type_Support::get_support_errors( $post ); - $status = post_supports_amp( $post ) ? self::ENABLED_STATUS : self::DISABLED_STATUS; + /* + * When theme support is present then theme templates can be served in AMP and we check first if the template is available. + * Checking for template availability will include a check for get_support_errors. Otherwise, if theme support is not present + * then we just check get_support_errors. + */ + if ( current_theme_supports( 'amp' ) ) { + $availability = AMP_Theme_Support::get_template_availability( $post ); + $status = $availability['supported'] ? self::ENABLED_STATUS : self::DISABLED_STATUS; + $errors = array_diff( $availability['errors'], array( 'post-status-disabled' ) ); // Subtract the status which the metabox will allow to be toggled. + if ( true === $availability['immutable'] ) { + $errors[] = 'status_immutable'; + } + } else { + $errors = AMP_Post_Type_Support::get_support_errors( $post ); + $status = empty( $errors ) ? self::ENABLED_STATUS : self::DISABLED_STATUS; + $errors = array_diff( $errors, array( 'post-status-disabled' ) ); // Subtract the status which the metabox will allow to be toggled. + } + $labels = array( 'enabled' => __( 'Enabled', 'amp' ), 'disabled' => __( 'Disabled', 'amp' ), ); // The preceding variables are used inside the following amp-status.php template. - include_once AMP__DIR__ . '/templates/admin/amp-status.php'; + include AMP__DIR__ . '/templates/admin/amp-status.php'; } /** diff --git a/includes/amp-helper-functions.php b/includes/amp-helper-functions.php index a5cc1e25626..aa2d825554d 100644 --- a/includes/amp-helper-functions.php +++ b/includes/amp-helper-functions.php @@ -123,6 +123,9 @@ function amp_get_permalink( $post_id ) { || // If the post type is hierarchical then the /amp/ endpoint isn't available. is_post_type_hierarchical( get_post_type( $post_id ) ) + || + // Attachment pages don't accept the /amp/ endpoint. + 'attachment' === get_post_type( $post_id ) ); if ( $use_query_var ) { $amp_url = add_query_arg( amp_get_slug(), '', $permalink ); @@ -239,61 +242,20 @@ function amp_add_amphtml_link() { * @see AMP_Post_Type_Support::get_support_errors() * * @param WP_Post $post Post. - * * @return bool Whether the post supports AMP. */ function post_supports_amp( $post ) { - if ( amp_is_canonical() ) { - return true; - } - - $errors = AMP_Post_Type_Support::get_support_errors( $post ); - - // Return false if an error is found. - if ( ! empty( $errors ) ) { - return false; - } - - switch ( get_post_meta( $post->ID, AMP_Post_Meta_Box::STATUS_POST_META_KEY, true ) ) { - case AMP_Post_Meta_Box::ENABLED_STATUS: - return true; - - case AMP_Post_Meta_Box::DISABLED_STATUS: - return false; - - // Disabled by default for custom page templates, page on front and page for posts. - default: - $enabled = ( - ! (bool) get_page_template_slug( $post ) - && - ! ( - 'page' === $post->post_type - && - 'page' === get_option( 'show_on_front' ) - && - in_array( (int) $post->ID, array( - (int) get_option( 'page_on_front' ), - (int) get_option( 'page_for_posts' ), - ), true ) - ) - ); - - /** - * Filters whether default AMP status should be enabled or not. - * - * @since 0.6 - * - * @param string $status Status. - * @param WP_Post $post Post. - */ - return apply_filters( 'amp_post_status_default_enabled', $enabled, $post ); - } + return 0 === count( AMP_Post_Type_Support::get_support_errors( $post ) ); } /** - * Are we currently on an AMP URL? + * Determine whether the current response being served as AMP. * - * @since 1.0 This function can be called before the `parse_query` action because the 'amp' query var is specifically and exclusively used when 'amp' theme support is added. + * This function cannot be called before the parse_query action because it needs to be able + * to determine the queried object is able to be served as AMP. If 'amp' theme support is not + * present, this function returns true just if the query var is present. If theme support is + * present, then it returns true in paired mode if an AMP template is available and the query + * var is present, or else in native mode if just the template is available. * * @return bool Whether it is the AMP endpoint. */ @@ -302,17 +264,29 @@ function is_amp_endpoint() { return false; } - // When 'amp' theme support is (or will be added) then these are the conditions that are key to be checked. - if ( amp_is_canonical() || isset( $_GET[ amp_get_slug() ] ) ) { // WPCS: CSRF OK. - return true; + $did_parse_query = did_action( 'parse_query' ); + + if ( ! $did_parse_query ) { + _doing_it_wrong( __FUNCTION__, sprintf( esc_html__( "is_amp_endpoint() was called before the 'parse_query' hook was called. This function will always return 'false' before the 'parse_query' hook is called.", 'amp' ) ), '0.4.2' ); } - // Condition for non-theme support when /amp/ endpoint is used. - if ( false !== get_query_var( amp_get_slug(), false ) ) { - return true; + $has_amp_query_var = ( + isset( $_GET[ amp_get_slug() ] ) // WPCS: CSRF OK. + || + false !== get_query_var( amp_get_slug(), false ) + ); + + if ( ! current_theme_supports( 'amp' ) ) { + return $has_amp_query_var; + } + + // When there is no query var and AMP is not canonical/native, then this is definitely not an AMP endpoint. + if ( ! $has_amp_query_var && ! amp_is_canonical() ) { + return false; } - return false; + $availability = AMP_Theme_Support::get_template_availability(); + return amp_is_canonical() ? $availability['supported'] : ( $has_amp_query_var && $availability['supported'] ); } /** diff --git a/includes/class-amp-post-type-support.php b/includes/class-amp-post-type-support.php index 22a1bef580b..48a8d6d4d5a 100644 --- a/includes/class-amp-post-type-support.php +++ b/includes/class-amp-post-type-support.php @@ -14,9 +14,11 @@ class AMP_Post_Type_Support { /** * Get post types that plugin supports out of the box (which cannot be disabled). * + * @deprecated * @return string[] Post types. */ public static function get_builtin_supported_post_types() { + _deprecated_function( __METHOD__, '1.0' ); return array_filter( array( 'post' ), 'post_type_exists' ); } @@ -27,17 +29,12 @@ public static function get_builtin_supported_post_types() { * @return string[] Post types eligible for AMP. */ public static function get_eligible_post_types() { - return array_merge( - self::get_builtin_supported_post_types(), - array( 'page' ), - array_values( get_post_types( - array( - 'public' => true, - '_builtin' => false, - ), - 'names' - ) ) - ); + return array_values( get_post_types( + array( + 'public' => true, + ), + 'names' + ) ); } /** @@ -49,10 +46,11 @@ public static function get_eligible_post_types() { * @since 0.6 */ public static function add_post_type_support() { - $post_types = array_merge( - self::get_builtin_supported_post_types(), - AMP_Options_Manager::get_option( 'supported_post_types', array() ) - ); + if ( current_theme_supports( 'amp' ) && AMP_Options_Manager::get_option( 'all_templates_supported' ) ) { + $post_types = self::get_eligible_post_types(); + } else { + $post_types = AMP_Options_Manager::get_option( 'supported_post_types', array() ); + } foreach ( $post_types as $post_type ) { add_post_type_support( $post_type, amp_get_slug() ); } @@ -72,8 +70,7 @@ public static function get_support_errors( $post ) { } $errors = array(); - // Because `add_rewrite_endpoint` doesn't let us target specific post_types. - if ( isset( $post->post_type ) && ! post_type_supports( $post->post_type, amp_get_slug() ) ) { + if ( ! post_type_supports( $post->post_type, amp_get_slug() ) ) { $errors[] = 'post-type-support'; } @@ -94,6 +91,48 @@ public static function get_support_errors( $post ) { $errors[] = 'skip-post'; } + $status = get_post_meta( $post->ID, AMP_Post_Meta_Box::STATUS_POST_META_KEY, true ); + if ( $status ) { + if ( AMP_Post_Meta_Box::DISABLED_STATUS === $status ) { + $errors[] = 'post-status-disabled'; + } + } else { + /* + * Disabled by default for custom page templates, page on front and page for posts, unless 'amp' theme + * support is present (in which case AMP_Theme_Support::get_template_availability() determines availability). + */ + $enabled = ( + current_theme_supports( 'amp' ) + || + ( + ! (bool) get_page_template_slug( $post ) + && + ! ( + 'page' === $post->post_type + && + 'page' === get_option( 'show_on_front' ) + && + in_array( (int) $post->ID, array( + (int) get_option( 'page_on_front' ), + (int) get_option( 'page_for_posts' ), + ), true ) + ) + ) + ); + + /** + * Filters whether default AMP status should be enabled or not. + * + * @since 0.6 + * + * @param string $status Status. + * @param WP_Post $post Post. + */ + $enabled = apply_filters( 'amp_post_status_default_enabled', $enabled, $post ); + if ( ! $enabled ) { + $errors[] = 'post-status-disabled'; + } + } return $errors; } } diff --git a/includes/class-amp-theme-support.php b/includes/class-amp-theme-support.php index 7389490b2bb..709d1082208 100644 --- a/includes/class-amp-theme-support.php +++ b/includes/class-amp-theme-support.php @@ -24,7 +24,7 @@ class AMP_Theme_Support { * * @var string */ - const RESPONSE_CACHE_GROUP = 'amp-reponse'; + const RESPONSE_CACHE_GROUP = 'amp-response'; /** * Sanitizer classes. @@ -90,13 +90,34 @@ class AMP_Theme_Support { */ protected static $is_output_buffering = false; + /** + * Original theme support args prior to being read. + * + * This is needed to be able to properly populate the admin screen with AMP options defined by theme support + * when the theme support is optional and disabled in the admin. For example, it allows for the original + * template `mode` from the theme support to be displayed in the admin screen even though read_theme_support + * may have removed the `optional` the theme support. + * + * @see AMP_Theme_Support::read_theme_support() + * @var array + */ + protected static $initial_theme_support_args = array(); + + /** + * Theme support options that were added via option. + * + * @since 1.0 + * @var false|array + */ + protected static $support_added_via_option = false; + /** * Initialize. * * @since 0.7 */ public static function init() { - self::apply_options(); + self::read_theme_support(); if ( ! current_theme_supports( 'amp' ) ) { return; } @@ -112,17 +133,6 @@ public static function init() { require_once AMP__DIR__ . '/includes/amp-post-template-actions.php'; - // Validate theme support usage. - $support = get_theme_support( 'amp' ); - if ( WP_DEBUG && is_array( $support ) ) { - $args = array_shift( $support ); - if ( ! is_array( $args ) ) { - trigger_error( esc_html__( 'Expected AMP theme support arg to be array.', 'amp' ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error - } elseif ( count( array_diff( array_keys( $args ), array( 'template_dir', 'available_callback', 'comments_live_list', '__added_via_option' ) ) ) !== 0 ) { - trigger_error( esc_html__( 'Expected AMP theme support to only have template_dir and/or available_callback.', 'amp' ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error - } - } - add_action( 'widgets_init', array( __CLASS__, 'register_widgets' ) ); /* @@ -134,25 +144,107 @@ public static function init() { } /** - * Apply options for whether theme support is enabled via admin and what sanitization is performed by default. + * Determine whether theme support was added via option. + * + * @since 1.0 + * @return bool Optional support added. + */ + public static function is_support_added_via_option() { + return false !== self::$support_added_via_option; + } + + /** + * Read theme support and apply options from admin for whether theme support is enabled and via what template is enabled. * * @see AMP_Post_Type_Support::add_post_type_support() For where post type support is added, since it is irrespective of theme support. */ - public static function apply_options() { - if ( ! current_theme_supports( 'amp' ) ) { - $theme_support_option = AMP_Options_Manager::get_option( 'theme_support' ); + public static function read_theme_support() { + self::$support_added_via_option = false; + + self::$initial_theme_support_args = false; + if ( current_theme_supports( 'amp' ) ) { + self::$initial_theme_support_args = array(); + + $support = get_theme_support( 'amp' ); + if ( is_array( $support ) ) { + self::$initial_theme_support_args = array_shift( $support ); + + // Validate theme support usage. + $keys = array( 'template_dir', 'comments_live_list', 'mode', 'optional', 'templates_supported', 'available_callback' ); + if ( ! is_array( self::$initial_theme_support_args ) ) { + self::$initial_theme_support_args = array(); + _doing_it_wrong( 'add_theme_support', esc_html__( 'Expected AMP theme support arg to be array.', 'amp' ), '1.0' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error + } elseif ( count( array_diff( array_keys( self::$initial_theme_support_args ), $keys ) ) !== 0 ) { + _doing_it_wrong( 'add_theme_support', esc_html( sprintf( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error + /* translators: %1$s is expected keys and %2$s is actual keys */ + __( 'Expected AMP theme support to keys (%1$s) but saw (%2$s)', 'amp' ), + join( ', ', $keys ), + join( ', ', array_keys( self::$initial_theme_support_args ) ) + ) ), '1.0' ); + } + + if ( isset( self::$initial_theme_support_args['available_callback'] ) ) { + _doing_it_wrong( 'add_theme_support', esc_html__( 'The available_callback is deprecated when adding amp theme support in favor of declaratively setting the supported_templates.', 'amp' ), '1.0' ); + } + } + } + + $theme_support_option = AMP_Options_Manager::get_option( 'theme_support' ); + $theme_support_args = self::$initial_theme_support_args; + + // If theme support is present in the theme, but it is marked as optional, then go ahead and remove it if theme support is not enabled in the admin. + if ( current_theme_supports( 'amp' ) && ! empty( $theme_support_args['optional'] ) && 'disabled' === $theme_support_option ) { + remove_theme_support( 'amp' ); + return; + } + + if ( ! $theme_support_args ) { + $theme_support_args = array(); + } + + // If theme support is not present (or it is optional), then allow it to be added via the admin. + if ( ! current_theme_supports( 'amp' ) || ! empty( $theme_support_args['optional'] ) ) { if ( 'disabled' === $theme_support_option ) { return; } - $args = array( - '__added_via_option' => true, + $option_args = array( + 'mode' => $theme_support_option, ); - if ( 'paired' === $theme_support_option ) { - $args['template_dir'] = './'; - } - add_theme_support( 'amp', $args ); + add_theme_support( 'amp', array_merge( $option_args, $theme_support_args ) ); + self::$support_added_via_option = $option_args; + } + } + + /** + * Get the theme support args. + * + * @since 1.0 + * + * @param array $options Options. + * @return array|false Theme support args. + */ + public static function get_theme_support_args( $options = array() ) { + $options = array_merge( + array( 'initial' => false ), + $options + ); + + if ( $options['initial'] ) { + return self::$initial_theme_support_args; } + + if ( ! current_theme_supports( 'amp' ) ) { + return false; + } + + $theme_support_args = get_theme_support( 'amp' ); + if ( is_array( $theme_support_args ) ) { + $theme_support_args = array_shift( $theme_support_args ); + } else { + $theme_support_args = array(); + } + return $theme_support_args; } /** @@ -168,8 +260,9 @@ public static function finish_init() { self::ensure_proper_amp_location(); - if ( ! amp_is_canonical() ) { - self::register_paired_hooks(); + $theme_support = self::get_theme_support_args(); + if ( ! empty( $theme_support['template_dir'] ) ) { + self::add_amp_template_filters(); } self::add_hooks(); @@ -217,10 +310,12 @@ public static function ensure_proper_amp_location( $exit = true ) { $new_url = add_query_arg( amp_get_slug(), '', amp_remove_endpoint( $old_url ) ); if ( $old_url !== $new_url ) { wp_safe_redirect( $new_url, 302 ); + // @codeCoverageIgnoreStart if ( $exit ) { exit; } return true; + // @codeCoverageIgnoreEnd } } } @@ -251,43 +346,35 @@ public static function redirect_ampless_url( $exit = true ) { * occur or when a non-canonical AMP theme is used. */ wp_safe_redirect( $ampless_url, 302 ); + // @codeCoverageIgnoreStart if ( $exit ) { exit; } return true; + // @codeCoverageIgnoreEnd } /** * Determines whether paired mode is available. * * When 'amp' theme support has not been added or canonical mode is enabled, then this returns false. - * Returns true when there is a template_dir defined in theme support, and if a defined available_callback - * returns true. * + * @since 0.7 + * + * @see amp_is_canonical() * @return bool Whether available. */ public static function is_paired_available() { - $support = get_theme_support( 'amp' ); - if ( empty( $support ) || amp_is_canonical() ) { + if ( ! current_theme_supports( 'amp' ) ) { return false; } - /** - * Queried object. - * - * @var WP_Post $queried_object - */ - $queried_object = get_queried_object(); - if ( is_singular() && ! post_supports_amp( $queried_object ) ) { + if ( amp_is_canonical() ) { return false; } - $args = array_shift( $support ); - - if ( isset( $args['available_callback'] ) && is_callable( $args['available_callback'] ) ) { - return call_user_func( $args['available_callback'] ); - } - return true; + $availability = self::get_template_availability(); + return $availability['supported']; } /** @@ -303,13 +390,386 @@ public static function is_customize_preview_iframe() { } /** - * Register hooks for paired mode. + * Register filters for loading AMP-specific templates. */ - public static function register_paired_hooks() { + public static function add_amp_template_filters() { foreach ( self::$template_types as $template_type ) { - add_filter( "{$template_type}_template_hierarchy", array( __CLASS__, 'filter_paired_template_hierarchy' ) ); + add_filter( "{$template_type}_template_hierarchy", array( __CLASS__, 'filter_amp_template_hierarchy' ) ); } - add_filter( 'template_include', array( __CLASS__, 'filter_paired_template_include' ), 100 ); + } + + /** + * Determine template availability of AMP for the given query. + * + * This is not intended to return whether AMP is available for a _specific_ post. For that, use `post_supports_amp()`. + * + * @since 1.0 + * @global WP_Query $wp_query + * @see post_supports_amp() + * + * @param WP_Query|WP_Post|null $query Query or queried post. If null then the global query will be used. + * @return array { + * Template availability. + * + * @type bool $supported Whether the template is supported in AMP. + * @type bool|null $immutable Whether the supported status is known to be unchangeable. + * @type string|null $template The ID of the matched template (conditional), such as 'is_singular', or null if nothing was matched. + * @type string[] $errors List of the errors or reasons for why the template is not available. + * } + */ + public static function get_template_availability( $query = null ) { + global $wp_query; + if ( ! $query ) { + $query = $wp_query; + } elseif ( $query instanceof WP_Post ) { + $post = $query; + $query = new WP_Query(); + if ( 'page' === $post->post_type ) { + $query->set( 'page_id', $post->ID ); + } else { + $query->set( 'p', $post->ID ); + } + $query->queried_object = $post; + $query->queried_object_id = $post->ID; + $query->parse_query_vars(); + } + + $default_response = array( + 'errors' => array(), + 'supported' => false, + 'immutable' => null, + 'template' => null, + ); + + if ( ! ( $query instanceof WP_Query ) ) { + _doing_it_wrong( __METHOD__, esc_html__( 'No WP_Query available.', 'amp' ), '1.0' ); + return array_merge( + $default_response, + array( 'errors' => array( 'no_query_available' ) ) + ); + } + + $theme_support_args = self::get_theme_support_args(); + if ( false === $theme_support_args ) { + return array_merge( + $default_response, + array( 'errors' => array( 'no_theme_support' ) ) + ); + } + + // Support available_callback from 0.7, though it is deprecated. + if ( isset( $theme_support_args['available_callback'] ) && is_callable( $theme_support_args['available_callback'] ) ) { + /** + * Queried object. + * + * @var WP_Post $queried_object + */ + $queried_object = $query->get_queried_object(); + if ( ( is_singular() || $query->is_posts_page ) && ! post_supports_amp( $queried_object ) ) { + return array_merge( + $default_response, + array( + 'errors' => array( 'no-post-support' ), + 'supported' => false, + 'immutable' => true, + ) + ); + } + + $response = array_merge( + $default_response, + array( + 'supported' => call_user_func( $theme_support_args['available_callback'] ), + 'immutable' => true, + ) + ); + if ( ! $response['supported'] ) { + $response['errors'][] = 'available_callback'; + } + return $response; + } + + $all_templates_supported_by_theme_support = false; + if ( isset( $theme_support_args['templates_supported'] ) ) { + $all_templates_supported_by_theme_support = 'all' === $theme_support_args['templates_supported']; + } + $all_templates_supported = ( + $all_templates_supported_by_theme_support || AMP_Options_Manager::get_option( 'all_templates_supported' ) + ); + + // Make sure global $wp_query is set in case of conditionals that unfortunately look at global scope. + $prev_query = $wp_query; + $wp_query = $query; // WPCS: override ok. + + $matching_templates = array(); + $supportable_templates = self::get_supportable_templates(); + foreach ( $supportable_templates as $id => $supportable_template ) { + if ( empty( $supportable_template['callback'] ) ) { + $callback = $id; + } else { + $callback = $supportable_template['callback']; + } + + // If the callback is a method on the query, then call the method on the query itself. + if ( is_string( $callback ) && 'is_' === substr( $callback, 0, 3 ) && method_exists( $query, $callback ) ) { + $is_match = call_user_func( array( $query, $callback ) ); + } elseif ( is_callable( $callback ) ) { + $is_match = call_user_func( $callback, $query ); + } else { + /* translators: %s is the supportable template ID. */ + _doing_it_wrong( __FUNCTION__, esc_html__( 'Supportable template "%s" does not have a callable callback.', 'amp' ), '1.0' ); + $is_match = false; + } + + if ( $is_match ) { + $matching_templates[ $id ] = array( + 'template' => $id, + 'supported' => ! empty( $supportable_template['supported'] ), + 'immutable' => ! empty( $supportable_template['immutable'] ), + ); + } + } + + // Restore previous $wp_query (if any). + $wp_query = $prev_query; // WPCS: override ok. + + // Make sure children override their parents. + $matching_template_ids = array_keys( $matching_templates ); + foreach ( array_diff( array_keys( $supportable_templates ), $matching_template_ids ) as $template_id ) { + unset( $supportable_templates[ $template_id ] ); + } + foreach ( $matching_template_ids as $id ) { + $has_children = false; + foreach ( $supportable_templates as $other_id => $supportable_template ) { + if ( $other_id === $id ) { + continue; + } + if ( isset( $supportable_template['parent'] ) && $id === $supportable_template['parent'] ) { + $has_children = true; + break; + } + } + + // Delete all matching parent templates since the child will override them. + if ( ! $has_children ) { + $supportable_template = $supportable_templates[ $id ]; + while ( ! empty( $supportable_template['parent'] ) ) { + $parent = $supportable_template['parent']; + $supportable_template = $supportable_templates[ $parent ]; + + // Let the child supported status override the parent's supported status. + unset( $matching_templates[ $parent ] ); + } + } + } + + // The is_home() condition is the default so discard it if there are other matching templates. + if ( count( $matching_templates ) > 1 && isset( $matching_templates['is_home'] ) ) { + unset( $matching_templates['is_home'] ); + } + + /* + * If there are more than one matching templates, then something is probably not right. + * Template conditions need to be set up properly to prevent this from happening. + */ + if ( count( $matching_templates ) > 1 ) { + _doing_it_wrong( __METHOD__, esc_html__( 'Did not expect there to be more than one matching template. Did you filter amp_supportable_templates to not honor the template hierarchy?', 'amp' ), '1.0' ); + } + + $matching_template = array_shift( $matching_templates ); + + // If there aren't any matching templates left that are supported, then we consider it to not be available. + if ( ! $matching_template ) { + if ( $all_templates_supported ) { + return array_merge( + $default_response, + array( + 'supported' => true, + ) + ); + } else { + return array_merge( + $default_response, + array( 'errors' => array( 'no_matching_template' ) ) + ); + } + } + $matching_template = array_merge( $default_response, $matching_template ); + + // If there aren't any matching templates left that are supported, then we consider it to not be available. + if ( empty( $matching_template['supported'] ) ) { + $matching_template['errors'][] = 'template_unsupported'; + } + + // For singular queries, post_supports_amp() is given the final say. + if ( $query->is_singular() || $query->is_posts_page ) { + /** + * Queried object. + * + * @var WP_Post $queried_object + */ + $queried_object = $query->get_queried_object(); + $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 is 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 is 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 ); + + // Obtain the initial template supported state by theme support flag. + $theme_support_args = self::get_theme_support_args( array( 'initial' => true ) ); + $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; } /** @@ -504,10 +964,10 @@ protected static function wp_kses_amp_mustache( $text ) { * @return string|null URL if redirect to be done; otherwise function will exist. */ public static function filter_comment_post_redirect( $url, $comment ) { - $theme_support = get_theme_support( 'amp' ); + $theme_support = self::get_theme_support_args(); // Cause a page refresh if amp-live-list is not implemented for comments via add_theme_support( 'amp', array( 'comments_live_list' => true ) ). - if ( empty( $theme_support[0]['comments_live_list'] ) ) { + if ( empty( $theme_support['comments_live_list'] ) ) { /* * Add the comment ID to the URL to force AMP to refresh the page. * This is ideally a temporary workaround to deal with https://github.com/ampproject/amphtml/issues/14170 @@ -722,41 +1182,22 @@ public static function amend_comment_form() { /** * Prepends template hierarchy with template_dir for AMP paired mode templates. * - * @see get_query_template() - * * @param array $templates Template hierarchy. * @return array Templates. */ - public static function filter_paired_template_hierarchy( $templates ) { - $support = get_theme_support( 'amp' ); - $args = array_shift( $support ); + public static function filter_amp_template_hierarchy( $templates ) { + $args = self::get_theme_support_args(); if ( isset( $args['template_dir'] ) ) { $amp_templates = array(); foreach ( $templates as $template ) { - $amp_templates[] = $args['template_dir'] . '/' . $template; + $amp_templates[] = $args['template_dir'] . '/' . $template; // Let template_dir have precedence. + $amp_templates[] = $template; } $templates = $amp_templates; } return $templates; } - /** - * Redirect to the non-canonical URL when the template to include is empty. - * - * This is a failsafe in case an index.php is not located in the AMP template_dir, - * and the available_callback fails to omit a given request from being available in AMP. - * - * @param string $template Template to include. - * @return string Template to include. - */ - public static function filter_paired_template_include( $template ) { - if ( empty( $template ) || ! self::is_paired_available() ) { - wp_safe_redirect( self::get_current_canonical_url(), 302 ); // Temporary redirect because support may come later. - exit; - } - return $template; - } - /** * Get canonical URL for current request. * diff --git a/includes/options/class-amp-options-manager.php b/includes/options/class-amp-options-manager.php index 7e0fbf4018e..e18bdbc1bad 100644 --- a/includes/options/class-amp-options-manager.php +++ b/includes/options/class-amp-options-manager.php @@ -23,12 +23,14 @@ class AMP_Options_Manager { * @var array */ protected static $defaults = array( - 'theme_support' => 'disabled', - 'supported_post_types' => array(), - 'analytics' => array(), - 'force_sanitization' => false, - 'accept_tree_shaking' => false, - 'disable_admin_bar' => false, + 'theme_support' => 'disabled', + 'supported_post_types' => array( 'post' ), + 'analytics' => array(), + 'force_sanitization' => false, + 'accept_tree_shaking' => false, + 'disable_admin_bar' => false, + 'all_templates_supported' => true, + 'supported_templates' => array( 'is_singular' ), ); /** @@ -121,8 +123,8 @@ public static function validate_options( $new_options ) { $options['disable_admin_bar'] = ! empty( $new_options['disable_admin_bar'] ); // Validate post type support. + $options['supported_post_types'] = array(); if ( isset( $new_options['supported_post_types'] ) ) { - $options['supported_post_types'] = array(); foreach ( $new_options['supported_post_types'] as $post_type ) { if ( ! post_type_exists( $post_type ) ) { add_settings_error( self::OPTION_NAME, 'unknown_post_type', __( 'Unrecognized post type.', 'amp' ) ); @@ -132,6 +134,22 @@ public static function validate_options( $new_options ) { } } + $theme_support_args = AMP_Theme_Support::get_theme_support_args( array( 'initial' => true ) ); + + $is_template_support_required = ( isset( $theme_support_args['templates_supported'] ) && 'all' === $theme_support_args['templates_supported'] ); + if ( ! $is_template_support_required && ! isset( $theme_support_args['available_callback'] ) ) { + $options['all_templates_supported'] = ! empty( $new_options['all_templates_supported'] ); + + // Validate supported templates. + $options['supported_templates'] = array(); + if ( isset( $new_options['supported_templates'] ) ) { + $options['supported_templates'] = array_intersect( + $new_options['supported_templates'], + array_keys( AMP_Theme_Support::get_supportable_templates() ) + ); + } + } + // Validate analytics. if ( isset( $new_options['analytics'] ) ) { foreach ( $new_options['analytics'] as $id => $data ) { @@ -177,6 +195,9 @@ public static function validate_options( $new_options ) { } } + // Store the current version with the options so we know the format. + $options['version'] = AMP__VERSION; + return $options; } @@ -187,11 +208,10 @@ public static function validate_options( $new_options ) { * @see add_settings_error() */ public static function check_supported_post_type_update_errors() { - $builtin_support = AMP_Post_Type_Support::get_builtin_supported_post_types(); $supported_types = self::get_option( 'supported_post_types', array() ); foreach ( AMP_Post_Type_Support::get_eligible_post_types() as $name ) { $post_type = get_post_type_object( $name ); - if ( empty( $post_type ) || in_array( $post_type->name, $builtin_support, true ) ) { + if ( empty( $post_type ) ) { continue; } diff --git a/includes/options/class-amp-options-menu.php b/includes/options/class-amp-options-menu.php index f3e51f34f7b..4ce89cf0958 100644 --- a/includes/options/class-amp-options-menu.php +++ b/includes/options/class-amp-options-menu.php @@ -99,13 +99,13 @@ public function add_menu_items() { ); add_settings_field( - 'supported_post_types', - __( 'Post Type Support', 'amp' ), - array( $this, 'render_post_types_support' ), + 'supported_templates', + __( 'Supported Templates', 'amp' ), + array( $this, 'render_supported_templates' ), AMP_Options_Manager::OPTION_NAME, 'general', array( - 'class' => 'amp-post-type-support-field', + 'class' => 'amp-template-support-field', ) ); @@ -126,20 +126,18 @@ public function add_menu_items() { */ public function render_theme_support() { $theme_support = AMP_Options_Manager::get_option( 'theme_support' ); - - $support_args = get_theme_support( 'amp' ); + $support_args = AMP_Theme_Support::get_theme_support_args( array( 'initial' => true ) ); $theme_support_mutable = ( - empty( $support_args ) + ! empty( $support_args['optional'] ) || - ! empty( $support_args[0]['__added_via_option'] ) + AMP_Theme_Support::is_support_added_via_option() ); - if ( ! $theme_support_mutable ) { - if ( amp_is_canonical() ) { - $theme_support = 'native'; - } else { - $theme_support = 'paired'; - } + + $mode_mutable = ! isset( $support_args['mode'] ); + + if ( ! $theme_support_mutable || ( ! $mode_mutable && 'disabled' !== $theme_support ) ) { + $theme_support = isset( $support_args['mode'] ) ? $support_args['mode'] : 'native'; } $should_have_theme_support = in_array( get_template(), array( 'twentyfifteen', 'twentysixteen', 'twentyseventeen' ), true ); @@ -164,24 +162,28 @@ public function render_theme_support() {
-
- > - -
-
- -
-
- > - -
-
- -
+ +
+ > + +
+
+ +
+ + +
+ > + +
+
+ +
+ jQuery( 'input[type=radio][name="amp-options[theme_support]"]' ).change( function() { jQuery( '.amp-force-sanitize' ).toggleClass( 'hidden', 'native' === this.value ); + jQuery( '.amp-validation-field' ).toggleClass( 'hidden', 'disabled' === this.value ); jQuery( '.amp-force-sanitize-canonical' ).toggleClass( 'hidden', 'native' !== this.value ); jQuery( '#force_sanitization' ).trigger( 'change' ); } ).filter( ':checked' ).trigger( 'change' ); @@ -272,45 +275,178 @@ public function render_validation_handling() { } /** - * Post types support section renderer. + * Supported templates section renderer. * - * @since 0.6 + * @since 1.0 */ - public function render_post_types_support() { - $builtin_support = AMP_Post_Type_Support::get_builtin_supported_post_types(); - $element_name = AMP_Options_Manager::OPTION_NAME . '[supported_post_types][]'; + public function render_supported_templates() { + $theme_support_args = AMP_Theme_Support::get_theme_support_args( array( 'initial' => true ) ); // Initial so we can get before removed if optional. ?> - -
+ + +
+ +
+

+ +

+
+ +

+ +

+

+ +

+ +
+ +
+

+ +

+
+ + +
+ +

+

+ +

+
    +
  • + name}"; ?> + name, amp_get_slug() ) ); ?> + > + +
  • + +
+
+ + +
+ +

name}"; - $is_builtin = in_array( $post_type->name, $builtin_support, true ); + self::list_template_conditional_options( AMP_Theme_Support::get_supportable_templates() ); ?> - - - - name, amp_get_slug() ) ); ?> - - > - -
+ +
+ + + + +
+

post type does not support it.', 'amp' ), admin_url( 'admin.php?page=' . AMP_Options_Manager::OPTION_NAME ) ) ); + $error_messages[] = wp_kses_post( sprintf( __( 'There are no supported templates to display this in AMP.', 'amp' ), esc_url( admin_url( 'admin.php?page=' . AMP_Options_Manager::OPTION_NAME ) ) ) ); } - if ( in_array( 'skip-post', $support_errors_codes, true ) ) { - $support_errors[] = __( 'A plugin or theme has disabled AMP support.', 'amp' ); + if ( in_array( 'password-protected', $errors, true ) ) { + $error_messages[] = __( 'AMP cannot be enabled on password protected posts.', 'amp' ); } - if ( count( array_diff( $support_errors_codes, array( 'page-on-front', 'page-for-posts', 'password-protected', 'post-type-support', 'skip-post' ) ) ) > 0 ) { - $support_errors[] = __( 'Unavailable for an unknown reason.', 'amp' ); + if ( in_array( 'post-type-support', $errors, true ) ) { + /* translators: %s is URL to AMP settings screen */ + $error_messages[] = wp_kses_post( sprintf( __( 'AMP cannot be enabled because this post type does not support it.', 'amp' ), esc_url( admin_url( 'admin.php?page=' . AMP_Options_Manager::OPTION_NAME ) ) ) ); + } + if ( in_array( 'skip-post', $errors, true ) ) { + $error_messages[] = __( 'A plugin or theme has disabled AMP support.', 'amp' ); + } + if ( count( array_diff( $errors, array( 'status_immutable', 'page-on-front', 'page-for-posts', 'password-protected', 'post-type-support', 'skip-post', 'template_unsupported', 'no_matching_template' ) ) ) > 0 ) { + $error_messages[] = __( 'Unavailable for an unknown reason.', 'amp' ); } - echo implode( ' ', $support_errors ); // WPCS: xss ok. + echo implode( ' ', $error_messages ); // WPCS: xss ok. ?>

diff --git a/tests/test-amp-helper-functions.php b/tests/test-amp-helper-functions.php index d7391910cfd..290660ba1ce 100644 --- a/tests/test-amp-helper-functions.php +++ b/tests/test-amp-helper-functions.php @@ -316,6 +316,7 @@ public function test_amp_add_amphtml_link( $canonical_url, $amphtml_url ) { * @covers \is_amp_endpoint() */ public function test_is_amp_endpoint() { + $this->go_to( get_permalink( $this->factory()->post->create() ) ); $this->assertFalse( is_amp_endpoint() ); // Legacy query var. diff --git a/tests/test-amp-render-post.php b/tests/test-amp-render-post.php index 13fa8b7ec59..4c839562c66 100644 --- a/tests/test-amp-render-post.php +++ b/tests/test-amp-render-post.php @@ -35,8 +35,8 @@ public function test__valid_post() { * @covers \is_amp_endpoint() */ public function test__is_amp_endpoint() { - $user_id = $this->factory->user->create(); - $post_id = $this->factory->post->create( array( + $user_id = $this->factory()->user->create(); + $post_id = $this->factory()->post->create( array( 'post_author' => $user_id, ) ); @@ -58,6 +58,7 @@ public function test__is_amp_endpoint() { $this->assertFalse( $after_is_amp_endpoint, 'is_amp_endpoint was not reset after amp_render_post' ); add_theme_support( 'amp' ); + $this->go_to( get_permalink( $post_id ) ); $this->assertTrue( is_amp_endpoint() ); // Make is_admin() true, as requests for an admin page aren't for AMP endpoints. diff --git a/tests/test-class-amp-base-sanitizer.php b/tests/test-class-amp-base-sanitizer.php index c51236f749b..fd6a3541874 100644 --- a/tests/test-class-amp-base-sanitizer.php +++ b/tests/test-class-amp-base-sanitizer.php @@ -108,7 +108,7 @@ public function get_data() { * @param array $source_attributes Source Attrs. * @param array $expected_attributes Expected Attrs. * @param array $args Args. - * @covers AMP_Base_Sanitizer::enforce_fixed_height() + * @covers AMP_Base_Sanitizer::set_layout() */ public function test_set_layout( $source_attributes, $expected_attributes, $args = array() ) { $sanitizer = new AMP_Test_Stub_Sanitizer( new DOMDocument(), $args ); @@ -316,7 +316,7 @@ public function test_filter_data_amp_attributes() { /** * Tests set_attachment_layout_attributes. * - * @covers AMP_Base_Sanitizer::set_attachment_layout_attributes() + * @covers AMP_Base_Sanitizer::filter_attachment_layout_attributes() */ public function test_filter_attachment_layout_attributes() { $sanitizer = new AMP_Test_Stub_Sanitizer( new DOMDocument(), array() ); diff --git a/tests/test-class-amp-gfycat-embed-handler.php b/tests/test-class-amp-gfycat-embed-handler.php index b22409aae25..d5da538f13b 100644 --- a/tests/test-class-amp-gfycat-embed-handler.php +++ b/tests/test-class-amp-gfycat-embed-handler.php @@ -19,6 +19,20 @@ public function setUp() { global $post; parent::setUp(); + // Mock the HTTP request. + add_filter( 'pre_http_request', function( $pre, $r, $url ) { + if ( false === strpos( $url, 'tautwhoppingcougar' ) ) { + return $pre; + } + return array( + 'body' => '{"version":"1.0","type":"video","provider_name":"https://gfycat.com","width":500,"height":281,"title":"Melanie Raccoon riding bike-side angle (reddit)","html":""}', + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + ); + }, 10, 3 ); + /* * As #34115 in 4.9 a post is not needed for context to run oEmbeds. Prior ot 4.9, the WP_Embed::shortcode() * method would short-circuit when this is the case: diff --git a/tests/test-class-amp-hulu-embed-handler.php b/tests/test-class-amp-hulu-embed-handler.php index e002fa3443b..dbf178ab0ee 100644 --- a/tests/test-class-amp-hulu-embed-handler.php +++ b/tests/test-class-amp-hulu-embed-handler.php @@ -19,6 +19,21 @@ public function setUp() { global $post; parent::setUp(); + // Mock the HTTP request. + add_filter( 'pre_http_request', function( $pre, $r, $url ) { + unset( $r ); + if ( false === strpos( $url, '771496' ) ) { + return $pre; + } + return array( + 'body' => '{"title":"Out of the Box / Run Down Race Car (Doc McStuffins)","author_name":"Disney Junior","type":"video","provider_name":"Hulu","air_date":"Fri Mar 23 00:00:00 UTC 2012","embed_url":"//www.hulu.com/embed.html?eid=_hHzwnAcj3RrXMJFDDvkuw","thumbnail_url":"http://ib.huluim.com/video/60528019?size=240x180&caller=h1o&img=i","width":500,"thumbnail_width":500,"provider_url":"//www.hulu.com/","thumbnail_height":375,"cache_age":3600,"version":"1.0","large_thumbnail_url":"http://ib.huluim.com/video/60528019?size=512x288&caller=h1o&img=i","height":289,"large_thumbnail_width":512,"html":"","duration":1446.25,"large_thumbnail_height":288}', + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + ); + }, 10, 3 ); + /* * As #34115 in 4.9 a post is not needed for context to run oEmbeds. Prior ot 4.9, the WP_Embed::shortcode() * method would short-circuit when this is the case: diff --git a/tests/test-class-amp-imgur-embed-handler.php b/tests/test-class-amp-imgur-embed-handler.php index 38f749e5d8a..8def972a598 100644 --- a/tests/test-class-amp-imgur-embed-handler.php +++ b/tests/test-class-amp-imgur-embed-handler.php @@ -19,6 +19,21 @@ public function setUp() { global $post; parent::setUp(); + // Mock the HTTP request. + add_filter( 'pre_http_request', function( $pre, $r, $url ) { + unset( $r ); + if ( false === strpos( $url, 'f462IUj' ) ) { + return $pre; + } + return array( + 'body' => '{"version":"1.0","type":"rich","provider_name":"Imgur","provider_url":"https:\\/\\/imgur.com","width":500,"height":750,"html":"
Getting that beach body ready<\\/a><\\/blockquote>', $sanitized_html ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript } + /** + * Test incorrect usage for get_template_availability. + * + * @expectedIncorrectUsage AMP_Theme_Support::get_template_availability + * @covers AMP_Theme_Support::get_template_availability() + */ + public function test_incorrect_usage_get_template_availability() { + global $wp_query; + + // Test no query available. + $wp_query = null; // WPCS: override ok. + $availability = AMP_Theme_Support::get_template_availability(); + $this->assertInternalType( 'array', $availability ); + $this->assertEquals( array( 'no_query_available' ), $availability['errors'] ); + $this->assertFalse( $availability['supported'] ); + $this->assertNull( $availability['immutable'] ); + $this->assertNull( $availability['template'] ); + + // Test no theme support. + remove_theme_support( 'amp' ); + $this->go_to( get_permalink( $this->factory()->post->create() ) ); + $availability = AMP_Theme_Support::get_template_availability(); + $this->assertEquals( array( 'no_theme_support' ), $availability['errors'] ); + $this->assertFalse( $availability['supported'] ); + $this->assertNull( $availability['immutable'] ); + $this->assertNull( $availability['template'] ); + } + + /** + * Test get_template_availability with available_callback. + * + * @expectedIncorrectUsage add_theme_support + * @covers AMP_Theme_Support::get_template_availability() + */ + public function test_get_template_availability_with_available_callback() { + $this->go_to( get_permalink( $this->factory()->post->create() ) ); + add_theme_support( 'amp', array( + 'available_callback' => '__return_true', + ) ); + AMP_Theme_Support::init(); + $availability = AMP_Theme_Support::get_template_availability(); + $this->assertEquals( + $availability, + array( + 'supported' => true, + 'immutable' => true, + 'template' => null, + 'errors' => array(), + ) + ); + + add_theme_support( 'amp', array( + 'available_callback' => '__return_false', + ) ); + AMP_Theme_Support::init(); + $availability = AMP_Theme_Support::get_template_availability(); + $this->assertEquals( + $availability, + array( + 'supported' => false, + 'immutable' => true, + 'template' => null, + 'errors' => array( 'available_callback' ), + ) + ); + } + + /** + * Test get_template_availability. + * + * @covers AMP_Theme_Support::get_template_availability() + */ + public function test_get_template_availability() { + global $wp_query; + $post_id = $this->factory()->post->create(); + query_posts( array( 'p' => $post_id ) ); // phpcs:ignore + + // Test successful match of singular template. + $this->assertTrue( is_singular() ); + AMP_Options_Manager::update_option( 'all_templates_supported', false ); + add_theme_support( 'amp' ); + $availability = AMP_Theme_Support::get_template_availability(); + $this->assertEmpty( $availability['errors'] ); + $this->assertTrue( $availability['supported'] ); + $this->assertFalse( $availability['immutable'] ); + $this->assertEquals( 'is_singular', $availability['template'] ); + + // Test successful match when passing WP_Query and WP_Post into method. + $query = $wp_query; + $wp_query = null; // WPCS: override ok. + $availability = AMP_Theme_Support::get_template_availability( $query ); + $this->assertTrue( $availability['supported'] ); + $this->assertEquals( 'is_singular', $availability['template'] ); + $availability = AMP_Theme_Support::get_template_availability( get_post( $post_id ) ); + $this->assertTrue( $availability['supported'] ); + $this->assertEquals( 'is_singular', $availability['template'] ); + $this->assertNull( $wp_query ); // Make sure it is reset. + + // Test nested hierarchy. + AMP_Options_Manager::update_option( 'supported_templates', array( 'is_special', 'is_custom' ) ); + add_filter( 'amp_supportable_templates', function( $templates ) { + $templates['is_single'] = array( + 'label' => 'Single post', + 'supported' => false, + 'parent' => 'is_singular', + ); + $templates['is_special'] = array( + 'label' => 'Special post', + 'parent' => 'is_single', + 'callback' => function( WP_Query $query ) { + return $query->is_singular() && 'special' === get_post( $query->get_queried_object_id() )->post_name; + }, + ); + $templates['is_page'] = array( + 'label' => 'Page', + 'supported' => true, + 'parent' => 'is_singular', + ); + $templates['is_custom'] = array( + 'label' => 'Custom', + 'callback' => function( WP_Query $query ) { + return false !== $query->get( 'custom', false ); + }, + ); + return $templates; + } ); + add_filter( 'query_vars', function( $vars ) { + $vars[] = 'custom'; + return $vars; + } ); + + $availability = AMP_Theme_Support::get_template_availability( get_post( $post_id ) ); + $this->assertFalse( $availability['supported'] ); + $this->assertTrue( $availability['immutable'] ); + $this->assertEquals( 'is_single', $availability['template'] ); + + $special_id = $this->factory()->post->create( array( + 'post_type' => 'post', + 'post_name' => 'special', + ) ); + $availability = AMP_Theme_Support::get_template_availability( get_post( $special_id ) ); + $this->assertTrue( $availability['supported'] ); + $this->assertEquals( 'is_special', $availability['template'] ); + $this->assertFalse( $availability['immutable'] ); + + $availability = AMP_Theme_Support::get_template_availability( $this->factory()->post->create_and_get( array( 'post_type' => 'page' ) ) ); + $this->assertFalse( $availability['supported'] ); + $this->assertEquals( array( 'post-type-support' ), $availability['errors'] ); + $this->assertEquals( 'is_page', $availability['template'] ); + add_post_type_support( 'page', 'amp' ); + $availability = AMP_Theme_Support::get_template_availability( $this->factory()->post->create_and_get( array( 'post_type' => 'page' ) ) ); + $this->assertTrue( $availability['supported'] ); + + // Test custom. + $this->go_to( '/?custom=1' ); + $availability = AMP_Theme_Support::get_template_availability(); + $this->assertTrue( $availability['supported'] ); + $this->assertEquals( 'is_custom', $availability['template'] ); + } + + /** + * Test get_supportable_templates. + * + * @covers AMP_Theme_Support::get_supportable_templates() + */ + public function test_get_supportable_templates() { + + register_taxonomy( 'accolade', 'post', array( + 'public' => true, + ) ); + register_taxonomy( 'complaint', 'post', array( + 'public' => false, + ) ); + register_post_type( 'announcement', array( + 'public' => true, + 'has_archive' => true, + ) ); + + // Test default case with non-static front page. + update_option( 'show_on_front', 'posts' ); + AMP_Options_Manager::update_option( 'all_templates_supported', true ); + $supportable_templates = AMP_Theme_Support::get_supportable_templates(); + foreach ( $supportable_templates as $id => $supportable_template ) { + $this->assertFalse( is_numeric( $id ) ); + $this->assertArrayHasKey( 'label', $supportable_template, "$id has label" ); + $this->assertTrue( $supportable_template['supported'] ); + $this->assertFalse( $supportable_template['immutable'] ); + } + $this->assertArrayHasKey( 'is_singular', $supportable_templates ); + $this->assertArrayNotHasKey( 'is_front_page', $supportable_templates ); + $this->assertArrayHasKey( 'is_home', $supportable_templates ); + $this->assertArrayNotHasKey( 'parent', $supportable_templates['is_home'] ); + + // Test common templates. + $this->assertArrayHasKey( 'is_singular', $supportable_templates ); + $this->assertArrayHasKey( 'is_archive', $supportable_templates ); + $this->assertArrayHasKey( 'is_author', $supportable_templates ); + $this->assertArrayHasKey( 'is_date', $supportable_templates ); + $this->assertArrayHasKey( 'is_search', $supportable_templates ); + $this->assertArrayHasKey( 'is_404', $supportable_templates ); + $this->assertArrayHasKey( 'is_category', $supportable_templates ); + $this->assertArrayHasKey( 'is_tag', $supportable_templates ); + $this->assertArrayHasKey( 'is_tax[accolade]', $supportable_templates ); + $this->assertEquals( 'is_archive', $supportable_templates['is_tax[accolade]']['parent'] ); + $this->assertArrayNotHasKey( 'is_tax[complaint]', $supportable_templates ); + $this->assertArrayHasKey( 'is_post_type_archive[announcement]', $supportable_templates ); + $this->assertEquals( 'is_archive', $supportable_templates['is_post_type_archive[announcement]']['parent'] ); + + // Test static homepage and page for posts. + $page_on_front = $this->factory()->post->create( array( 'post_type' => 'page' ) ); + $page_for_posts = $this->factory()->post->create( array( 'post_type' => 'page' ) ); + update_option( 'show_on_front', 'page' ); + update_option( 'page_for_posts', $page_for_posts ); + update_option( 'page_on_front', $page_on_front ); + $supportable_templates = AMP_Theme_Support::get_supportable_templates(); + foreach ( $supportable_templates as $id => $supportable_template ) { + $this->assertFalse( is_numeric( $id ) ); + $this->assertArrayHasKey( 'label', $supportable_template, "$id has label" ); + } + $this->assertArrayHasKey( 'is_front_page', $supportable_templates ); + $this->assertArrayHasKey( 'parent', $supportable_templates['is_front_page'] ); + $this->assertEquals( 'is_singular', $supportable_templates['is_front_page']['parent'] ); + + // Test inclusion of custom template, forcing category to be not-supported, and singular to be supported. + add_filter( 'amp_supportable_templates', function( $templates ) { + $templates['is_category']['supported'] = false; + $templates['is_singular']['supported'] = true; + + $templates['is_custom'] = array( + 'label' => 'Custom', + 'callback' => function( WP_Query $query ) { + return false !== $query->get( 'custom', false ); + }, + ); + return $templates; + } ); + $supportable_templates = AMP_Theme_Support::get_supportable_templates(); + $this->assertTrue( $supportable_templates['is_category']['immutable'] ); + $this->assertFalse( $supportable_templates['is_category']['supported'] ); + $this->assertFalse( $supportable_templates['is_category']['user_supported'] ); + $this->assertTrue( $supportable_templates['is_singular']['immutable'] ); + $this->assertTrue( $supportable_templates['is_singular']['supported'] ); + $this->assertTrue( $supportable_templates['is_singular']['user_supported'] ); + $this->assertArrayHasKey( 'is_custom', $supportable_templates ); + remove_all_filters( 'amp_supportable_templates' ); + + // Test supporting templates by theme support args: all. + AMP_Options_Manager::update_option( 'all_templates_supported', false ); + add_theme_support( 'amp', array( + 'templates_supported' => 'all', + ) ); + AMP_Theme_Support::init(); + $supportable_templates = AMP_Theme_Support::get_supportable_templates(); + foreach ( $supportable_templates as $supportable_template ) { + $this->assertTrue( $supportable_template['supported'] ); + $this->assertTrue( $supportable_template['immutable'] ); + } + + // Test supporting templates by theme support args: selective templates. + AMP_Options_Manager::update_option( 'all_templates_supported', false ); + add_theme_support( 'amp', array( + 'templates_supported' => array( + 'is_date' => true, + 'is_author' => false, + ), + ) ); + AMP_Theme_Support::init(); + $supportable_templates = AMP_Theme_Support::get_supportable_templates(); + $this->assertTrue( $supportable_templates['is_date']['supported'] ); + $this->assertTrue( $supportable_templates['is_date']['immutable'] ); + $this->assertFalse( $supportable_templates['is_author']['supported'] ); + $this->assertTrue( $supportable_templates['is_author']['immutable'] ); + $this->assertTrue( $supportable_templates['is_singular']['supported'] ); + $this->assertFalse( $supportable_templates['is_singular']['immutable'] ); + $this->assertFalse( $supportable_templates['is_category']['supported'] ); + $this->assertFalse( $supportable_templates['is_category']['immutable'] ); + } + /** * Test add_hooks. * @@ -759,11 +1115,11 @@ public function test_amend_comment_form() { } /** - * Test filter_paired_template_hierarchy. + * Test filter_amp_template_hierarchy. * - * @covers AMP_Theme_Support::filter_paired_template_hierarchy() + * @covers AMP_Theme_Support::filter_amp_template_hierarchy() */ - public function test_filter_paired_template_hierarchy() { + public function test_filter_amp_template_hierarchy() { $template_dir = 'amp-templates'; add_theme_support( 'amp', array( 'template_dir' => $template_dir, @@ -773,31 +1129,15 @@ public function test_filter_paired_template_hierarchy() { 'single-post.php', 'single.php', ); - $filtered_templates = AMP_Theme_Support::filter_paired_template_hierarchy( $templates ); - foreach ( $filtered_templates as $key => $filtered_template ) { - $this->assertEquals( $template_dir . '/' . $templates[ $key ], $filtered_template ); - } - } + $filtered_templates = AMP_Theme_Support::filter_amp_template_hierarchy( $templates ); - /** - * Test filter_paired_template_include. - * - * @covers AMP_Theme_Support::filter_paired_template_include() - */ - public function test_filter_paired_template_include() { - $template_dir = 'amp-templates'; - $template = 'single.php'; - add_theme_support( 'amp', array( - 'template_dir' => $template_dir, - ) ); - $this->assertEquals( $template, AMP_Theme_Support::filter_paired_template_include( $template ) ); - remove_theme_support( 'amp' ); - try { - AMP_Theme_Support::filter_paired_template_include( $template ); - } catch ( Exception $exception ) { - $e = $exception; + $expected_templates = array(); + foreach ( $templates as $template ) { + $expected_templates[] = $template_dir . '/' . $template; + $expected_templates[] = $template; } - $this->assertTrue( isset( $e ) ); + + $this->assertEquals( $expected_templates, $filtered_templates ); } /** @@ -913,6 +1253,15 @@ public function test_filter_cancel_comment_reply_link() { $this->assertContains( 'Click here to cancel reply.', $filtered_link_no_text_passed ); } + /** + * Test init_admin_bar. + * + * @covers \AMP_Theme_Support::init_admin_bar() + */ + public function test_init_admin_bar() { + $this->markTestIncomplete(); + } + /** * Test print_amp_styles. * @@ -1085,6 +1434,8 @@ public function test_prepare_response() { $wp_widget_factory = new WP_Widget_Factory(); wp_widgets_init(); + $this->assertTrue( is_amp_endpoint() ); + add_action( 'wp_enqueue_scripts', function() { wp_enqueue_script( 'amp-list' ); } ); diff --git a/tests/test-class-amp-widget-archives.php b/tests/test-class-amp-widget-archives.php index bef6d8639ee..4c88a277824 100644 --- a/tests/test-class-amp-widget-archives.php +++ b/tests/test-class-amp-widget-archives.php @@ -32,6 +32,16 @@ public function setUp() { $this->widget = new AMP_Widget_Archives(); } + /** + * Tear down. + * + * @inheritdoc + */ + public function tearDown() { + parent::tearDown(); + remove_theme_support( 'amp' ); + } + /** * Test construct(). * @@ -52,6 +62,7 @@ public function test_construct() { * @see AMP_Widget_Archives::widget(). */ public function test_widget() { + $this->assertTrue( is_amp_endpoint() ); $arguments = array( 'before_widget' => '
', 'after_widget' => '
', diff --git a/tests/test-class-amp-widget-categories.php b/tests/test-class-amp-widget-categories.php index b79bc42dea2..0eea973f18a 100644 --- a/tests/test-class-amp-widget-categories.php +++ b/tests/test-class-amp-widget-categories.php @@ -15,7 +15,7 @@ class Test_AMP_Widget_Categories extends WP_UnitTestCase { /** * Instance of the widget. * - * @var object. + * @var AMP_Widget_Categories */ public $widget; @@ -26,12 +26,22 @@ class Test_AMP_Widget_Categories extends WP_UnitTestCase { */ public function setUp() { parent::setUp(); - add_theme_support( 'amp ' ); + add_theme_support( 'amp' ); wp_maybe_load_widgets(); AMP_Theme_Support::init(); $this->widget = new AMP_Widget_Categories(); } + /** + * Tear down. + * + * @inheritdoc + */ + public function tearDown() { + parent::tearDown(); + remove_theme_support( 'amp' ); + } + /** * Test construct(). * @@ -52,6 +62,7 @@ public function test_construct() { * @covers AMP_Widget_Categories::widget() */ public function test_widget() { + $this->assertTrue( is_amp_endpoint() ); $arguments = array( 'before_widget' => '
', 'after_widget' => '
', diff --git a/tests/test-class-amp-widget-media-video.php b/tests/test-class-amp-widget-media-video.php index d93556d85ab..b1efc616337 100644 --- a/tests/test-class-amp-widget-media-video.php +++ b/tests/test-class-amp-widget-media-video.php @@ -17,7 +17,7 @@ class Test_AMP_Widget_Media_Video extends WP_UnitTestCase { /** * Instance of the widget. * - * @var object. + * @var AMP_Widget_Media_Video. */ public $widget; @@ -34,6 +34,17 @@ public function setUp() { parent::setUp(); wp_maybe_load_widgets(); $this->widget = new $class(); + add_theme_support( 'amp' ); + } + + /** + * Tear down. + * + * @inheritdoc + */ + public function tearDown() { + parent::tearDown(); + remove_theme_support( 'amp' ); } /** @@ -42,6 +53,7 @@ public function setUp() { * @covers AMP_Widget_Media_Video::inject_video_max_width_style() */ public function test_inject_video_max_width_style() { + $this->assertTrue( is_amp_endpoint() ); $video = ''; $video_no_height = ''; $this->assertEquals( $video, $this->widget->inject_video_max_width_style( $video ) ); diff --git a/tests/test-class-amp-widget-text.php b/tests/test-class-amp-widget-text.php index 26d371d666f..f0f0e8b6694 100644 --- a/tests/test-class-amp-widget-text.php +++ b/tests/test-class-amp-widget-text.php @@ -34,6 +34,17 @@ public function setUp() { parent::setUp(); wp_maybe_load_widgets(); $this->widget = new $class(); + add_theme_support( 'amp' ); + } + + /** + * Tear down. + * + * @inheritdoc + */ + public function tearDown() { + parent::tearDown(); + remove_theme_support( 'amp' ); } /** @@ -42,7 +53,7 @@ public function setUp() { * @covers AMP_Widget_Text::inject_video_max_width_style() */ public function test_inject_video_max_width_style() { - add_theme_support( 'amp' ); + $this->assertTrue( is_amp_endpoint() ); $video = ''; $video_only_width = '