diff --git a/amp.php b/amp.php index 7b080e33d08..56344025e68 100644 --- a/amp.php +++ b/amp.php @@ -22,6 +22,7 @@ require_once AMP__DIR__ . '/includes/class-amp-post-type-support.php'; require_once AMP__DIR__ . '/includes/admin/functions.php'; require_once AMP__DIR__ . '/includes/admin/class-amp-customizer.php'; +require_once AMP__DIR__ . '/includes/admin/class-amp-post-meta-box.php'; require_once AMP__DIR__ . '/includes/settings/class-amp-customizer-settings.php'; require_once AMP__DIR__ . '/includes/settings/class-amp-customizer-design-settings.php'; require_once AMP__DIR__ . '/includes/actions/class-amp-frontend-actions.php'; @@ -61,6 +62,7 @@ function amp_init() { load_plugin_textdomain( 'amp', false, plugin_basename( AMP__DIR__ ) . '/languages' ); + amp_define_query_var(); add_rewrite_endpoint( AMP_QUERY_VAR, EP_PERMALINK ); add_filter( 'request', 'amp_force_query_var_value' ); @@ -82,7 +84,11 @@ function amp_init() { * * @since 0.6 */ -function define_query_var() { +function amp_define_query_var() { + if ( defined( 'AMP_QUERY_VAR' ) ) { + return; + } + /** * Filter the AMP query variable. * @@ -91,7 +97,7 @@ function define_query_var() { */ define( 'AMP_QUERY_VAR', apply_filters( 'amp_query_var', 'amp' ) ); } -add_action( 'after_setup_theme', 'define_query_var', 3 ); +add_action( 'after_setup_theme', 'amp_define_query_var', 3 ); // Make sure the `amp` query var has an explicit value. // Avoids issues when filtering the deprecated `query_string` hook. @@ -147,24 +153,40 @@ function amp_prepare_render() { add_action( 'template_redirect', 'amp_render' ); } +/** + * Render AMP for queried post. + * + * @since 0.1 + */ function amp_render() { - $post_id = get_queried_object_id(); - amp_render_post( $post_id ); + + // Note that queried object is used instead of the ID so that the_preview for the queried post can apply. + amp_render_post( get_queried_object() ); exit; } -function amp_render_post( $post_id ) { - $post = get_post( $post_id ); - if ( ! $post ) { - return; +/** + * Render AMP post template. + * + * @since 0.5 + * @param WP_Post|int $post Post. + */ +function amp_render_post( $post ) { + + if ( ! ( $post instanceof WP_Post ) ) { + $post = get_post( $post ); + if ( ! $post ) { + return; + } } + $post_id = $post->ID; amp_load_classes(); do_action( 'pre_amp_render_post', $post_id ); amp_add_post_template_actions(); - $template = new AMP_Post_Template( $post_id ); + $template = new AMP_Post_Template( $post ); $template->load(); } diff --git a/assets/css/amp-post-meta-box.css b/assets/css/amp-post-meta-box.css new file mode 100644 index 00000000000..154667b649f --- /dev/null +++ b/assets/css/amp-post-meta-box.css @@ -0,0 +1,57 @@ +/** +* 1.0 AMP preview. +* +* Submit box preview buttons. +*/ + +/* Core preview button */ +.wp-core-ui #preview-action.has-amp-preview #post-preview { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + float: none; +} + +/* AMP preview button */ +.wp-core-ui #amp-post-preview.preview { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + text-indent: -9999px; + padding-right: 7px; + padding-left: 7px; +} + +.wp-core-ui #amp-post-preview.preview::after { + content: "icon"; + width: 14px; + float: left; + background: no-repeat center url( '../images/amp-icon.svg' ); + background-size: 14px !important; +} + +.wp-core-ui #amp-post-preview.preview.disabled::after { + opacity: 0.6; +} + +/* AMP status */ +.misc-amp-status .amp-icon { + float: left; + background: transparent url( '../images/amp-icon.svg' ) no-repeat left; + background-size: 17px; + width: 17px; + height: 17px; + margin: 0 8px 0 1px; +} + +#amp-status-select { + margin-top: 4px; +} + +.amp-status-actions { + margin-top: 10px; +} + +@media screen and ( max-width: 782px ) { + #amp-status-select { + line-height: 280%; + } +} diff --git a/assets/images/amp-icon.svg b/assets/images/amp-icon.svg new file mode 100644 index 00000000000..a30ea6d2f0d --- /dev/null +++ b/assets/images/amp-icon.svg @@ -0,0 +1,7 @@ + + + AMP-Icon + + + + diff --git a/assets/js/amp-post-meta-box.js b/assets/js/amp-post-meta-box.js new file mode 100644 index 00000000000..6d5d7253fda --- /dev/null +++ b/assets/js/amp-post-meta-box.js @@ -0,0 +1,170 @@ +/* exported ampPostMetaBox */ + +/** + * AMP Post Meta Box. + * + * @todo Rename this to be just the ampEditPostScreen? + * + * @since 0.6 + */ +var ampPostMetaBox = ( function( $ ) { + 'use strict'; + + var component = { + + /** + * Holds data. + * + * @since 0.6 + */ + data: { + previewLink: '', + disabled: false, + statusInputName: '', + l10n: { + ampPreviewBtnLabel: '' + } + }, + + /** + * Toggle animation speed. + * + * @since 0.6 + */ + toggleSpeed: 200, + + /** + * Core preview button selector. + * + * @since 0.6 + */ + previewBtnSelector: '#post-preview', + + /** + * AMP preview button selector. + * + * @since 0.6 + */ + ampPreviewBtnSelector: '#amp-post-preview' + }; + + /** + * Boot plugin. + * + * @since 0.6 + * @param {Object} data Object data. + * @return {void} + */ + component.boot = function boot( data ) { + component.data = data; + $( document ).ready( function() { + if ( ! component.data.disabled ) { + component.addPreviewButton(); + } + component.listen(); + } ); + }; + + /** + * Events listener. + * + * @since 0.6 + * @return {void} + */ + component.listen = function listen() { + $( component.ampPreviewBtnSelector ).on( 'click.amp-post-preview', function( e ) { + e.preventDefault(); + component.onAmpPreviewButtonClick(); + } ); + + $( '.edit-amp-status, [href="#amp_status"]' ).click( function( e ) { + e.preventDefault(); + component.toggleAmpStatus( $( e.target ) ); + } ); + + $( '#submitpost input[type="submit"]' ).on( 'click', function() { + $( component.ampPreviewBtnSelector ).addClass( 'disabled' ); + } ); + }; + + /** + * Add AMP Preview button. + * + * @since 0.6 + * @return {void} + */ + component.addPreviewButton = function addPreviewButton() { + var previewBtn = $( component.previewBtnSelector ); + previewBtn + .clone() + .insertAfter( previewBtn ) + .prop( { + 'href': component.data.previewLink, + 'id': component.ampPreviewBtnSelector.replace( '#', '' ) + } ) + .text( component.data.l10n.ampPreviewBtnLabel ) + .parent() + .addClass( 'has-amp-preview' ); + }; + + /** + * AMP Preview button click handler. + * + * We trigger the Core preview link for events propagation purposes. + * + * @since 0.6 + * @return {void} + */ + component.onAmpPreviewButtonClick = function onAmpPreviewButtonClick() { + var $input; + + // Flag the AMP preview referer. + $input = $( '' ) + .prop( { + 'type': 'hidden', + 'name': 'amp-preview', + 'value': 'do-preview' + } ) + .insertAfter( component.ampPreviewBtnSelector ); + + // Trigger Core preview button and remove AMP flag. + $( component.previewBtnSelector ).click(); + $input.remove(); + }; + + /** + * Add AMP status toggle. + * + * @since 0.6 + * @param {Object} $target Event target. + * @return {void} + */ + component.toggleAmpStatus = function toggleAmpStatus( $target ) { + var $container = $( '#amp-status-select' ), + status = $container.data( 'amp-status' ), + $checked, + editAmpStatus = $( '.edit-amp-status' ); + + // Don't modify status on cancel button click. + if ( ! $target.hasClass( 'button-cancel' ) ) { + status = $( '[name="' + component.data.statusInputName + '"]:checked' ).val(); + } + + $checked = $( '#amp-status-' + status ); + + // Toggle elements. + editAmpStatus.fadeToggle( component.toggleSpeed, function() { + if ( editAmpStatus.is( ':visible' ) ) { + editAmpStatus.focus(); + } + } ); + $container.slideToggle( component.toggleSpeed ); + + // Update status. + $container.data( 'amp-status', status ); + $checked.prop( 'checked', true ); + $( '.amp-status-text' ).text( $checked.next().text() ); + }; + + return component; +})( window.jQuery ); diff --git a/includes/admin/class-amp-post-meta-box.php b/includes/admin/class-amp-post-meta-box.php new file mode 100644 index 00000000000..e3b23270357 --- /dev/null +++ b/includes/admin/class-amp-post-meta-box.php @@ -0,0 +1,200 @@ +base ) + && + 'post' === $screen->base + && + post_type_supports( $post->post_type, AMP_QUERY_VAR ) + ); + if ( ! $validate ) { + return; + } + + // Styles. + wp_enqueue_style( + self::ASSETS_HANDLE, + amp_get_asset_url( 'css/amp-post-meta-box.css' ), + false, + AMP__VERSION + ); + + // Scripts. + wp_enqueue_script( + self::ASSETS_HANDLE, + amp_get_asset_url( 'js/amp-post-meta-box.js' ), + array( 'jquery' ), + AMP__VERSION + ); + wp_add_inline_script( self::ASSETS_HANDLE, sprintf( 'ampPostMetaBox.boot( %s );', + wp_json_encode( array( + 'previewLink' => esc_url_raw( add_query_arg( AMP_QUERY_VAR, '', get_preview_post_link( $post ) ) ), + 'disabled' => (bool) get_post_meta( $post->ID, self::DISABLED_POST_META_KEY, true ), + 'statusInputName' => self::STATUS_INPUT_NAME, + 'l10n' => array( + 'ampPreviewBtnLabel' => __( 'Preview changes in AMP (opens in new window)', 'amp' ), + ), + ) ) + ) ); + } + + /** + * Render AMP status. + * + * @since 0.6 + * @param WP_Post $post Post. + */ + public function render_status( $post ) { + $verify = ( + isset( $post->ID ) + && + isset( $post->post_type ) + && + post_type_supports( $post->post_type, AMP_QUERY_VAR ) + && + current_user_can( 'edit_post', $post->ID ) + ); + + if ( true !== $verify ) { + return; + } + + // The following variables are used inside amp-status.php template. + $disabled = (bool) get_post_meta( $post->ID, self::DISABLED_POST_META_KEY, true ); + $status = $disabled ? 'disabled' : 'enabled'; + $labels = array( + 'enabled' => __( 'Enabled', 'amp' ), + 'disabled' => __( 'Disabled', 'amp' ), + ); + + include_once AMP__DIR__ . '/templates/admin/amp-status.php'; + } + + /** + * Save AMP Status. + * + * @since 0.6 + * @param int $post_id The Post ID. + */ + public function save_amp_status( $post_id ) { + $verify = ( + isset( $_POST[ self::NONCE_NAME ] ) + && + isset( $_POST[ self::STATUS_INPUT_NAME ] ) + && + wp_verify_nonce( sanitize_key( wp_unslash( $_POST[ self::NONCE_NAME ] ) ), self::NONCE_ACTION ) + && + current_user_can( 'edit_post', $post_id ) + && + ! wp_is_post_revision( $post_id ) + && + ! wp_is_post_autosave( $post_id ) + ); + + if ( true === $verify ) { + if ( 'disabled' === $_POST[ self::STATUS_INPUT_NAME ] ) { + update_post_meta( $post_id, self::DISABLED_POST_META_KEY, true ); + } else { + delete_post_meta( $post_id, self::DISABLED_POST_META_KEY ); + } + } + } + + /** + * Modify post preview link. + * + * Add the AMP query var is the amp-preview flag is set. + * + * @since 0.6 + * + * @param string $link The post preview link. + * @return string Preview URL. + */ + public function preview_post_link( $link ) { + $is_amp = ( + isset( $_POST['amp-preview'] ) // WPCS: CSRF ok. + && + 'do-preview' === sanitize_key( wp_unslash( $_POST['amp-preview'] ) ) // WPCS: CSRF ok. + ); + + if ( $is_amp ) { + $link = add_query_arg( AMP_QUERY_VAR, true, $link ); + } + + return $link; + } + +} diff --git a/includes/admin/functions.php b/includes/admin/functions.php index a6a8e82e8fd..85faf0dd310 100644 --- a/includes/admin/functions.php +++ b/includes/admin/functions.php @@ -124,3 +124,16 @@ function amp_add_custom_analytics( $analytics ) { return $analytics; } add_filter( 'amp_post_template_analytics', 'amp_add_custom_analytics' ); + +/** + * Bootstrap AMP post meta box. + * + * This function must be invoked only once through the 'wp_loaded' action. + * + * @since 0.6 + */ +function amp_post_meta_box() { + $post_meta_box = new AMP_Post_Meta_Box(); + $post_meta_box->init(); +} +add_action( 'wp_loaded', 'amp_post_meta_box' ); diff --git a/includes/amp-helper-functions.php b/includes/amp-helper-functions.php index d9c5d86ea93..040418d440f 100644 --- a/includes/amp-helper-functions.php +++ b/includes/amp-helper-functions.php @@ -17,16 +17,39 @@ function amp_get_permalink( $post_id ) { return apply_filters( 'amp_get_permalink', $amp_url, $post_id ); } +/** + * Determine whether a given post supports AMP. + * + * @since 0.1 + * + * @param WP_Post $post Post. + * @return bool Whether the post supports AMP. + */ function post_supports_amp( $post ) { - // Because `add_rewrite_endpoint` doesn't let us target specific post_types :( + + // Because `add_rewrite_endpoint` doesn't let us target specific post_types. if ( ! post_type_supports( $post->post_type, AMP_QUERY_VAR ) ) { return false; } + // Skip based on postmeta. + if ( ! isset( $post->ID ) || (bool) get_post_meta( $post->ID, AMP_Post_Meta_Box::DISABLED_POST_META_KEY, true ) ) { + return false; + } + if ( post_password_required( $post ) ) { return false; } + /** + * Filters whether to skip the post from AMP. + * + * @since 0.3 + * + * @param bool $skipped Skipped. + * @param int $post_id Post ID. + * @param WP_Post $post Post. + */ if ( true === apply_filters( 'amp_skip_post', false, $post->ID, $post ) ) { return false; } diff --git a/includes/class-amp-post-template.php b/includes/class-amp-post-template.php index 8c1adc7cdc6..49177f8ede4 100644 --- a/includes/class-amp-post-template.php +++ b/includes/class-amp-post-template.php @@ -1,48 +1,127 @@ template_dir = apply_filters( 'amp_post_template_dir', AMP__DIR__ . '/templates' ); - $this->ID = $post_id; - $this->post = get_post( $post_id ); + if ( $post instanceof WP_Post ) { + $this->post = $post; + } else { + $this->post = get_post( $post ); + } + $this->ID = $this->post->ID; $content_max_width = self::CONTENT_MAX_WIDTH; if ( isset( $GLOBALS['content_width'] ) && $GLOBALS['content_width'] > 0 ) { @@ -51,34 +130,34 @@ public function __construct( $post_id ) { $content_max_width = apply_filters( 'amp_content_max_width', $content_max_width ); $this->data = array( - 'content_max_width' => $content_max_width, + 'content_max_width' => $content_max_width, - 'document_title' => function_exists( 'wp_get_document_title' ) ? wp_get_document_title() : wp_title( '', false ), // back-compat with 4.3 - 'canonical_url' => get_permalink( $post_id ), - 'home_url' => home_url(), - 'blog_name' => get_bloginfo( 'name' ), + 'document_title' => function_exists( 'wp_get_document_title' ) ? wp_get_document_title() : wp_title( '', false ), // Back-compat with 4.3. + 'canonical_url' => get_permalink( $this->ID ), + 'home_url' => home_url(), + 'blog_name' => get_bloginfo( 'name' ), 'generator_metadata' => 'AMP Plugin v' . AMP__VERSION, - 'html_tag_attributes' => array(), - 'body_class' => '', + 'html_tag_attributes' => array(), + 'body_class' => '', - 'site_icon_url' => apply_filters( 'amp_site_icon_url', function_exists( 'get_site_icon_url' ) ? get_site_icon_url( self::SITE_ICON_SIZE ) : '' ), + 'site_icon_url' => apply_filters( 'amp_site_icon_url', function_exists( 'get_site_icon_url' ) ? get_site_icon_url( self::SITE_ICON_SIZE ) : '' ), 'placeholder_image_url' => amp_get_asset_url( 'images/placeholder-icon.png' ), - 'featured_image' => false, - 'comments_link_url' => false, - 'comments_link_text' => false, + 'featured_image' => false, + 'comments_link_url' => false, + 'comments_link_text' => false, - 'amp_runtime_script' => 'https://cdn.ampproject.org/v0.js', + 'amp_runtime_script' => 'https://cdn.ampproject.org/v0.js', 'amp_component_scripts' => array(), - 'customizer_settings' => array(), + 'customizer_settings' => array(), - 'font_urls' => array( + 'font_urls' => array( 'merriweather' => 'https://fonts.googleapis.com/css?family=Merriweather:400,400italic,700,700italic', ), - 'post_amp_styles' => array(), + 'post_amp_styles' => array(), /** * Add amp-analytics tags. @@ -86,18 +165,26 @@ public function __construct( $post_id ) { * This filter allows you to easily insert any amp-analytics tags without needing much heavy lifting. * * @since 0.4 - *. - * @param array $analytics An associative array of the analytics entries we want to output. Each array entry must have a unique key, and the value should be an array with the following keys: `type`, `attributes`, `script_data`. See readme for more details. - * @param object $post The current post. + * + * @param array $analytics An associative array of the analytics entries we want to output. Each array entry must have a unique key, and the value should be an array with the following keys: `type`, `attributes`, `script_data`. See readme for more details. + * @param WP_Post $post The current post. */ 'amp_analytics' => apply_filters( 'amp_post_template_analytics', array(), $this->post ), - ); + ); $this->build_post_content(); $this->build_post_data(); $this->build_customizer_settings(); $this->build_html_tag_attributes(); + /** + * Filters AMP template data. + * + * @since 0.2 + * + * @param array $data Template data. + * @param WP_Post $post Post. + */ $this->data = apply_filters( 'amp_post_template_data', $this->data, $this->post ); } diff --git a/templates/admin/amp-status.php b/templates/admin/amp-status.php new file mode 100644 index 00000000000..170b4f13c7b --- /dev/null +++ b/templates/admin/amp-status.php @@ -0,0 +1,34 @@ + +
+ + + + + + + +
+ > + +
+ > + +
+ +
+ + +
+
+
diff --git a/tests/test-class-amp-meta-box.php b/tests/test-class-amp-meta-box.php new file mode 100644 index 00000000000..e9b7d7eaa71 --- /dev/null +++ b/tests/test-class-amp-meta-box.php @@ -0,0 +1,157 @@ +instance = new AMP_Post_Meta_Box(); + } + + /** + * Test init. + * + * @see AMP_Settings::init() + */ + public function test_init() { + $this->instance->init(); + $this->assertEquals( 10, has_action( 'admin_enqueue_scripts', array( $this->instance, 'enqueue_admin_assets' ) ) ); + $this->assertEquals( 10, has_action( 'post_submitbox_misc_actions', array( $this->instance, 'render_status' ) ) ); + $this->assertEquals( 10, has_action( 'save_post', array( $this->instance, 'save_amp_status' ) ) ); + } + + /** + * Test enqueue_admin_assets. + * + * @see AMP_Settings::enqueue_admin_assets() + */ + public function test_enqueue_admin_assets() { + // Test enqueue outside of a post with AMP support. + $this->assertFalse( wp_style_is( AMP_Post_Meta_Box::ASSETS_HANDLE ) ); + $this->assertFalse( wp_script_is( AMP_Post_Meta_Box::ASSETS_HANDLE ) ); + $this->instance->enqueue_admin_assets( 'foo-bar.php' ); + $this->assertFalse( wp_style_is( AMP_Post_Meta_Box::ASSETS_HANDLE ) ); + + // Test enqueue on a post with AMP support. + $post = self::factory()->post->create_and_get(); + $GLOBALS['post'] = $post; + set_current_screen( 'post.php' ); + $this->instance->enqueue_admin_assets(); + $this->assertTrue( wp_style_is( AMP_Post_Meta_Box::ASSETS_HANDLE ) ); + $this->assertTrue( wp_script_is( AMP_Post_Meta_Box::ASSETS_HANDLE ) ); + $script_data = wp_scripts()->get_data( AMP_Post_Meta_Box::ASSETS_HANDLE, 'after' ); + + if ( empty( $script_data ) ) { + $this->markTestIncomplete( 'Script data could not be found.' ); + } + + // Test inline script boot. + $this->assertTrue( false !== stripos( wp_json_encode( $script_data ), 'ampPostMetaBox.boot(' ) ); + unset( $GLOBALS['post'] ); + } + + /** + * Test render_status. + * + * @see AMP_Settings::render_status() + */ + public function test_render_status() { + $post = $this->factory->post->create_and_get(); + wp_set_current_user( $this->factory->user->create( array( + 'role' => 'administrator', + ) ) ); + + ob_start(); + $this->instance->render_status( $post ); + $this->assertContains( '
instance->render_status( $post ); + $this->assertEmpty( ob_get_clean() ); + + add_post_type_support( 'post', AMP_QUERY_VAR ); + wp_set_current_user( $this->factory->user->create( array( + 'role' => 'subscriber', + ) ) ); + + ob_start(); + $this->instance->render_status( $post ); + $this->assertEmpty( ob_get_clean() ); + } + + /** + * Test save_amp_status. + * + * @see AMP_Settings::save_amp_status() + */ + public function test_save_amp_status() { + // Test failure. + $post_id = $this->factory->post->create(); + $this->assertEmpty( get_post_meta( $post_id, AMP_Post_Meta_Box::DISABLED_POST_META_KEY, true ) ); + + // Setup for success. + wp_set_current_user( $this->factory->user->create( array( + 'role' => 'administrator', + ) ) ); + $_POST[ AMP_Post_Meta_Box::NONCE_NAME ] = wp_create_nonce( AMP_Post_Meta_Box::NONCE_ACTION ); + $_POST[ AMP_Post_Meta_Box::STATUS_INPUT_NAME ] = 'disabled'; + + // Test revision bail. + $post_id = $this->factory->post->create(); + delete_post_meta( $post_id, AMP_Post_Meta_Box::DISABLED_POST_META_KEY ); + wp_save_post_revision( $post_id ); + $this->assertEmpty( get_post_meta( $post_id, AMP_Post_Meta_Box::DISABLED_POST_META_KEY, true ) ); + + // Test post update success to disable. + $post_id = $this->factory->post->create(); + delete_post_meta( $post_id, AMP_Post_Meta_Box::DISABLED_POST_META_KEY ); + wp_update_post( array( + 'ID' => $post_id, + 'post_title' => 'updated', + ) ); + $this->assertTrue( (bool) get_post_meta( $post_id, AMP_Post_Meta_Box::DISABLED_POST_META_KEY, true ) ); + + // Test post update success to enable. + $_POST[ AMP_Post_Meta_Box::STATUS_INPUT_NAME ] = 'enabled'; + delete_post_meta( $post_id, AMP_Post_Meta_Box::DISABLED_POST_META_KEY ); + wp_update_post( array( + 'ID' => $post_id, + 'post_title' => 'updated', + ) ); + $this->assertFalse( (bool) get_post_meta( $post_id, AMP_Post_Meta_Box::DISABLED_POST_META_KEY, true ) ); + } + + /** + * Test preview_post_link. + * + * @see AMP_Settings::preview_post_link() + */ + public function test_preview_post_link() { + $link = 'https://foo.bar'; + $this->assertEquals( 'https://foo.bar', $this->instance->preview_post_link( $link ) ); + $_POST['amp-preview'] = 'do-preview'; + $this->assertEquals( 'https://foo.bar?' . AMP_QUERY_VAR . '=1', $this->instance->preview_post_link( $link ) ); + } + +}