PDF rausgenommen

This commit is contained in:
aschwarz
2023-01-23 11:03:31 +01:00
parent 82d562a322
commit a6523903eb
28078 changed files with 4247552 additions and 2 deletions

View File

@ -0,0 +1,149 @@
<?php
/**
* Admin pointer class.
*
* @package AMP
* @since 1.0
*/
/**
* AMP_Admin_Pointer class.
*
* Outputs an admin pointer to show the new features of v1.0.
* Based on https://code.tutsplus.com/articles/integrating-with-wordpress-ui-admin-pointers--wp-26853
*
* @since 1.0
*/
class AMP_Admin_Pointer {
/**
* The ID of the template mode admin pointer.
*
* @var string
*/
const TEMPLATE_POINTER_ID = 'amp_template_mode_pointer_10';
/**
* The slug of the script.
*
* @var string
*/
const SCRIPT_SLUG = 'amp-admin-pointer';
/**
* The slug of the tooltip script.
*
* @var string
*/
const TOOLTIP_SLUG = 'amp-validation-tooltips';
/**
* Initializes the class.
*
* @since 1.0
*/
public function init() {
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_pointer' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'register_tooltips' ) );
}
/**
* Enqueues the pointer assets.
*
* If the pointer has not been dismissed, enqueues the style and script.
* And outputs the pointer data for the script.
*
* @since 1.0
*/
public function enqueue_pointer() {
if ( $this->is_pointer_dismissed() ) {
return;
}
wp_enqueue_style( 'wp-pointer' );
wp_enqueue_script(
self::SCRIPT_SLUG,
amp_get_asset_url( 'js/' . self::SCRIPT_SLUG . '.js' ),
array( 'jquery', 'wp-pointer' ),
AMP__VERSION,
true
);
wp_add_inline_script(
self::SCRIPT_SLUG,
sprintf( 'ampAdminPointer.load( %s );', wp_json_encode( $this->get_pointer_data() ) )
);
}
/**
* Registers style and script for tooltips.
*
* @since 1.0
*/
public function register_tooltips() {
wp_register_style(
self::TOOLTIP_SLUG,
amp_get_asset_url( 'css/' . self::TOOLTIP_SLUG . '.css' ),
array( 'wp-pointer' ),
AMP__VERSION
);
wp_register_script(
self::TOOLTIP_SLUG,
amp_get_asset_url( 'js/' . self::TOOLTIP_SLUG . '.js' ),
array( 'jquery', 'wp-pointer' ),
AMP__VERSION,
true
);
}
/**
* Whether the AMP admin pointer has been dismissed.
*
* @since 1.0
* @return boolean Is dismissed.
*/
protected function is_pointer_dismissed() {
// Consider dismissed in v1.1, since admin pointer is only to educate about the new modes in 1.0.
if ( version_compare( strtok( AMP__VERSION, '-' ), '1.1', '>=' ) ) {
return true;
}
$dismissed = get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true );
if ( empty( $dismissed ) ) {
return false;
}
$dismissed = explode( ',', strval( $dismissed ) );
return in_array( self::TEMPLATE_POINTER_ID, $dismissed, true );
}
/**
* Gets the pointer data to pass to the script.
*
* @since 1.0
* @return array Pointer data.
*/
public function get_pointer_data() {
return array(
'pointer' => array(
'pointer_id' => self::TEMPLATE_POINTER_ID,
'target' => '#toplevel_page_amp-options',
'options' => array(
'content' => sprintf(
'<h3>%s</h3><p><strong>%s</strong></p><p>%s</p>',
__( 'AMP', 'amp' ),
__( 'New AMP Template Modes', 'amp' ),
__( 'You can now reuse your theme\'s templates and styles in AMP responses, in both &#8220;Transitional&#8221; and &#8220;Native&#8221; modes.', 'amp' )
),
'position' => array(
'edge' => 'left',
'align' => 'middle',
),
),
),
);
}
}

View File

@ -0,0 +1,332 @@
<?php
/**
* Class AMP_Template_Customizer
*
* @package AMP
*/
/**
* AMP class that implements a template style editor in the Customizer.
*
* A direct, formed link to the AMP editor in the Customizer is added via
* {@see amp_customizer_editor_link()} as a submenu to the Appearance menu.
*
* @since 0.4
*/
class AMP_Template_Customizer {
/**
* AMP template editor panel ID.
*
* @since 0.4
* @var string
*/
const PANEL_ID = 'amp_panel';
/**
* Customizer instance.
*
* @since 0.4
* @access protected
* @var WP_Customize_Manager $wp_customize
*/
protected $wp_customize;
/**
* Initialize the template Customizer feature class.
*
* @static
* @since 0.4
* @access public
*
* @param WP_Customize_Manager $wp_customize Customizer instance.
*/
public static function init( $wp_customize ) {
$self = new self();
$self->wp_customize = $wp_customize;
/**
* Fires when the AMP Template Customizer initializes.
*
* In practice the `customize_register` hook should be used instead.
*
* @since 0.4
* @param AMP_Template_Customizer $self Instance.
*/
do_action( 'amp_customizer_init', $self );
$self->register_settings();
$self->register_ui();
add_action( 'customize_controls_enqueue_scripts', array( $self, 'add_customizer_scripts' ) );
add_action( 'customize_controls_print_footer_scripts', array( $self, 'print_controls_templates' ) );
add_action( 'customize_preview_init', array( $self, 'init_preview' ) );
}
/**
* Init Customizer preview.
*
* @since 0.4
* @global WP_Customize_Manager $wp_customize
*/
public function init_preview() {
add_action( 'amp_post_template_head', 'wp_no_robots' );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_preview_scripts' ) );
add_action( 'amp_customizer_enqueue_preview_scripts', array( $this, 'enqueue_preview_scripts' ) );
// Output scripts and styles which will break AMP validation only when preview is opened with controls for manipulation.
if ( $this->wp_customize->get_messenger_channel() ) {
add_action( 'amp_post_template_head', array( $this->wp_customize, 'customize_preview_loading_style' ) );
add_action( 'amp_post_template_css', array( $this, 'add_customize_preview_styles' ) );
add_action( 'amp_post_template_head', array( $this->wp_customize, 'remove_frameless_preview_messenger_channel' ) );
add_action( 'amp_post_template_footer', array( $this, 'add_preview_scripts' ) );
}
}
/**
* Sets up the AMP Customizer preview.
*/
public function register_ui() {
$this->wp_customize->add_panel(
self::PANEL_ID,
array(
'type' => 'amp',
'title' => __( 'AMP', 'amp' ),
/* translators: placeholder is URL to AMP project. */
'description' => sprintf( __( '<a href="%s" target="_blank">The AMP Project</a> is a Google-led initiative that dramatically improves loading speeds on phones and tablets. You can use the Customizer to preview changes to your AMP template before publishing them.', 'amp' ), 'https://ampproject.org' ),
)
);
/**
* Fires after the AMP panel has been registered for plugins to add additional controls.
*
* In practice the `customize_register` hook should be used instead.
*
* @since 0.4
* @param WP_Customize_Manager $manager Manager.
*/
do_action( 'amp_customizer_register_ui', $this->wp_customize );
}
/**
* Registers settings for customizing AMP templates.
*
* @since 0.4
*/
public function register_settings() {
/**
* Fires when plugins should register settings for AMP.
*
* In practice the `customize_register` hook should be used instead.
*
* @since 0.4
* @param WP_Customize_Manager $manager Manager.
*/
do_action( 'amp_customizer_register_settings', $this->wp_customize );
}
/**
* Load up AMP scripts needed for Customizer integrations.
*
* @since 0.6
*/
public function add_customizer_scripts() {
if ( ! amp_is_canonical() ) {
wp_enqueue_script(
'amp-customize-controls',
amp_get_asset_url( 'js/amp-customize-controls.js' ),
array( 'jquery', 'customize-controls' ),
AMP__VERSION,
true
);
wp_add_inline_script(
'amp-customize-controls',
sprintf(
'ampCustomizeControls.boot( %s );',
wp_json_encode(
array(
'queryVar' => amp_get_slug(),
'panelId' => self::PANEL_ID,
'ampUrl' => amp_admin_get_preview_permalink(),
'l10n' => array(
'unavailableMessage' => __( 'AMP is not available for the page currently being previewed.', 'amp' ),
'unavailableLinkText' => __( 'Navigate to an AMP compatible page', 'amp' ),
),
)
)
)
);
// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
wp_enqueue_style(
'amp-customizer',
amp_get_asset_url( 'css/amp-customizer.css' )
);
}
/**
* Fires when plugins should register settings for AMP.
*
* In practice the `customize_controls_enqueue_scripts` hook should be used instead.
*
* @since 0.4
* @param WP_Customize_Manager $manager Manager.
*/
do_action( 'amp_customizer_enqueue_scripts', $this->wp_customize );
}
/**
* Enqueues scripts used in both the AMP and non-AMP Customizer preview.
*
* @since 0.6
* @global WP_Query $wp_query
*/
public function enqueue_preview_scripts() {
global $wp_query;
// Bail if user can't customize anyway.
if ( ! current_user_can( 'customize' ) ) {
return;
}
wp_enqueue_script(
'amp-customize-preview',
amp_get_asset_url( 'js/amp-customize-preview.js' ),
array( 'jquery', 'customize-preview' ),
AMP__VERSION,
true
);
if ( current_theme_supports( AMP_Theme_Support::SLUG ) ) {
$availability = AMP_Theme_Support::get_template_availability();
$available = $availability['supported'];
} elseif ( is_singular() || $wp_query->is_posts_page ) {
/**
* Queried object.
*
* @var WP_Post $queried_object
*/
$queried_object = get_queried_object();
$available = post_supports_amp( $queried_object );
} else {
$available = false;
}
wp_add_inline_script(
'amp-customize-preview',
sprintf(
'ampCustomizePreview.boot( %s );',
wp_json_encode(
array(
'available' => $available,
'enabled' => is_amp_endpoint(),
)
)
)
);
}
/**
* Add AMP Customizer preview styles.
*/
public function add_customize_preview_styles() {
?>
/* Text meant only for screen readers; this is needed for wp.a11y.speak() */
.screen-reader-text {
border: 0;
clip: rect(1px, 1px, 1px, 1px);
-webkit-clip-path: inset(50%);
clip-path: inset(50%);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
word-wrap: normal !important;
}
body.wp-customizer-unloading {
opacity: 0.25 !important; /* Because AMP sets body to opacity:1 once layout complete. */
}
<?php
}
/**
* Enqueues scripts and does wp_print_footer_scripts() so we can output customizer scripts.
*
* This breaks AMP validation in the customizer but is necessary for the live preview.
*
* @since 0.6
*/
public function add_preview_scripts() {
// Bail if user can't customize anyway.
if ( ! current_user_can( 'customize' ) ) {
return;
}
wp_enqueue_script( 'customize-selective-refresh' );
wp_enqueue_script( 'amp-customize-preview' );
/**
* Fires when plugins should enqueue their own scripts for the AMP Customizer preview.
*
* @since 0.4
* @param WP_Customize_Manager $wp_customize Manager.
*/
do_action( 'amp_customizer_enqueue_preview_scripts', $this->wp_customize );
$this->wp_customize->customize_preview_settings();
$this->wp_customize->selective_refresh->export_preview_data();
wp_print_footer_scripts();
}
/**
* Print templates needed for AMP in Customizer.
*
* @since 0.6
*/
public function print_controls_templates() {
?>
<script type="text/html" id="tmpl-customize-amp-enabled-toggle">
<div class="amp-toggle">
<# var elementIdPrefix = _.uniqueId( 'customize-amp-enabled-toggle' ); #>
<div id="{{ elementIdPrefix }}tooltip" aria-hidden="true" class="tooltip" role="tooltip">
{{ data.message }}
<# if ( data.url ) { #>
<a href="{{ data.url }}">{{ data.linkText }}</a>
<# } #>
</div>
<input id="{{ elementIdPrefix }}checkbox" type="checkbox" class="disabled" aria-describedby="{{ elementIdPrefix }}tooltip">
<span class="slider"></span>
<label for="{{ elementIdPrefix }}checkbox" class="screen-reader-text"><?php esc_html_e( 'AMP preview enabled', 'amp' ); ?></label>
</div>
</script>
<script type="text/html" id="tmpl-customize-amp-unavailable-notification">
<li class="notice notice-{{ data.type || 'info' }} {{ data.alt ? 'notice-alt' : '' }} {{ data.containerClasses || '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}">
<div class="notification-message">
{{ data.message }}
<# if ( data.url ) { #>
<a href="{{ data.url }}">{{ data.linkText }}</a>
<# } #>
</div>
</li>
</script>
<?php
}
/**
* Whether the Customizer is AMP. This is always true since the AMP Customizer has been merged with the main Customizer.
*
* @deprecated 0.6
* @return bool
*/
public static function is_amp_customizer() {
_deprecated_function( __METHOD__, '0.6' );
return true;
}
}

View File

@ -0,0 +1,210 @@
<?php
/**
* AMP Editor Blocks extending.
*
* @package AMP
* @since 1.0
*/
/**
* Class AMP_Editor_Blocks
*/
class AMP_Editor_Blocks {
/**
* List of AMP scripts that need to be printed when AMP components are used in non-AMP document context ("dirty AMP").
*
* @var array
*/
public $content_required_amp_scripts = array();
/**
* AMP components that have blocks.
*
* @var array
*/
public $amp_blocks = array(
'amp-mathml',
'amp-timeago',
'amp-o2-player',
'amp-ooyala-player',
'amp-reach-player',
'amp-springboard-player',
'amp-jwplayer',
'amp-brid-player',
'amp-ima-video',
'amp-fit-text',
);
/**
* Init.
*/
public function init() {
if ( function_exists( 'register_block_type' ) ) {
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 transitional 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 <https://github.com/ampproject/amp-wp/issues/1192>.
*/
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' ) );
}
}
}
/**
* Whitelist elements and attributes used for AMP.
*
* This prevents AMP markup from being deleted in
*
* @param array $tags Array of allowed post tags.
* @param string $context Context.
* @return mixed Modified array.
*/
public function whitelist_block_atts_in_wp_kses_allowed_html( $tags, $context ) {
if ( 'post' !== $context ) {
return $tags;
}
foreach ( $tags as &$tag ) {
if ( ! is_array( $tag ) ) {
continue;
}
$tag['data-amp-layout'] = true;
$tag['data-amp-noloading'] = true;
$tag['data-amp-lightbox'] = true;
$tag['data-close-button-aria-label'] = true;
}
foreach ( $this->amp_blocks as $amp_block ) {
if ( ! isset( $tags[ $amp_block ] ) ) {
$tags[ $amp_block ] = array();
}
// @todo The global attributes included here should be matched up with what is actually used by each block.
$tags[ $amp_block ] = array_merge(
array_fill_keys(
array(
'layout',
'width',
'height',
'class',
),
true
),
$tags[ $amp_block ]
);
$amp_tag_specs = AMP_Allowed_Tags_Generated::get_allowed_tag( $amp_block );
foreach ( $amp_tag_specs as $amp_tag_spec ) {
if ( ! isset( $amp_tag_spec[ AMP_Rule_Spec::ATTR_SPEC_LIST ] ) ) {
continue;
}
$tags[ $amp_block ] = array_merge(
$tags[ $amp_block ],
array_fill_keys( array_keys( $amp_tag_spec[ AMP_Rule_Spec::ATTR_SPEC_LIST ] ), true )
);
}
}
return $tags;
}
/**
* Enqueue filters for extending core blocks attributes.
* Has to be loaded before registering the blocks in registerCoreBlocks.
*/
public function enqueue_block_editor_assets() {
// Enqueue script and style for AMP-specific blocks.
if ( amp_is_canonical() ) {
wp_enqueue_style(
'amp-editor-blocks-style',
amp_get_asset_url( 'css/amp-editor-blocks.css' ),
array(),
AMP__VERSION
);
// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NotInFooter
wp_enqueue_script(
'amp-editor-blocks-build',
amp_get_asset_url( 'js/amp-blocks-compiled.js' ),
array( 'wp-editor', 'wp-blocks', 'lodash', 'wp-i18n', 'wp-element', 'wp-components' ),
AMP__VERSION
);
if ( function_exists( 'wp_set_script_translations' ) ) {
wp_set_script_translations( 'amp-editor-blocks-build', 'amp' );
}
}
wp_enqueue_script(
'amp-editor-blocks',
amp_get_asset_url( 'js/amp-editor-blocks.js' ),
array( 'underscore', 'wp-hooks', 'wp-i18n', 'wp-components' ),
AMP__VERSION,
true
);
wp_add_inline_script(
'amp-editor-blocks',
sprintf(
'ampEditorBlocks.boot( %s );',
wp_json_encode(
array(
'hasThemeSupport' => current_theme_supports( AMP_Theme_Support::SLUG ),
)
)
)
);
if ( function_exists( 'wp_set_script_translations' ) ) {
wp_set_script_translations( 'amp-editor-blocks', 'amp' );
} elseif ( function_exists( 'wp_get_jed_locale_data' ) || function_exists( 'gutenberg_get_jed_locale_data' ) ) {
$locale_data = function_exists( 'wp_get_jed_locale_data' ) ? wp_get_jed_locale_data( 'amp' ) : gutenberg_get_jed_locale_data( 'amp' );
wp_add_inline_script(
'wp-i18n',
'wp.i18n.setLocaleData( ' . wp_json_encode( $locale_data ) . ', "amp" );',
'after'
);
}
}
/**
* 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 );
}
}
}

View File

@ -0,0 +1,394 @@
<?php
/**
* AMP meta box settings.
*
* @package AMP
* @since 0.6
*/
/**
* Post meta box class.
*
* @since 0.6
*/
class AMP_Post_Meta_Box {
/**
* Assets handle.
*
* @since 0.6
* @var string
*/
const ASSETS_HANDLE = 'amp-post-meta-box';
/**
* Block asset handle.
*
* @since 1.0
* @var string
*/
const BLOCK_ASSET_HANDLE = 'amp-block-editor-toggle-compiled';
/**
* The enabled status post meta value.
*
* @since 0.6
* @var string
*/
const ENABLED_STATUS = 'enabled';
/**
* The disabled status post meta value.
*
* @since 0.6
* @var string
*/
const DISABLED_STATUS = 'disabled';
/**
* The status post meta key.
*
* @since 0.6
* @var string
*/
const STATUS_POST_META_KEY = 'amp_status';
/**
* The field name for the enabled/disabled radio buttons.
*
* @since 0.6
* @var string
*/
const STATUS_INPUT_NAME = 'amp_status';
/**
* The nonce name.
*
* @since 0.6
* @var string
*/
const NONCE_NAME = 'amp-status-nonce';
/**
* The nonce action.
*
* @since 0.6
* @var string
*/
const NONCE_ACTION = 'amp-update-status';
/**
* Initialize.
*
* @since 0.6
*/
public function init() {
register_meta(
'post',
self::STATUS_POST_META_KEY,
array(
'sanitize_callback' => array( $this, 'sanitize_status' ),
'type' => 'string',
'description' => __( 'AMP status.', 'amp' ),
'show_in_rest' => true,
'single' => true,
)
);
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_assets' ) );
add_action( 'post_submitbox_misc_actions', array( $this, 'render_status' ) );
add_action( 'save_post', array( $this, 'save_amp_status' ) );
add_filter( 'preview_post_link', array( $this, 'preview_post_link' ) );
}
/**
* Sanitize status.
*
* @param string $status Status.
* @return string Sanitized status. Empty string when invalid.
*/
public function sanitize_status( $status ) {
$status = strtolower( trim( $status ) );
if ( ! in_array( $status, array( self::ENABLED_STATUS, self::DISABLED_STATUS ), true ) ) {
/*
* In lieu of actual validation being available, clear the status entirely
* so that the underlying default status will be used instead.
* In the future it would be ideal if register_meta() accepted a
* validate_callback as well which the REST API could leverage.
*/
$status = '';
}
return $status;
}
/**
* Enqueue admin assets.
*
* @since 0.6
*/
public function enqueue_admin_assets() {
$post = get_post();
$screen = get_current_screen();
$validate = (
isset( $screen->base )
&&
'post' === $screen->base
&&
is_post_type_viewable( $post->post_type )
);
if ( ! $validate ) {
return;
}
wp_enqueue_style(
self::ASSETS_HANDLE,
amp_get_asset_url( 'css/amp-post-meta-box.css' ),
false,
AMP__VERSION
);
// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NotInFooter
wp_enqueue_script(
self::ASSETS_HANDLE,
amp_get_asset_url( 'js/amp-post-meta-box.js' ),
array( 'jquery' ),
AMP__VERSION
);
if ( current_theme_supports( AMP_Theme_Support::SLUG ) ) {
$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' => 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' ),
),
)
)
)
);
}
/**
* Enqueues block assets.
*
* @since 1.0
*/
public function enqueue_block_assets() {
$post = get_post();
if ( ! is_post_type_viewable( $post->post_type ) ) {
return;
}
wp_enqueue_script(
self::BLOCK_ASSET_HANDLE,
amp_get_asset_url( 'js/' . self::BLOCK_ASSET_HANDLE . '.js' ),
array( 'wp-hooks', 'wp-i18n', 'wp-components' ),
AMP__VERSION,
true
);
$status_and_errors = $this->get_status_and_errors( $post );
$enabled_status = $status_and_errors['status'];
$error_messages = $this->get_error_messages( $status_and_errors['status'], $status_and_errors['errors'] );
$script_data = array(
'possibleStati' => array( self::ENABLED_STATUS, self::DISABLED_STATUS ),
'defaultStatus' => $enabled_status,
'errorMessages' => $error_messages,
);
if ( function_exists( 'wp_set_script_translations' ) ) {
wp_set_script_translations( self::BLOCK_ASSET_HANDLE, 'amp' );
} elseif ( function_exists( 'wp_get_jed_locale_data' ) ) {
$script_data['i18n'] = wp_get_jed_locale_data( 'amp' );
} elseif ( function_exists( 'gutenberg_get_jed_locale_data' ) ) {
$script_data['i18n'] = gutenberg_get_jed_locale_data( 'amp' );
}
wp_add_inline_script(
self::BLOCK_ASSET_HANDLE,
sprintf( 'var wpAmpEditor = %s;', wp_json_encode( $script_data ) ),
'before'
);
}
/**
* Render AMP status.
*
* @since 0.6
* @param WP_Post $post Post.
*/
public function render_status( $post ) {
$verify = (
isset( $post->ID )
&&
is_post_type_viewable( $post->post_type )
&&
current_user_can( 'edit_post', $post->ID )
);
if ( true !== $verify ) {
return;
}
$status_and_errors = $this->get_status_and_errors( $post );
$status = $status_and_errors['status'];
$errors = $status_and_errors['errors'];
$error_messages = $this->get_error_messages( $status, $errors );
$labels = array(
'enabled' => __( 'Enabled', 'amp' ),
'disabled' => __( 'Disabled', 'amp' ),
);
// The preceding variables are used inside the following amp-status.php template.
include AMP__DIR__ . '/templates/admin/amp-status.php';
}
/**
* Gets the AMP enabled status and errors.
*
* @since 1.0
* @param WP_Post $post The post to check.
* @return array {
* The status and errors.
*
* @type string $status The AMP enabled status.
* @type string[] $errors AMP errors.
* }
*/
public function get_status_and_errors( $post ) {
/*
* 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_Theme_Support::SLUG ) ) {
$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.
}
return compact( 'status', 'errors' );
}
/**
* Gets the AMP enabled error message(s).
*
* @since 1.0
* @param string $status The AMP enabled status.
* @param array $errors The AMP enabled errors.
* @return array $error_messages The error messages, as an array of strings.
*/
public function get_error_messages( $status, $errors ) {
$error_messages = array();
if ( in_array( 'status_immutable', $errors, true ) ) {
if ( self::ENABLED_STATUS === $status ) {
$error_messages[] = __( 'Your site does not allow AMP to be disabled.', 'amp' );
} else {
$error_messages[] = __( 'Your site does not allow AMP to be enabled.', 'amp' );
}
}
if ( in_array( 'template_unsupported', $errors, true ) || in_array( 'no_matching_template', $errors, true ) ) {
$error_messages[] = sprintf(
/* translators: %s is a link to the AMP settings screen */
__( 'There are no <a href="%s">supported templates</a> to display this in AMP.', 'amp' ),
esc_url( admin_url( 'admin.php?page=' . AMP_Options_Manager::OPTION_NAME ) )
);
}
if ( in_array( 'password-protected', $errors, true ) ) {
$error_messages[] = __( 'AMP cannot be enabled on password protected posts.', 'amp' );
}
if ( in_array( 'post-type-support', $errors, true ) ) {
$error_messages[] = sprintf(
/* translators: %s is a link to the AMP settings screen */
__( 'AMP cannot be enabled because this <a href="%s">post type does not support it</a>.', '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' );
}
return $error_messages;
}
/**
* 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 ) {
update_post_meta(
$post_id,
self::STATUS_POST_META_KEY,
$_POST[ self::STATUS_INPUT_NAME ] // Note: The sanitize_callback has been supplied in the register_meta() call above.
);
}
}
/**
* 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'] ) // phpcs:ignore WordPress.Security.NonceVerification.Missing
&&
'do-preview' === sanitize_key( wp_unslash( $_POST['amp-preview'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Missing
);
if ( $is_amp ) {
$link = add_query_arg( amp_get_slug(), true, $link );
}
return $link;
}
}

View File

@ -0,0 +1,201 @@
<?php
/**
* Callbacks for adding AMP-related things to the admin.
*
* @package AMP
*/
/**
* Obsolete constant for flagging when Customizer is opened for AMP.
*
* @deprecated
* @var string
*/
define( 'AMP_CUSTOMIZER_QUERY_VAR', 'customize_amp' );
/**
* Sets up the AMP template editor for the Customizer.
*
* If this is in AMP canonical mode, exit.
* There's no need for the 'AMP' Customizer panel,
* And this does not need to toggle between the AMP and normal display.
*/
function amp_init_customizer() {
if ( amp_is_canonical() ) {
return;
}
// Fire up the AMP Customizer.
add_action( 'customize_register', array( 'AMP_Template_Customizer', 'init' ), 500 );
// Add some basic design settings + controls to the Customizer.
add_action( 'amp_init', array( 'AMP_Customizer_Design_Settings', 'init' ) );
// Add a link to the Customizer.
add_action( 'admin_menu', 'amp_add_customizer_link' );
}
/**
* Get permalink for the first AMP-eligible post.
*
* @return string|null URL on success, null if none found.
*/
function amp_admin_get_preview_permalink() {
/**
* Filter the post type to retrieve the latest for use in the AMP template customizer.
*
* @param string $post_type Post type slug. Default 'post'.
*/
$post_type = (string) apply_filters( 'amp_customizer_post_type', 'post' );
// Make sure the desired post type is actually supported, and if so, prefer it.
$supported_post_types = get_post_types_by_support( AMP_Post_Type_Support::SLUG );
if ( in_array( $post_type, $supported_post_types, true ) ) {
$supported_post_types = array_unique( array_merge( array( $post_type ), $supported_post_types ) );
}
// Bail if there are no supported post types.
if ( empty( $supported_post_types ) ) {
return null;
}
// If theme support is present, then bail if the singular template is not supported.
if ( current_theme_supports( AMP_Theme_Support::SLUG ) ) {
$supported_templates = AMP_Theme_Support::get_supportable_templates();
if ( empty( $supported_templates['is_singular']['supported'] ) ) {
return null;
}
}
$post_ids = get_posts(
array(
'no_found_rows' => true,
'suppress_filters' => false,
'post_status' => 'publish',
'post_password' => '',
'post_type' => $supported_post_types,
'posts_per_page' => 1,
'fields' => 'ids',
// @todo This should eventually do a meta_query to make sure there are none that have AMP_Post_Meta_Box::STATUS_POST_META_KEY = DISABLED_STATUS.
)
);
if ( empty( $post_ids ) ) {
return false;
}
$post_id = $post_ids[0];
return amp_get_permalink( $post_id );
}
/**
* Registers a submenu page to access the AMP template editor panel in the Customizer.
*/
function amp_add_customizer_link() {
/** This filter is documented in includes/settings/class-amp-customizer-design-settings.php */
if ( ! apply_filters( 'amp_customizer_is_enabled', true ) || current_theme_supports( AMP_Theme_Support::SLUG ) ) {
return;
}
$menu_slug = add_query_arg(
array(
'autofocus[panel]' => AMP_Template_Customizer::PANEL_ID,
'url' => rawurlencode( amp_admin_get_preview_permalink() ),
'return' => rawurlencode( admin_url() ),
),
'customize.php'
);
// Add the theme page.
add_theme_page(
__( 'AMP', 'amp' ),
__( 'AMP', 'amp' ),
'edit_theme_options',
$menu_slug
);
}
/**
* Registers AMP settings.
*/
function amp_add_options_menu() {
if ( ! is_admin() ) {
return;
}
/**
* Filter whether to enable the AMP settings.
*
* @since 0.5
* @param bool $enable Whether to enable the AMP settings. Default true.
*/
$short_circuit = apply_filters( 'amp_options_menu_is_enabled', true );
if ( true !== $short_circuit ) {
return;
}
$amp_options = new AMP_Options_Menu();
$amp_options->init();
}
/**
* Add custom analytics.
*
* This is currently only used for legacy AMP post templates.
*
* @since 0.5
* @see amp_get_analytics()
*
* @param array $analytics Analytics.
* @return array Analytics.
*/
function amp_add_custom_analytics( $analytics = array() ) {
$analytics = amp_get_analytics( $analytics );
/**
* Add amp-analytics tags.
*
* This filter allows you to easily insert any amp-analytics tags without needing much heavy lifting.
* This filter should be used to alter entries for legacy AMP templates.
*
* @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 WP_Post $post The current post.
*/
$analytics = apply_filters( 'amp_post_template_analytics', $analytics, get_queried_object() );
return $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();
}
/**
* Bootstrap AMP Editor core blocks.
*/
function amp_editor_core_blocks() {
$editor_blocks = new AMP_Editor_Blocks();
$editor_blocks->init();
}
/**
* Bootstrap the AMP admin pointer class.
*
* @since 1.0
*/
function amp_admin_pointer() {
$admin_pointer = new AMP_Admin_Pointer();
$admin_pointer->init();
}

View File

@ -0,0 +1,33 @@
<?php
/**
* Callbacks for adding AMP-related things to the main theme.
*
* @deprecated Function in this file has been moved to amp-helper-functions.php.
* @package AMP
*/
_deprecated_file(
__FILE__,
'1.0',
null,
sprintf(
/* translators: 1: amp_add_amphtml_link(). 2: amp-helper-functions.php */
esc_html__( 'Use %1$s function which is already included from %2$s', 'amp' ),
'amp_add_amphtml_link()',
'amp-helper-functions.php'
)
);
/**
* Add amphtml link to frontend.
*
* @deprecated
*
* @since 0.2
* @since 1.0 Deprecated
* @see amp_add_amphtml_link()
*/
function amp_frontend_add_canonical() {
_deprecated_function( __FUNCTION__, '1.0', 'amp_add_amphtml_link' );
amp_add_amphtml_link();
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,142 @@
<?php
/**
* Callbacks for adding content to an AMP template.
*
* @package AMP
*/
/**
* Register hooks.
*/
function amp_post_template_init_hooks() {
add_action( 'amp_post_template_head', 'amp_post_template_add_title' );
add_action( 'amp_post_template_head', 'amp_post_template_add_canonical' );
add_action( 'amp_post_template_head', 'amp_post_template_add_scripts' );
add_action( 'amp_post_template_head', 'amp_post_template_add_fonts' );
add_action( 'amp_post_template_head', 'amp_post_template_add_boilerplate_css' );
add_action( 'amp_post_template_head', 'amp_print_schemaorg_metadata' );
add_action( 'amp_post_template_head', 'amp_add_generator_metadata' );
add_action( 'amp_post_template_head', 'wp_generator' );
add_action( 'amp_post_template_css', 'amp_post_template_add_styles', 99 );
add_action( 'amp_post_template_data', 'amp_post_template_add_analytics_script' );
add_action( 'amp_post_template_footer', 'amp_post_template_add_analytics_data' );
}
/**
* Add title.
*
* @param AMP_Post_Template $amp_template template.
*/
function amp_post_template_add_title( $amp_template ) {
?>
<title><?php echo esc_html( $amp_template->get( 'document_title' ) ); ?></title>
<?php
}
/**
* Add canonical link.
*
* @param AMP_Post_Template $amp_template Template.
*/
function amp_post_template_add_canonical( $amp_template ) {
?>
<link rel="canonical" href="<?php echo esc_url( $amp_template->get( 'canonical_url' ) ); ?>" />
<?php
}
/**
* Print scripts.
*
* @see amp_register_default_scripts()
* @see amp_filter_script_loader_tag()
* @param AMP_Post_Template $amp_template Template.
*/
function amp_post_template_add_scripts( $amp_template ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo amp_render_scripts(
array_merge(
array(
// Just in case the runtime has been overridden by amp_post_template_data filter.
'amp-runtime' => $amp_template->get( 'amp_runtime_script' ),
),
$amp_template->get( 'amp_component_scripts', array() )
)
);
}
/**
* Print fonts.
*
* @param AMP_Post_Template $amp_template Template.
*/
function amp_post_template_add_fonts( $amp_template ) {
$font_urls = $amp_template->get( 'font_urls', array() );
foreach ( $font_urls as $slug => $url ) {
printf( '<link rel="stylesheet" href="%s">', esc_url( esc_url( $url ) ) ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
}
}
/**
* Print boilerplate CSS.
*
* @since 0.3
* @see amp_get_boilerplate_code()
*/
function amp_post_template_add_boilerplate_css() {
echo amp_get_boilerplate_code(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Print Schema.org metadata.
*
* @deprecated Since 0.7
*/
function amp_post_template_add_schemaorg_metadata() {
_deprecated_function( __FUNCTION__, '0.7', 'amp_print_schemaorg_metadata' );
amp_print_schemaorg_metadata();
}
/**
* Print styles.
*
* @param AMP_Post_Template $amp_template Template.
*/
function amp_post_template_add_styles( $amp_template ) {
$stylesheets = $amp_template->get( 'post_amp_stylesheets' );
if ( ! empty( $stylesheets ) ) {
echo '/* Inline stylesheets */' . PHP_EOL; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo implode( '', $stylesheets ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
$styles = $amp_template->get( 'post_amp_styles' );
if ( ! empty( $styles ) ) {
echo '/* Inline styles */' . PHP_EOL; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
foreach ( $styles as $selector => $declarations ) {
$declarations = implode( ';', $declarations ) . ';';
printf( '%1$s{%2$s}', $selector, $declarations ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
}
}
/**
* Add analytics scripts.
*
* @param array $data Data.
* @return array Data.
*/
function amp_post_template_add_analytics_script( $data ) {
if ( ! empty( $data['amp_analytics'] ) ) {
$data['amp_component_scripts']['amp-analytics'] = 'https://cdn.ampproject.org/v0/amp-analytics-0.1.js';
}
return $data;
}
/**
* Print analytics data.
*
* @since 0.3.2
*/
function amp_post_template_add_analytics_data() {
$analytics = amp_add_custom_analytics();
amp_print_analytics( $analytics );
}

View File

@ -0,0 +1,163 @@
<?php
/**
* Class AMP_Autoloader
*
* @package AMP
*/
/**
* Autoload the classes used by the AMP plugin.
*
* Class AMP_Autoloader
*/
class AMP_Autoloader {
/**
* Map of Classname to relative filepath sans extension.
*
* @note We omitted the leading slash and the .php extension from each
* relative filepath because they are redundant and to include
* them would take up unnecessary bytes of memory at runtime.
*
* @example Format (note no leading / and no .php extension):
*
* array(
* 'Class_Name1' => 'subdir-of-includes/filename1',
* 'Class_Name2' => '2nd-subdir-of-includes/filename2',
* );
*
* @var string[]
*/
private static $classmap = array(
'AMP_Editor_Blocks' => 'includes/admin/class-amp-editor-blocks',
'AMP_Theme_Support' => 'includes/class-amp-theme-support',
'AMP_Service_Worker' => 'includes/class-amp-service-worker',
'AMP_HTTP' => 'includes/class-amp-http',
'AMP_Comment_Walker' => 'includes/class-amp-comment-walker',
'AMP_Template_Customizer' => 'includes/admin/class-amp-customizer',
'AMP_Post_Meta_Box' => 'includes/admin/class-amp-post-meta-box',
'AMP_Admin_Pointer' => 'includes/admin/class-amp-admin-pointer',
'AMP_Post_Type_Support' => 'includes/class-amp-post-type-support',
'AMP_Base_Embed_Handler' => 'includes/embeds/class-amp-base-embed-handler',
'AMP_DailyMotion_Embed_Handler' => 'includes/embeds/class-amp-dailymotion-embed',
'AMP_Facebook_Embed_Handler' => 'includes/embeds/class-amp-facebook-embed',
'AMP_Gallery_Embed_Handler' => 'includes/embeds/class-amp-gallery-embed',
'AMP_Gfycat_Embed_Handler' => 'includes/embeds/class-amp-gfycat-embed-handler',
'AMP_Hulu_Embed_Handler' => 'includes/embeds/class-amp-hulu-embed-handler',
'AMP_Imgur_Embed_Handler' => 'includes/embeds/class-amp-imgur-embed-handler',
'AMP_Core_Block_Handler' => 'includes/embeds/class-amp-core-block-handler',
'AMP_Instagram_Embed_Handler' => 'includes/embeds/class-amp-instagram-embed',
'AMP_Issuu_Embed_Handler' => 'includes/embeds/class-amp-issuu-embed-handler',
'AMP_Meetup_Embed_Handler' => 'includes/embeds/class-amp-meetup-embed-handler',
'AMP_Pinterest_Embed_Handler' => 'includes/embeds/class-amp-pinterest-embed',
'AMP_Playlist_Embed_Handler' => 'includes/embeds/class-amp-playlist-embed-handler',
'AMP_Reddit_Embed_Handler' => 'includes/embeds/class-amp-reddit-embed-handler',
'AMP_SoundCloud_Embed_Handler' => 'includes/embeds/class-amp-soundcloud-embed',
'AMP_Tumblr_Embed_Handler' => 'includes/embeds/class-amp-tumblr-embed-handler',
'AMP_Twitter_Embed_Handler' => 'includes/embeds/class-amp-twitter-embed',
'AMP_Vimeo_Embed_Handler' => 'includes/embeds/class-amp-vimeo-embed',
'AMP_Vine_Embed_Handler' => 'includes/embeds/class-amp-vine-embed',
'AMP_YouTube_Embed_Handler' => 'includes/embeds/class-amp-youtube-embed',
'AMP_Analytics_Options_Submenu' => 'includes/options/class-amp-analytics-options-submenu',
'AMP_Options_Menu' => 'includes/options/class-amp-options-menu',
'AMP_Options_Manager' => 'includes/options/class-amp-options-manager',
'AMP_Analytics_Options_Submenu_Page' => 'includes/options/views/class-amp-analytics-options-submenu-page',
'AMP_Options_Menu_Page' => 'includes/options/views/class-amp-options-menu-page',
'AMP_Rule_Spec' => 'includes/sanitizers/class-amp-rule-spec',
'AMP_Allowed_Tags_Generated' => 'includes/sanitizers/class-amp-allowed-tags-generated',
'AMP_Audio_Sanitizer' => 'includes/sanitizers/class-amp-audio-sanitizer',
'AMP_Base_Sanitizer' => 'includes/sanitizers/class-amp-base-sanitizer',
'AMP_Blacklist_Sanitizer' => 'includes/sanitizers/class-amp-blacklist-sanitizer',
'AMP_Block_Sanitizer' => 'includes/sanitizers/class-amp-block-sanitizer',
'AMP_Gallery_Block_Sanitizer' => 'includes/sanitizers/class-amp-gallery-block-sanitizer',
'AMP_Iframe_Sanitizer' => 'includes/sanitizers/class-amp-iframe-sanitizer',
'AMP_Img_Sanitizer' => 'includes/sanitizers/class-amp-img-sanitizer',
'AMP_Nav_Menu_Toggle_Sanitizer' => 'includes/sanitizers/class-amp-nav-menu-toggle-sanitizer',
'AMP_Nav_Menu_Dropdown_Sanitizer' => 'includes/sanitizers/class-amp-nav-menu-dropdown-sanitizer',
'AMP_Comments_Sanitizer' => 'includes/sanitizers/class-amp-comments-sanitizer',
'AMP_Form_Sanitizer' => 'includes/sanitizers/class-amp-form-sanitizer',
'AMP_O2_Player_Sanitizer' => 'includes/sanitizers/class-amp-o2-player-sanitizer',
'AMP_Playbuzz_Sanitizer' => 'includes/sanitizers/class-amp-playbuzz-sanitizer',
'AMP_Style_Sanitizer' => 'includes/sanitizers/class-amp-style-sanitizer',
'AMP_Script_Sanitizer' => 'includes/sanitizers/class-amp-script-sanitizer',
'AMP_Embed_Sanitizer' => 'includes/sanitizers/class-amp-embed-sanitizer',
'AMP_Tag_And_Attribute_Sanitizer' => 'includes/sanitizers/class-amp-tag-and-attribute-sanitizer',
'AMP_Video_Sanitizer' => 'includes/sanitizers/class-amp-video-sanitizer',
'AMP_Core_Theme_Sanitizer' => 'includes/sanitizers/class-amp-core-theme-sanitizer',
'AMP_Noscript_Fallback' => 'includes/sanitizers/trait-amp-noscript-fallback',
'AMP_Customizer_Design_Settings' => 'includes/settings/class-amp-customizer-design-settings',
'AMP_Customizer_Settings' => 'includes/settings/class-amp-customizer-settings',
'AMP_Content' => 'includes/templates/class-amp-content',
'AMP_Content_Sanitizer' => 'includes/templates/class-amp-content-sanitizer',
'AMP_Post_Template' => 'includes/templates/class-amp-post-template',
'AMP_DOM_Utils' => 'includes/utils/class-amp-dom-utils',
'AMP_HTML_Utils' => 'includes/utils/class-amp-html-utils',
'AMP_Image_Dimension_Extractor' => 'includes/utils/class-amp-image-dimension-extractor',
'AMP_Validation_Manager' => 'includes/validation/class-amp-validation-manager',
'AMP_Validated_URL_Post_Type' => 'includes/validation/class-amp-validated-url-post-type',
'AMP_Validation_Error_Taxonomy' => 'includes/validation/class-amp-validation-error-taxonomy',
'AMP_CLI' => 'includes/class-amp-cli',
'AMP_String_Utils' => 'includes/utils/class-amp-string-utils',
'AMP_WP_Utils' => 'includes/utils/class-amp-wp-utils',
'AMP_Widget_Archives' => 'includes/widgets/class-amp-widget-archives',
'AMP_Widget_Categories' => 'includes/widgets/class-amp-widget-categories',
'AMP_Widget_Text' => 'includes/widgets/class-amp-widget-text',
'WPCOM_AMP_Polldaddy_Embed' => 'wpcom/class-amp-polldaddy-embed',
'AMP_Test_Stub_Sanitizer' => 'tests/stubs',
'AMP_Test_World_Sanitizer' => 'tests/stubs',
);
/**
* Is registered.
*
* @var bool
*/
public static $is_registered = false;
/**
* Perform the autoload on demand when requested by PHP runtime.
*
* Design Goal: Execute as few lines of code as possible each call.
*
* @since 0.6
*
* @param string $class_name Class name.
*/
protected static function autoload( $class_name ) {
if ( ! isset( self::$classmap[ $class_name ] ) ) {
return;
}
$filepath = self::$classmap[ $class_name ];
require AMP__DIR__ . "/{$filepath}.php";
}
/**
* Registers this autoloader to PHP.
*
* @since 0.6
*
* Called at the end of this file; calling a second time has no effect.
*/
public static function register() {
if ( file_exists( AMP__DIR__ . '/vendor/autoload.php' ) ) {
require_once AMP__DIR__ . '/vendor/autoload.php';
}
if ( ! self::$is_registered ) {
spl_autoload_register( array( __CLASS__, 'autoload' ) );
self::$is_registered = true;
}
}
/**
* Allows an extensions plugin to register a class and its file for autoloading
*
* @since 0.6
*
* @param string $class_name Full classname (include namespace if applicable).
* @param string $filepath Absolute filepath to class file, including .php extension.
*/
public static function register_autoload_class( $class_name, $filepath ) {
self::$classmap[ $class_name ] = '!' . $filepath;
}
}

View File

@ -0,0 +1,675 @@
<?php
/**
* Class AMP_CLI
*
* @package AMP
*/
/**
* Class AMP_CLI
*
* Registers and implements a WP-CLI command to crawl the entire site for AMP validity.
* To use this, run: wp amp validate-site.
*
* @since 1.0
*/
class AMP_CLI {
/**
* The WP-CLI flag to force validation.
*
* By default, the WP-CLI command does not validate templates that the user has opted-out of.
* For example, by unchecking 'Categories' in 'AMP Settings' > 'Supported Templates'.
* But with this flag, validation will ignore these options.
*
* @var string
*/
const FLAG_NAME_FORCE_VALIDATION = 'force';
/**
* The WP-CLI argument to validate based on certain conditionals
*
* For example, --include=is_tag,is_author
* Normally, this script will validate all of the templates that don't have AMP disabled.
* But this allows validating only specific templates.
*
* @var string
*/
const INCLUDE_ARGUMENT = 'include';
/**
* The WP-CLI argument for the maximum URLs to validate for each type.
*
* If this is passed in the command,
* it overrides the value of self::$maximum_urls_to_validate_for_each_type.
*
* @var string
*/
const LIMIT_URLS_ARGUMENT = 'limit';
/**
* The WP CLI progress bar.
*
* @see https://make.wordpress.org/cli/handbook/internal-api/wp-cli-utils-make-progress-bar/
* @var \cli\progress\Bar|\WP_CLI\NoOp
*/
public static $wp_cli_progress;
/**
* The total number of validation errors, regardless of whether they were accepted.
*
* @var int
*/
public static $total_errors = 0;
/**
* The total number of unaccepted validation errors.
*
* If an error has been accepted in the /wp-admin validation UI,
* it won't count toward this.
*
* @var int
*/
public static $unaccepted_errors = 0;
/**
* The number of URLs crawled, regardless of whether they have validation errors.
*
* @var int
*/
public static $number_crawled = 0;
/**
* Whether to force crawling of URLs.
*
* By default, this script only crawls URLs that support AMP,
* where the user has not opted-out of AMP for the URL.
* For example, by un-checking 'Posts' in 'AMP Settings' > 'Supported Templates'.
* Or un-checking 'Enable AMP' in the post's editor.
*
* @var int
*/
public static $force_crawl_urls = false;
/**
* A whitelist of conditionals to use for validation.
*
* Usually, this script will validate all of the templates that don't have AMP disabled.
* But this allows validating based on only these conditionals.
* This is set if the WP-CLI command has an --include argument.
*
* @var array
*/
public static $include_conditionals = array();
/**
* The maximum number of URLs to validate for each type.
*
* Templates are each a separate type, like those for is_category() and is_tag().
* Also, each post type is a separate type.
* This value is overridden if the WP-CLI command has an --limit argument, like --limit=10.
*
* @var int
*/
public static $limit_type_validate_count;
/**
* The validation counts by type, like template or post type.
*
* @var array[][] {
* Validity by type.
*
* @type int $valid The number of valid URLs for this type.
* @type int $total The total number of URLs for this type, valid or invalid.
* }
*/
public static $validity_by_type = array();
/**
* Crawl the entire site to get AMP validation results.
*
* ## OPTIONS
*
* [--limit=<count>]
* : The maximum number of URLs to validate for each template and content type.
* ---
* default: 100
* ---
*
* [--include=<templates>]
* : Only validates a URL if one of the conditionals is true.
*
* [--force]
* : Force validation of URLs even if their associated templates or object types do not have AMP enabled.
*
* ## EXAMPLES
*
* wp amp validate-site --include=is_author,is_tag
*
* @subcommand validate-site
* @param array $args Positional args.
* @param array $assoc_args Associative args.
* @throws Exception If an error happens.
*/
public function validate_site( $args, $assoc_args ) {
unset( $args );
self::$include_conditionals = array();
self::$force_crawl_urls = false;
self::$limit_type_validate_count = (int) $assoc_args[ self::LIMIT_URLS_ARGUMENT ];
/*
* Handle the argument and flag passed to the command: --include and --force.
* If the self::INCLUDE_ARGUMENT is present, force crawling or URLs.
* The WP-CLI command should indicate which templates are crawled, not the /wp-admin options.
*/
if ( ! empty( $assoc_args[ self::INCLUDE_ARGUMENT ] ) ) {
self::$include_conditionals = explode( ',', $assoc_args[ self::INCLUDE_ARGUMENT ] );
self::$force_crawl_urls = true;
} elseif ( isset( $assoc_args[ self::FLAG_NAME_FORCE_VALIDATION ] ) ) {
self::$force_crawl_urls = true;
}
if ( ! current_theme_supports( AMP_Theme_Support::SLUG ) ) {
if ( self::$force_crawl_urls ) {
/*
* There is no theme support added programmatically or via options.
* So make sure that theme support is present so that AMP_Validation_Manager::validate_url()
* will use a canonical URL as the basis for obtaining validation results.
*/
add_theme_support( AMP_Theme_Support::SLUG );
} else {
WP_CLI::error(
sprintf(
'Your templates are currently in Reader mode, which does not allow crawling the site. Please pass the --%s flag in order to force crawling for validation.',
self::FLAG_NAME_FORCE_VALIDATION
)
);
}
}
$number_urls_to_crawl = self::count_urls_to_validate();
if ( ! $number_urls_to_crawl ) {
if ( ! empty( self::$include_conditionals ) ) {
WP_CLI::error(
sprintf(
'The templates passed via the --%s argument did not match any URLs. You might try passing different templates to it.',
self::INCLUDE_ARGUMENT
)
);
} else {
WP_CLI::error(
sprintf(
'All of your templates might be unchecked in AMP Settings > Supported Templates. You might pass --%s to this command.',
self::FLAG_NAME_FORCE_VALIDATION
)
);
}
}
WP_CLI::log( 'Crawling the site for AMP validity.' );
self::$wp_cli_progress = WP_CLI\Utils\make_progress_bar(
sprintf( 'Validating %d URLs...', $number_urls_to_crawl ),
$number_urls_to_crawl
);
self::crawl_site();
self::$wp_cli_progress->finish();
$key_template_type = 'Template or content type';
$key_url_count = 'URL Count';
$key_validity_rate = 'Validity Rate';
$table_validation_by_type = array();
foreach ( self::$validity_by_type as $type_name => $validity ) {
$table_validation_by_type[] = array(
$key_template_type => $type_name,
$key_url_count => $validity['total'],
$key_validity_rate => sprintf( '%d%%', 100.0 * ( $validity['valid'] / $validity['total'] ) ),
);
}
if ( empty( $table_validation_by_type ) ) {
WP_CLI::error( 'No validation results were obtained from the URLs.' );
return;
}
WP_CLI::success(
sprintf(
'%3$d crawled URLs have unaccepted issue(s) out of %2$d total with AMP validation issue(s); %1$d URLs were crawled.',
self::$number_crawled,
self::$total_errors,
self::$unaccepted_errors
)
);
// Output a table of validity by template/content type.
WP_CLI\Utils\format_items(
'table',
$table_validation_by_type,
array( $key_template_type, $key_url_count, $key_validity_rate )
);
$url_more_details = add_query_arg(
'post_type',
AMP_Validated_URL_Post_Type::POST_TYPE_SLUG,
admin_url( 'edit.php' )
);
WP_CLI::line( sprintf( 'For more details, please see: %s', $url_more_details ) );
}
/**
* Reset all validation data on a site.
*
* This deletes all amp_validated_url posts and all amp_validation_error terms.
*
* ## OPTIONS
*
* [--yes]
* : Proceed to empty the site validation data without a confirmation prompt.
*
* ## EXAMPLES
*
* wp amp reset-site-validation --yes
*
* @subcommand reset-site-validation
* @param array $args Positional args. Unused.
* @param array $assoc_args Associative args.
* @throws Exception If an error happens.
*/
public function reset_site_validation( $args, $assoc_args ) {
unset( $args );
global $wpdb;
WP_CLI::confirm( 'Are you sure you want to empty all amp_validated_url posts and amp_validation_error taxonomy terms?', $assoc_args );
// Delete all posts.
$count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->posts WHERE post_type = %s", AMP_Validated_URL_Post_Type::POST_TYPE_SLUG ) );
$query = $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type = %s", AMP_Validated_URL_Post_Type::POST_TYPE_SLUG );
$posts = new WP_CLI\Iterators\Query( $query, 10000 );
$progress = WP_CLI\Utils\make_progress_bar(
sprintf( 'Deleting %d amp_validated_url posts...', $count ),
$count
);
while ( $posts->valid() ) {
$post_id = $posts->current()->ID;
$posts->next();
wp_delete_post( $post_id, true );
$progress->tick();
}
$progress->finish();
// Delete all terms. Note that many terms should get deleted when their post counts go to zero above.
$count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( * ) FROM $wpdb->term_taxonomy WHERE taxonomy = %s", AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG ) );
$query = $wpdb->prepare( "SELECT term_id FROM $wpdb->term_taxonomy WHERE taxonomy = %s", AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG );
$terms = new WP_CLI\Iterators\Query( $query, 10000 );
$progress = WP_CLI\Utils\make_progress_bar(
sprintf( 'Deleting %d amp_taxonomy_error terms...', $count ),
$count
);
while ( $terms->valid() ) {
$term_id = $terms->current()->term_id;
$terms->next();
wp_delete_term( $term_id, AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG );
$progress->tick();
}
$progress->finish();
WP_CLI::success( 'All AMP validation data has been removed.' );
}
/**
* Gets the total number of URLs to validate.
*
* By default, this only counts AMP-enabled posts and terms.
* But if $force_crawl_urls is true, it counts all of them, regardless of their AMP status.
* It also uses self::$maximum_urls_to_validate_for_each_type,
* which can be overridden with a command line argument.
*
* @return int The number of URLs to validate.
*/
public static function count_urls_to_validate() {
/*
* If the homepage is set to 'Your latest posts,' start the $total_count at 1.
* Otherwise, it will probably be counted in the query for pages below.
*/
$total_count = 'posts' === get_option( 'show_on_front' ) && self::is_template_supported( 'is_home' ) ? 1 : 0;
$amp_enabled_taxonomies = array_filter(
get_taxonomies( array( 'public' => true ) ),
array( 'AMP_CLI', 'does_taxonomy_support_amp' )
);
// Count all public taxonomy terms.
foreach ( $amp_enabled_taxonomies as $taxonomy ) {
$term_query = new WP_Term_Query(
array(
'taxonomy' => $taxonomy,
'fields' => 'ids',
'number' => self::$limit_type_validate_count,
)
);
// If $term_query->terms is an empty array, passing it to count() will throw an error.
$total_count += ! empty( $term_query->terms ) ? count( $term_query->terms ) : 0;
}
// Count posts by type, like post, page, attachment, etc.
$public_post_types = get_post_types( array( 'public' => true ), 'names' );
foreach ( $public_post_types as $post_type ) {
$posts = self::get_posts_that_support_amp( self::get_posts_by_type( $post_type ) );
$total_count += ! empty( $posts ) ? count( $posts ) : 0;
}
// Count author pages, like https://example.com/author/admin/.
$total_count += count( self::get_author_page_urls() );
// Count a single example date page, like https://example.com/?year=2019.
if ( self::get_date_page() ) {
$total_count++;
}
// Count a single example search page, like https://example.com/?s=example.
if ( self::get_search_page() ) {
$total_count++;
}
return $total_count;
}
/**
* Gets the posts IDs that support AMP.
*
* By default, this only gets the post IDs if they support AMP.
* This means that 'Posts' isn't deselected in 'AMP Settings' > 'Supported Templates'.
* And 'Enable AMP' isn't unchecked in the post's editor.
* But if $force_crawl_urls is true, this simply returns all of the IDs.
*
* @param array $ids THe post IDs to check for AMP support.
* @return array The post IDs that support AMP, or an empty array.
*/
public static function get_posts_that_support_amp( $ids ) {
if ( ! self::is_template_supported( 'is_singular' ) ) {
return array();
}
if ( self::$force_crawl_urls ) {
return $ids;
}
return array_filter(
$ids,
function( $id ) {
return post_supports_amp( $id );
}
);
}
/**
* Gets whether the taxonomy supports AMP.
*
* This only gets the term IDs if they support AMP.
* If their taxonomy is unchecked in 'AMP Settings' > 'Supported Templates,' this does not return them.
* For example, if 'Categories' is unchecked.
* This can be overridden by passing the self::FLAG_NAME_FORCE_VALIDATION argument to the WP-CLI command.
*
* @param string $taxonomy The taxonomy.
* @return boolean Whether the taxonomy supports AMP.
*/
public static function does_taxonomy_support_amp( $taxonomy ) {
if ( 'post_tag' === $taxonomy ) {
$taxonomy = 'tag';
}
$taxonomy_key = 'is_' . $taxonomy;
$custom_taxonomy_key = sprintf( 'is_tax[%s]', $taxonomy );
return self::is_template_supported( $taxonomy_key ) || self::is_template_supported( $custom_taxonomy_key );
}
/**
* Gets whether the template is supported.
*
* If the user has passed an include argument to the WP-CLI command, use that to find if this template supports AMP.
* For example, wp amp validate-site --include=is_tag,is_category
* would return true only if is_tag() or is_category().
* But passing the self::FLAG_NAME_FORCE_VALIDATION argument to the WP-CLI command overrides this.
*
* @param string $template The template to check.
* @return bool Whether the template is supported.
*/
public static function is_template_supported( $template ) {
// If the --include argument is present in the WP-CLI command, this template conditional must be present in it.
if ( ! empty( self::$include_conditionals ) ) {
return in_array( $template, self::$include_conditionals, true );
}
if ( self::$force_crawl_urls ) {
return true;
}
$supportable_templates = AMP_Theme_Support::get_supportable_templates();
// Check whether this taxonomy's template is supported, including in the 'AMP Settings' > 'Supported Templates' UI.
return ! empty( $supportable_templates[ $template ]['supported'] );
}
/**
* Gets the IDs of public, published posts.
*
* @param string $post_type The post type.
* @param int|null $offset The offset of the query (optional).
* @param int|null $number The number of posts to query for (optional).
* @return int[] $post_ids The post IDs in an array.
*/
public static function get_posts_by_type( $post_type, $offset = null, $number = null ) {
$args = array(
'post_type' => $post_type,
'posts_per_page' => is_int( $number ) ? $number : self::$limit_type_validate_count,
'post_status' => 'publish',
'orderby' => 'ID',
'order' => 'DESC',
'fields' => 'ids',
);
if ( is_int( $offset ) ) {
$args['offset'] = $offset;
}
// Attachment posts usually have the post_status of 'inherit,' so they can use the status of the post they're attached to.
if ( 'attachment' === $post_type ) {
$args['post_status'] = 'inherit';
}
$query = new WP_Query( $args );
return $query->posts;
}
/**
* Gets the front-end links for taxonomy terms.
* For example, https://example.org/?cat=2
*
* @param string $taxonomy The name of the taxonomy, like 'category' or 'post_tag'.
* @param int|string $offset The number at which to offset the query (optional).
* @param int $number The maximum amount of links to get (optional).
* @return string[] The term links, as an array of strings.
*/
public static function get_taxonomy_links( $taxonomy, $offset = '', $number = 1 ) {
return array_map(
'get_term_link',
get_terms(
array_merge(
compact( 'taxonomy', 'offset', 'number' ),
array(
'orderby' => 'id',
)
)
)
);
}
/**
* Gets the author page URLs, like https://example.com/author/admin/.
*
* Accepts an $offset parameter, for the query of authors.
* 0 is the first author in the query, and 1 is the second.
*
* @param int|string $offset The offset for the URL to query for, should be an int if passing an argument.
* @param int|string $number The total number to query for, should be an int if passing an argument.
* @return array The author page URLs, or an empty array.
*/
public static function get_author_page_urls( $offset = '', $number = '' ) {
$author_page_urls = array();
if ( ! self::is_template_supported( 'is_author' ) ) {
return $author_page_urls;
}
$number = ! empty( $number ) ? $number : self::$limit_type_validate_count;
foreach ( get_users( compact( 'offset', 'number' ) ) as $author ) {
$author_page_urls[] = get_author_posts_url( $author->ID, $author->user_nicename );
}
return $author_page_urls;
}
/**
* Gets a single search page URL, like https://example.com/?s=example.
*
* @return string|null An example search page, or null.
*/
public static function get_search_page() {
if ( ! self::is_template_supported( 'is_search' ) ) {
return null;
}
return add_query_arg( 's', 'example', home_url( '/' ) );
}
/**
* Gets a single date page URL, like https://example.com/?year=2018.
*
* @return string|null An example search page, or null.
*/
public static function get_date_page() {
if ( ! self::is_template_supported( 'is_date' ) ) {
return null;
}
return add_query_arg( 'year', date( 'Y' ), home_url( '/' ) );
}
/**
* Validates the URLs of the entire site.
*
* Includes the URLs of public, published posts, public taxonomies, and other templates.
* This validates one of each type at a time,
* and iterates until it reaches the maximum number of URLs for each type.
*/
public static function crawl_site() {
/*
* If 'Your homepage displays' is set to 'Your latest posts', validate the homepage.
* It will not be part of the page validation below.
*/
if ( 'posts' === get_option( 'show_on_front' ) && self::is_template_supported( 'is_home' ) ) {
self::validate_and_store_url( home_url( '/' ), 'home' );
}
$amp_enabled_taxonomies = array_filter(
get_taxonomies( array( 'public' => true ) ),
array( 'AMP_CLI', 'does_taxonomy_support_amp' )
);
$public_post_types = get_post_types( array( 'public' => true ), 'names' );
// Validate one URL of each template/content type, then another URL of each type on the next iteration.
for ( $i = 0; $i < self::$limit_type_validate_count; $i++ ) {
// Validate all public, published posts.
foreach ( $public_post_types as $post_type ) {
$post_ids = self::get_posts_that_support_amp( self::get_posts_by_type( $post_type, $i, 1 ) );
if ( ! empty( $post_ids[0] ) ) {
self::validate_and_store_url( get_permalink( $post_ids[0] ), $post_type );
}
}
foreach ( $amp_enabled_taxonomies as $taxonomy ) {
$taxonomy_links = self::get_taxonomy_links( $taxonomy, $i, 1 );
$link = reset( $taxonomy_links );
if ( ! empty( $link ) ) {
self::validate_and_store_url( $link, $taxonomy );
}
}
$author_page_urls = self::get_author_page_urls( $i, 1 );
if ( ! empty( $author_page_urls[0] ) ) {
self::validate_and_store_url( $author_page_urls[0], 'author' );
}
}
// Only validate 1 date and 1 search page.
$url = self::get_date_page();
if ( $url ) {
self::validate_and_store_url( $url, 'date' );
}
$url = self::get_search_page();
if ( $url ) {
self::validate_and_store_url( $url, 'search' );
}
}
/**
* Validates the URL, stores the results, and increments the counts.
*
* @param string $url The URL to validate.
* @param string $type The type of template, post, or taxonomy.
*/
public static function validate_and_store_url( $url, $type ) {
$validity = AMP_Validation_Manager::validate_url( $url );
/*
* If the request to validate this returns a WP_Error, return.
* One cause of an error is if the validation request results in a 404 response code.
*/
if ( is_wp_error( $validity ) ) {
WP_CLI::warning( sprintf( 'Validate URL error (%1$s): %2$s URL: %3$s', $validity->get_error_code(), $validity->get_error_message(), $url ) );
return;
}
if ( self::$wp_cli_progress ) {
self::$wp_cli_progress->tick();
}
$validation_errors = wp_list_pluck( $validity['results'], 'error' );
AMP_Validated_URL_Post_Type::store_validation_errors(
$validation_errors,
$validity['url'],
wp_array_slice_assoc( $validity, array( 'queried_object' ) )
);
$unaccepted_error_count = count(
array_filter(
$validation_errors,
function( $error ) {
$validation_status = AMP_Validation_Error_Taxonomy::get_validation_error_sanitization( $error );
return (
AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACK_ACCEPTED_STATUS !== $validation_status['term_status']
&&
AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_ACCEPTED_STATUS !== $validation_status['term_status']
);
}
)
);
if ( count( $validation_errors ) > 0 ) {
self::$total_errors++;
}
if ( $unaccepted_error_count > 0 ) {
self::$unaccepted_errors++;
}
self::$number_crawled++;
if ( ! isset( self::$validity_by_type[ $type ] ) ) {
self::$validity_by_type[ $type ] = array(
'valid' => 0,
'total' => 0,
);
}
self::$validity_by_type[ $type ]['total']++;
if ( 0 === $unaccepted_error_count ) {
self::$validity_by_type[ $type ]['valid']++;
}
}
}

View File

@ -0,0 +1,127 @@
<?php
/**
* Class AMP_Comment_Walker
*
* @deprecated 1.1.0 This functionality was moved to AMP_Comments_Sanitizer
* @package AMP
*/
/* translators: 1: AMP_Comment_Walker. 2: AMP_Comments_Sanitizer. */
_deprecated_file( __FILE__, '1.1', null, sprintf( esc_html__( '%1$s functionality has been moved to %2$s.', 'amp' ), 'AMP_Comment_Walker', 'AMP_Comments_Sanitizer' ) );
/**
* Class AMP_Comment_Walker
*
* Walker to wrap comments in mustache tags for amp-template.
*
* @deprecated 1.1.0 This functionality was moved to AMP_Comments_Sanitizer
*/
class AMP_Comment_Walker extends Walker_Comment {
/**
* The original comments arguments.
*
* @since 0.7
* @var array
*/
public $args;
/**
* Holds the timestamp of the most recent comment in a thread.
*
* @since 0.7
* @var array
*/
private $comment_thread_age = array();
/**
* Starts the element output.
*
* @since 0.7.0
*
* @see Walker::start_el()
* @see wp_list_comments()
* @global int $comment_depth
* @global WP_Comment $comment
*
* @param string $output Used to append additional content. Passed by reference.
* @param WP_Comment $comment Comment data object.
* @param int $depth Optional. Depth of the current comment in reference to parents. Default 0.
* @param array $args Optional. An array of arguments. Default empty array.
* @param int $id Optional. ID of the current comment. Default 0 (unused).
*/
public function start_el( &$output, $comment, $depth = 0, $args = array(), $id = 0 ) {
$new_out = '';
parent::start_el( $new_out, $comment, $depth, $args, $id );
if ( 'div' === $args['style'] ) {
$tag = '<div';
} else {
$tag = '<li';
}
$new_tag = $tag . ' data-sort-time="' . esc_attr( strtotime( $comment->comment_date ) ) . '"';
if ( ! empty( $this->comment_thread_age[ $comment->comment_ID ] ) ) {
$new_tag .= ' data-update-time="' . esc_attr( $this->comment_thread_age[ $comment->comment_ID ] ) . '"';
}
$output .= $new_tag . substr( ltrim( $new_out ), strlen( $tag ) );
}
/**
* Output amp-list template code and place holder for comments.
*
* @since 0.7
* @see Walker::paged_walk()
*
* @param WP_Comment[] $elements List of comment Elements.
* @param int $max_depth The maximum hierarchical depth.
* @param int $page_num The specific page number, beginning with 1.
* @param int $per_page Per page counter.
*
* @return string XHTML of the specified page of elements.
*/
public function paged_walk( $elements, $max_depth, $page_num, $per_page ) {
if ( empty( $elements ) || $max_depth < -1 ) {
return '';
}
$this->build_thread_latest_date( $elements );
$args = array_slice( func_get_args(), 4 );
return parent::paged_walk( $elements, $max_depth, $page_num, $per_page, $args[0] );
}
/**
* Find the timestamp of the latest child comment of a thread to set the updated time.
*
* @since 0.7
*
* @param WP_Comment[] $elements The list of comments to get thread times for.
* @param int $time $the timestamp to check against.
* @param bool $is_child Flag used to set the the value or return the time.
* @return int Latest time.
*/
protected function build_thread_latest_date( $elements, $time = 0, $is_child = false ) {
foreach ( $elements as $element ) {
$children = $element->get_children();
$this_time = strtotime( $element->comment_date );
if ( ! empty( $children ) ) {
$this_time = $this->build_thread_latest_date( $children, $this_time, true );
}
if ( $this_time > $time ) {
$time = $this_time;
}
if ( false === $is_child ) {
$this->comment_thread_age[ $element->comment_ID ] = $time;
}
}
return $time;
}
}

View File

@ -0,0 +1,448 @@
<?php
/**
* Class AMP_HTTP
*
* @since 1.0
* @package AMP
*/
/**
* Class AMP_HTTP
*/
class AMP_HTTP {
/**
* Headers sent (or attempted to be sent).
*
* @since 1.0
* @see AMP_HTTP::send_header()
* @var array[]
*/
public static $headers_sent = array();
/**
* AMP-specific query vars that were purged.
*
* @since 0.7
* @since 1.0 Moved to AMP_HTTP class.
* @see AMP_HTTP::purge_amp_query_vars()
* @var string[]
*/
public static $purged_amp_query_vars = array();
/**
* Send an HTTP response header.
*
* This largely exists to facilitate unit testing but it also provides a better interface for sending headers.
*
* @since 0.7.0
* @since 1.0 Moved to AMP_HTTP class.
*
* @param string $name Header name.
* @param string $value Header value.
* @param array $args {
* Args to header().
*
* @type bool $replace Whether to replace a header previously sent. Default true.
* @type int $status_code Status code to send with the sent header.
* }
* @return bool Whether the header was sent.
*/
public static function send_header( $name, $value, $args = array() ) {
$args = array_merge(
array(
'replace' => true,
'status_code' => null,
),
$args
);
self::$headers_sent[] = array_merge( compact( 'name', 'value' ), $args );
if ( headers_sent() ) {
return false;
}
header(
sprintf( '%s: %s', $name, $value ),
$args['replace'],
$args['status_code']
);
return true;
}
/**
* Send Server-Timing header.
*
* If WP_DEBUG is not enabled and an admin user (who can manage_options) is not logged-in, the Server-Header will not be sent.
*
* @since 1.0
*
* @param string $name Name.
* @param float $duration Duration. If negative, will be added to microtime( true ). Optional.
* @param string $description Description. Optional.
* @return bool Return value of send_header call. If WP_DEBUG is not enabled or admin user (who can manage_options) is not logged-in, this will always return false.
*/
public static function send_server_timing( $name, $duration = null, $description = null ) {
if ( ! WP_DEBUG && ! current_user_can( 'manage_options' ) ) {
return false;
}
$value = $name;
if ( isset( $description ) ) {
$value .= sprintf( ';desc="%s"', str_replace( array( '\\', '"' ), '', substr( $description, 0, 100 ) ) );
}
if ( isset( $duration ) ) {
if ( $duration < 0 ) {
$duration = microtime( true ) + $duration;
}
$value .= sprintf( ';dur=%f', $duration * 1000 );
}
return self::send_header( 'Server-Timing', $value, array( 'replace' => false ) );
}
/**
* Remove query vars that come in requests such as for amp-live-list.
*
* WordPress should generally not respond differently to requests when these parameters
* are present. In some cases, when a query param such as __amp_source_origin is present
* then it would normally get included into pagination links generated by get_pagenum_link().
* The whitelist sanitizer empties out links that contain this string as it matches the
* blacklisted_value_regex. So by preemptively scrubbing any reference to these query vars
* we can ensure that WordPress won't end up referencing them in any way.
*
* @since 0.7
* @since 1.0 Moved to AMP_HTTP class.
*/
public static function purge_amp_query_vars() {
$query_vars = array(
'__amp_source_origin',
'_wp_amp_action_xhr_converted',
'amp_latest_update_time',
'amp_last_check_time',
);
// Scrub input vars.
foreach ( $query_vars as $query_var ) {
if ( ! isset( $_GET[ $query_var ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
continue;
}
self::$purged_amp_query_vars[ $query_var ] = wp_unslash( $_GET[ $query_var ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
unset( $_REQUEST[ $query_var ], $_GET[ $query_var ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$scrubbed = true;
}
if ( isset( $scrubbed ) ) {
$build_query = function ( $query ) use ( $query_vars ) {
$pattern = '/^(' . join( '|', $query_vars ) . ')(?==|$)/';
$pairs = array();
foreach ( explode( '&', $query ) as $pair ) {
if ( ! preg_match( $pattern, $pair ) ) {
$pairs[] = $pair;
}
}
return join( '&', $pairs );
};
// Scrub QUERY_STRING.
if ( ! empty( $_SERVER['QUERY_STRING'] ) ) {
$_SERVER['QUERY_STRING'] = $build_query( $_SERVER['QUERY_STRING'] );
}
// Scrub REQUEST_URI.
if ( ! empty( $_SERVER['REQUEST_URI'] ) ) {
list( $path, $query ) = explode( '?', $_SERVER['REQUEST_URI'], 2 );
$pairs = $build_query( $query );
$_SERVER['REQUEST_URI'] = $path;
if ( ! empty( $pairs ) ) {
$_SERVER['REQUEST_URI'] .= "?{$pairs}";
}
}
}
}
/**
* Filter the allowed redirect hosts to include AMP caches.
*
* @since 1.0
*
* @param array $allowed_hosts Allowed hosts.
* @return array Allowed redirect hosts.
*/
public static function filter_allowed_redirect_hosts( $allowed_hosts ) {
return array_merge( $allowed_hosts, self::get_amp_cache_hosts() );
}
/**
* Get list of AMP cache hosts (that is, CORS origins).
*
* @since 1.0
* @link https://www.ampproject.org/docs/fundamentals/amp-cors-requests#1)-allow-requests-for-specific-cors-origins
*
* @return array AMP cache hosts.
*/
public static function get_amp_cache_hosts() {
$hosts = array();
// Google AMP Cache (legacy).
$hosts[] = 'cdn.ampproject.org';
// From the publishers own origins.
$domains = array_unique(
array(
wp_parse_url( site_url(), PHP_URL_HOST ),
wp_parse_url( home_url(), PHP_URL_HOST ),
)
);
/*
* From AMP docs:
* "When possible, the Google AMP Cache will create a subdomain for each AMP document's domain by first converting it
* from IDN (punycode) to UTF-8. The caches replaces every - (dash) with -- (2 dashes) and replace every . (dot) with
* - (dash). For example, pub.com will map to pub-com.cdn.ampproject.org."
*/
foreach ( $domains as $domain ) {
if ( function_exists( 'idn_to_utf8' ) ) {
// The third parameter is set explicitly to prevent issues with newer PHP versions compiled with an old ICU version.
// phpcs:ignore PHPCompatibility.Constants.RemovedConstants.intl_idna_variant_2003Deprecated
$domain = idn_to_utf8( $domain, IDNA_DEFAULT, defined( 'INTL_IDNA_VARIANT_UTS46' ) ? INTL_IDNA_VARIANT_UTS46 : INTL_IDNA_VARIANT_2003 );
}
$subdomain = str_replace( '-', '--', $domain );
$subdomain = str_replace( '.', '-', $subdomain );
// Google AMP Cache subdomain.
$hosts[] = sprintf( '%s.cdn.ampproject.org', $subdomain );
// Cloudflare AMP Cache.
$hosts[] = sprintf( '%s.amp.cloudflare.com', $subdomain );
// Bing AMP Cache.
$hosts[] = sprintf( '%s.bing-amp.com', $subdomain );
}
return $hosts;
}
/**
* Send cors headers.
*
* From the AMP docs:
* Restrict requests to source origins
* In all fetch requests, the AMP Runtime passes the "__amp_source_origin" query parameter, which contains
* the value of the source origin (for example, "https://publisher1.com").
*
* To restrict requests to only source origins, check that the value of the "__amp_source_origin" parameter
* is within a set of the Publisher's own origins.
*
* Access-Control-Allow-Origin: <origin>
* This header is a W3 CORS Spec requirement, where origin refers to the requesting origin that was allowed
* via the CORS Origin request header (for example, "https://<publisher's subdomain>.cdn.ampproject.org").
*
* Although the W3 CORS spec allows the value of * to be returned in the response, for improved security, you should:
*
* - If the Origin header is present, validate and echo the value of the Origin header.
* - If the Origin header isn't present, validate and echo the value of the "__amp_source_origin".
*
* (Otherwise, no Access-Control-Allow-Origin header is sent.)
*
* AMP-Access-Control-Allow-Source-Origin: <source-origin>
* This header allows the specified source-origin to read the authorization response. The source-origin is
* the value specified and verified in the "__amp_source_origin" URL parameter (for example, "https://publisher1.com").
*
* Access-Control-Expose-Headers: AMP-Access-Control-Allow-Source-Origin
* This header simply allows the CORS response to contain the AMP-Access-Control-Allow-Source-Origin header.
*
* @link https://www.ampproject.org/docs/fundamentals/amp-cors-requests
* @since 1.0
*/
public static function send_cors_headers() {
$origin = null;
$source_origin = null;
if ( isset( $_SERVER['HTTP_ORIGIN'] ) ) {
$origin = wp_validate_redirect( wp_sanitize_redirect( esc_url_raw( wp_unslash( $_SERVER['HTTP_ORIGIN'] ) ) ) );
}
if ( isset( self::$purged_amp_query_vars['__amp_source_origin'] ) ) {
$source_origin = wp_validate_redirect( wp_sanitize_redirect( esc_url_raw( self::$purged_amp_query_vars['__amp_source_origin'] ) ) );
}
if ( ! $origin ) {
$origin = $source_origin;
}
if ( $origin ) {
self::send_header( 'Access-Control-Allow-Origin', $origin, array( 'replace' => false ) );
self::send_header( 'Access-Control-Allow-Credentials', 'true' );
self::send_header( 'Vary', 'Origin', array( 'replace' => false ) );
}
if ( $source_origin ) {
self::send_header( 'AMP-Access-Control-Allow-Source-Origin', $source_origin );
self::send_header( 'Access-Control-Expose-Headers', 'AMP-Access-Control-Allow-Source-Origin', array( 'replace' => false ) );
}
}
/**
* Hook into a POST form submissions, such as the comment form or some other form submission.
*
* @since 0.7.0
* @since 1.0 Moved to AMP_HTTP class. Extracted some logic to send_cors_headers method.
*/
public static function handle_xhr_request() {
$is_amp_xhr = (
! empty( self::$purged_amp_query_vars['_wp_amp_action_xhr_converted'] )
&&
( ! empty( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] )
);
if ( ! $is_amp_xhr ) {
return;
}
// Intercept POST requests which redirect.
add_filter( 'wp_redirect', array( __CLASS__, 'intercept_post_request_redirect' ), PHP_INT_MAX );
// Add special handling for redirecting after comment submission.
add_filter( 'comment_post_redirect', array( __CLASS__, 'filter_comment_post_redirect' ), PHP_INT_MAX, 2 );
// Add die handler for AMP error display, most likely due to problem with comment.
add_filter(
'wp_die_handler',
function () {
return array( __CLASS__, 'handle_wp_die' );
}
);
}
/**
* Intercept the response to a POST request.
*
* @since 0.7.0
* @since 1.0 Moved to AMP_HTTP class.
* @see wp_redirect()
*
* @param string $location The location to redirect to.
*/
public static function intercept_post_request_redirect( $location ) {
// Make sure relative redirects get made absolute.
$parsed_location = array_merge(
array(
'scheme' => 'https',
'host' => wp_parse_url( home_url(), PHP_URL_HOST ),
'path' => isset( $_SERVER['REQUEST_URI'] ) ? strtok( wp_unslash( $_SERVER['REQUEST_URI'] ), '?' ) : '/',
),
wp_parse_url( $location )
);
$absolute_location = '';
if ( 'https' === $parsed_location['scheme'] ) {
$absolute_location .= $parsed_location['scheme'] . ':';
}
$absolute_location .= '//' . $parsed_location['host'];
if ( isset( $parsed_location['port'] ) ) {
$absolute_location .= ':' . $parsed_location['port'];
}
$absolute_location .= $parsed_location['path'];
if ( isset( $parsed_location['query'] ) ) {
$absolute_location .= '?' . $parsed_location['query'];
}
if ( isset( $parsed_location['fragment'] ) ) {
$absolute_location .= '#' . $parsed_location['fragment'];
}
self::send_header( 'AMP-Redirect-To', $absolute_location );
self::send_header( 'Access-Control-Expose-Headers', 'AMP-Redirect-To', array( 'replace' => false ) );
wp_send_json_success();
}
/**
* New error handler for AMP form submission.
*
* @since 0.7.0
* @since 1.0 Moved to AMP_HTTP class.
* @see wp_die()
*
* @param WP_Error|string $error The error to handle.
* @param string|int $title Optional. Error title. If `$message` is a `WP_Error` object,
* error data with the key 'title' may be used to specify the title.
* If `$title` is an integer, then it is treated as the response
* code. Default empty.
* @param string|array|int $args {
* Optional. Arguments to control behavior. If `$args` is an integer, then it is treated
* as the response code. Default empty array.
*
* @type int $response The HTTP response code. Default 200 for Ajax requests, 500 otherwise.
* }
*/
public static function handle_wp_die( $error, $title = '', $args = array() ) {
if ( is_int( $title ) ) {
$status_code = $title;
} elseif ( is_int( $args ) ) {
$status_code = $args;
} elseif ( is_array( $args ) && isset( $args['response'] ) ) {
$status_code = $args['response'];
} else {
$status_code = 500;
}
status_header( $status_code );
if ( is_wp_error( $error ) ) {
$error = $error->get_error_message();
}
// Message will be shown in template defined by AMP_Theme_Support::amend_comment_form().
wp_send_json(
array(
'error' => amp_wp_kses_mustache( $error ),
)
);
}
/**
* Handle comment_post_redirect to ensure page reload is done when comments_live_list is not supported, while sending back a success message when it is.
*
* @since 0.7.0
* @since 1.0 Moved to AMP_HTTP class.
*
* @param string $url Comment permalink to redirect to.
* @param WP_Comment $comment Posted comment.
*
* @return string|null URL if redirect to be done; otherwise function will exist.
*/
public static function filter_comment_post_redirect( $url, $comment ) {
$theme_support = AMP_Theme_Support::get_theme_support_args();
// Cause a page refresh if amp-live-list is not implemented for comments via add_theme_support( AMP_Theme_Support::SLUG, array( 'comments_live_list' => true ) ).
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
*/
$url = add_query_arg( 'comment', $comment->comment_ID, $url );
// Pass URL along to wp_redirect().
return $url;
}
// Create a success message to display to the user.
if ( '1' === (string) $comment->comment_approved ) {
$message = __( 'Your comment has been posted.', 'amp' );
} else {
$message = __( 'Your comment is awaiting moderation.', 'amp' );
}
/**
* Filters the message when comment submitted success message when
*
* @since 0.7
*/
$message = apply_filters( 'amp_comment_posted_message', $message, $comment );
// Message will be shown in template defined by AMP_Theme_Support::amend_comment_form().
wp_send_json(
array(
'message' => amp_wp_kses_mustache( $message ),
)
);
return null;
}
}

View File

@ -0,0 +1,151 @@
<?php
/**
* AMP Post type support.
*
* @package AMP
* @since 0.6
*/
/**
* Class AMP_Post_Type_Support.
*/
class AMP_Post_Type_Support {
/**
* Post type support slug.
*
* @var string
*/
const SLUG = 'amp';
/**
* 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' );
}
/**
* Get post types that are eligible for AMP support.
*
* @since 0.6
* @return string[] Post types eligible for AMP.
*/
public static function get_eligible_post_types() {
return array_values(
get_post_types(
array(
'public' => true,
),
'names'
)
);
}
/**
* Declare support for post types.
*
* This function should only be invoked through the 'after_setup_theme' action to
* allow plugins/theme to overwrite the post types support.
*
* @since 0.6
*/
public static function add_post_type_support() {
if ( current_theme_supports( AMP_Theme_Support::SLUG ) && 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, self::SLUG );
}
}
/**
* Return error codes for why a given post does not have AMP support.
*
* @since 0.6
*
* @param WP_Post|int $post Post.
* @return array Error codes for why a given post does not have AMP support.
*/
public static function get_support_errors( $post ) {
if ( ! ( $post instanceof WP_Post ) ) {
$post = get_post( $post );
}
$errors = array();
if ( ! post_type_supports( $post->post_type, self::SLUG ) ) {
$errors[] = 'post-type-support';
}
if ( post_password_required( $post ) ) {
$errors[] = 'password-protected';
}
/**
* 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 ( isset( $post->ID ) && true === apply_filters( 'amp_skip_post', false, $post->ID, $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_Theme_Support::SLUG )
||
(
! (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;
}
}

View File

@ -0,0 +1,347 @@
<?php
/**
* AMP Service Workers.
*
* @package AMP
* @since 1.1
*/
/**
* Class AMP_Service_Worker.
*/
class AMP_Service_Worker {
/**
* Query var that is used to signal a request to install the service worker in an iframe.
*
* @link https://www.ampproject.org/docs/reference/components/amp-install-serviceworker#data-iframe-src-(optional)
*/
const INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR = 'amp_install_service_worker_iframe';
/**
* Init.
*/
public static function init() {
if ( ! class_exists( 'WP_Service_Workers' ) ) {
return;
}
// Shim support for service worker installation from PWA feature plugin.
add_filter( 'query_vars', array( __CLASS__, 'add_query_var' ) );
add_action( 'parse_request', array( __CLASS__, 'handle_service_worker_iframe_install' ) );
add_action( 'wp', array( __CLASS__, 'add_install_hooks' ) );
$theme_support = AMP_Theme_Support::get_theme_support_args();
if ( isset( $theme_support['service_worker'] ) && false === $theme_support['service_worker'] ) {
return;
}
/*
* The default-enabled options reflect which features are not commented-out in the AMP-by-Example service worker.
* See <https://github.com/ampproject/amp-by-example/blob/e093edb401b1617859b5365e80b639d81b06f058/boilerplate-generator/templates/files/serviceworkerJs.js>.
*/
$enabled_options = array(
'cdn_script_caching' => true,
'image_caching' => false,
'google_fonts_caching' => false,
);
if ( isset( $theme_support['service_worker'] ) && is_array( $theme_support['service_worker'] ) ) {
$enabled_options = array_merge(
$enabled_options,
$theme_support['service_worker']
);
}
if ( $enabled_options['cdn_script_caching'] ) {
add_action( 'wp_front_service_worker', array( __CLASS__, 'add_cdn_script_caching' ) );
}
if ( $enabled_options['image_caching'] ) {
add_action( 'wp_front_service_worker', array( __CLASS__, 'add_image_caching' ) );
}
if ( $enabled_options['google_fonts_caching'] ) {
add_action( 'wp_front_service_worker', array( __CLASS__, 'add_google_fonts_caching' ) );
}
}
/**
* Add query var for iframe service worker request.
*
* @param array $vars Query vars.
* @return array Amended query vars.
*/
public static function add_query_var( $vars ) {
$vars[] = self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR;
return $vars;
}
/**
* Add runtime caching for scripts loaded from the AMP CDN with a stale-while-revalidate strategy.
*
* @link https://github.com/ampproject/amp-by-example/blob/4593af61609898043302a101826ddafe7206bfd9/boilerplate-generator/templates/files/serviceworkerJs.js
*
* @param WP_Service_Worker_Scripts $service_workers Service worker registry.
*/
public static function add_cdn_script_caching( $service_workers ) {
if ( ! ( $service_workers instanceof WP_Service_Worker_Scripts ) ) {
/* translators: %s: WP_Service_Worker_Cache_Registry. */
_doing_it_wrong( __METHOD__, sprintf( esc_html__( 'Please update to PWA v0.2. Expected argument to be %s.', 'amp' ), 'WP_Service_Worker_Cache_Registry' ), '1.1' );
return;
}
// Add AMP scripts to runtime cache which will then get stale-while-revalidate strategy.
$service_workers->register(
'amp-cdn-runtime-caching',
function() {
$urls = AMP_Service_Worker::get_precached_script_cdn_urls();
if ( empty( $urls ) ) {
return '';
}
$js = file_get_contents( AMP__DIR__ . '/assets/js/amp-service-worker-runtime-precaching.js' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.WP.AlternativeFunctions.file_system_read_file_get_contents
$js = preg_replace( '#/\*\s*global.+?\*/#', '', $js );
$js = str_replace(
'URLS',
wp_json_encode( $urls ),
$js
);
return $js;
}
);
// Serve the AMP Runtime from cache and check for an updated version in the background. See <https://github.com/ampproject/amp-by-example/blob/4593af61609898043302a101826ddafe7206bfd9/boilerplate-generator/templates/files/serviceworkerJs.js#L54-L58>.
$service_workers->caching_routes()->register(
'^https:\/\/cdn\.ampproject\.org\/.*',
array(
'strategy' => WP_Service_Worker_Caching_Routes::STRATEGY_STALE_WHILE_REVALIDATE,
)
);
}
/**
* Add runtime image caching from the origin with a cache-first strategy.
*
* @link https://github.com/ampproject/amp-by-example/blob/4593af61609898043302a101826ddafe7206bfd9/boilerplate-generator/templates/files/serviceworkerJs.js#L60-L74
*
* @param WP_Service_Worker_Scripts $service_workers Service workers.
*/
public static function add_image_caching( $service_workers ) {
if ( ! ( $service_workers instanceof WP_Service_Worker_Scripts ) ) {
_doing_it_wrong( __METHOD__, esc_html__( 'Please update to PWA v0.2. Expected argument to be WP_Service_Worker_Scripts.', 'amp' ), '1.1' );
return;
}
$service_workers->caching_routes()->register(
'^' . preg_quote( set_url_scheme( content_url( '/' ), 'https' ), '/' ) . '[^\?]+?\.(?:png|gif|jpg|jpeg|svg|webp)(\?.*)?$',
array(
'strategy' => WP_Service_Worker_Caching_Routes::STRATEGY_CACHE_FIRST,
'cacheName' => 'images',
'plugins' => array(
'cacheableResponse' => array(
'statuses' => array( 0, 200 ),
),
'expiration' => array(
'maxEntries' => 60,
'maxAgeSeconds' => MONTH_IN_SECONDS,
),
),
)
);
}
/**
* Add runtime caching of Google Fonts with stale-while-revalidate strategy for stylesheets and cache-first strategy for webfont files.
*
* @link https://developers.google.com/web/tools/workbox/guides/common-recipes#google_fonts
* @link https://github.com/ampproject/amp-by-example/blob/4593af61609898043302a101826ddafe7206bfd9/boilerplate-generator/templates/files/serviceworkerJs.js#L76-L103
* @link https://github.com/xwp/pwa-wp/blob/master/integrations/class-wp-service-worker-fonts-integration.php
*
* @param WP_Service_Worker_Scripts $service_workers Service workers.
*/
public static function add_google_fonts_caching( $service_workers ) {
if ( ! ( $service_workers instanceof WP_Service_Worker_Scripts ) ) {
_doing_it_wrong( __METHOD__, esc_html__( 'Please update to PWA v0.2. Expected argument to be WP_Service_Worker_Scripts.', 'amp' ), '1.1' );
return;
}
// The PWA plugin also automatically adds runtime caching for Google Fonts when WP_SERVICE_WORKER_INTEGRATIONS_ENABLED is set.
if ( class_exists( 'WP_Service_Worker_Fonts_Integration' ) ) {
return;
}
// Cache the Google Fonts stylesheets with a stale while revalidate strategy.
$service_workers->caching_routes()->register(
'^https:\/\/fonts\.googleapis\.com',
array(
'strategy' => WP_Service_Worker_Caching_Routes::STRATEGY_STALE_WHILE_REVALIDATE,
'cacheName' => 'google-fonts-stylesheets',
)
);
// Cache the Google Fonts webfont files with a cache first strategy for 1 year.
$service_workers->caching_routes()->register(
'^https:\/\/fonts\.gstatic\.com',
array(
'strategy' => WP_Service_Worker_Caching_Routes::STRATEGY_CACHE_FIRST,
'cacheName' => 'google-fonts-webfonts',
'plugins' => array(
'cacheableResponse' => array(
'statuses' => array( 0, 200 ),
),
'expiration' => array(
'maxAgeSeconds' => YEAR_IN_SECONDS,
'maxEntries' => 30,
),
),
)
);
}
/**
* Register URLs that will be precached in the runtime cache. (Yes, this sounds somewhat strange.)
*
* Note that the PWA plugin handles the precaching of custom logo, custom header,
* and custom background. The PWA plugin also handles precaching & serving of the
* offline/500 error pages and enabling navigation preload.
*
* @link https://github.com/ampproject/amp-by-example/blob/4593af61609898043302a101826ddafe7206bfd9/boilerplate-generator/templates/files/serviceworkerJs.js#L9-L22
* @see AMP_Service_Worker::add_cdn_script_caching()
*
* @return array Runtime pre-cached URLs.
*/
public static function get_precached_script_cdn_urls() {
// List of AMP scripts that we know will be used in WordPress always.
$precached_handles = array(
'amp-runtime',
'amp-bind', // Used by comments.
'amp-form', // Used by comments.
'amp-install-serviceworker',
);
$theme_support = AMP_Theme_Support::get_theme_support_args();
if ( ! empty( $theme_support['comments_live_list'] ) ) {
$precached_handles[] = 'amp-live-list';
}
if ( amp_get_analytics() ) {
$precached_handles[] = 'amp-analytics';
}
$urls = array();
foreach ( $precached_handles as $handle ) {
if ( wp_script_is( $handle, 'registered' ) ) {
$urls[] = wp_scripts()->registered[ $handle ]->src;
}
}
return $urls;
}
/**
* Add hooks to install the service worker from AMP page.
*/
public static function add_install_hooks() {
if ( current_theme_supports( 'amp' ) && is_amp_endpoint() ) {
add_action( 'wp_footer', array( __CLASS__, 'install_service_worker' ) );
// Prevent validation error due to the script that installs the service worker on non-AMP pages.
$priority = has_action( 'wp_print_scripts', 'wp_print_service_workers' );
if ( false !== $priority ) {
remove_action( 'wp_print_scripts', 'wp_print_service_workers', $priority );
}
}
// Reader mode integration.
add_action( 'amp_post_template_footer', array( __CLASS__, 'install_service_worker' ) );
add_filter(
'amp_post_template_data',
function ( $data ) {
$data['amp_component_scripts']['amp-install-serviceworker'] = true;
return $data;
}
);
}
/**
* Install service worker(s).
*
* @since 1.1
* @see wp_print_service_workers()
* @link https://github.com/xwp/pwa-wp
*/
public static function install_service_worker() {
if ( ! function_exists( 'wp_service_workers' ) || ! function_exists( 'wp_get_service_worker_url' ) ) {
return;
}
$src = wp_get_service_worker_url( WP_Service_Workers::SCOPE_FRONT );
$iframe_src = add_query_arg(
self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR,
WP_Service_Workers::SCOPE_FRONT,
home_url( '/', 'https' )
);
?>
<amp-install-serviceworker
src="<?php echo esc_url( $src ); ?>"
data-iframe-src="<?php echo esc_url( $iframe_src ); ?>"
layout="nodisplay"
>
</amp-install-serviceworker>
<?php
}
/**
* Handle request to install service worker via iframe.
*
* @see wp_print_service_workers()
* @link https://www.ampproject.org/docs/reference/components/amp-install-serviceworker#data-iframe-src-(optional)
*/
public static function handle_service_worker_iframe_install() {
if ( ! isset( $GLOBALS['wp']->query_vars[ self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR ] ) ) {
return;
}
$scope = intval( $GLOBALS['wp']->query_vars[ self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR ] );
if ( WP_Service_Workers::SCOPE_ADMIN !== $scope && WP_Service_Workers::SCOPE_FRONT !== $scope ) {
wp_die(
esc_html__( 'No service workers registered for the requested scope.', 'amp' ),
esc_html__( 'Service Worker Installation', 'amp' ),
array( 'response' => 404 )
);
}
$front_scope = home_url( '/', 'relative' );
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title><?php esc_html_e( 'Service Worker Installation', 'amp' ); ?></title>
</head>
<body>
<?php esc_html_e( 'Installing service worker...', 'amp' ); ?>
<?php
printf(
'<script>navigator.serviceWorker.register( %s, %s );</script>',
wp_json_encode( wp_get_service_worker_url( $scope ) ),
wp_json_encode( array( 'scope' => $front_scope ) )
);
?>
</body>
</html>
<?php
// Die in a way that can be unit tested.
add_filter(
'wp_die_handler',
function() {
return function() {
die();
};
},
1
);
wp_die();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
<?php
/**
* Deprecated functions.
*
* @package AMP
*/
/**
* Load classes for FasterImage.
*
* @deprecated This is obsolete now that there is an autoloader.
*/
function amp_load_fasterimage_classes() {
_deprecated_function( __FUNCTION__, '0.6' );
}
/**
* Get FasterImage client for user agent.
*
* @deprecated This function is no longer used in favor of just instantiating the class.
*
* @param string $user_agent User Agent.
* @return \FasterImage\FasterImage Instance.
*/
function amp_get_fasterimage_client( $user_agent ) {
_deprecated_function( __FUNCTION__, '1.0' );
return new FasterImage\FasterImage( $user_agent );
}

View File

@ -0,0 +1,80 @@
<?php
/**
* Class AMP_Base_Embed_Handler
*
* Used by some children.
*
* @package AMP
*/
/**
* Class AMP_Base_Embed_Handler
*/
abstract class AMP_Base_Embed_Handler {
/**
* Default width.
*
* @var int
*/
protected $DEFAULT_WIDTH = 600;
/**
* Default height.
*
* @var int
*/
protected $DEFAULT_HEIGHT = 480;
/**
* Default arguments.
*
* @var array
*/
protected $args = array();
/**
* Whether or not conversion was completed.
*
* @var boolean
*/
protected $did_convert_elements = false;
/**
* Registers embed.
*/
abstract public function register_embed();
/**
* Unregisters embed.
*/
abstract public function unregister_embed();
/**
* Constructor.
*
* @param array $args Height and width for embed.
*/
public function __construct( $args = array() ) {
$this->args = wp_parse_args(
$args,
array(
'width' => $this->DEFAULT_WIDTH,
'height' => $this->DEFAULT_HEIGHT,
)
);
}
/**
* Get mapping of AMP component names to AMP script URLs.
*
* This is normally no longer needed because the whitelist
* sanitizer will automatically detect the need for them via
* the spec.
*
* @see AMP_Tag_And_Attribute_Sanitizer::get_scripts()
* @return array Scripts.
*/
public function get_scripts() {
return array();
}
}

View File

@ -0,0 +1,131 @@
<?php
/**
* Class AMP_Core_Block_Handler
*
* @package AMP
*/
/**
* Class AMP_Core_Block_Handler
*
* @since 1.0
*/
class AMP_Core_Block_Handler extends AMP_Base_Embed_Handler {
/**
* Methods to ampify blocks.
*
* @var array
*/
protected $block_ampify_methods = array(
'core/categories' => 'ampify_categories_block',
'core/archives' => 'ampify_archives_block',
);
/**
* Register embed.
*/
public function register_embed() {
add_filter( 'render_block', array( $this, 'filter_rendered_block' ), 0, 2 );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
remove_filter( 'render_block', array( $this, 'filter_rendered_block' ), 0 );
}
/**
* Filters the content of a single block to make it AMP valid.
*
* @param string $block_content The block content about to be appended.
* @param array $block The full block, including name and attributes.
* @return string Filtered block content.
*/
public function filter_rendered_block( $block_content, $block ) {
if ( ! isset( $block['blockName'] ) ) {
return $block_content;
}
if ( isset( $this->block_ampify_methods[ $block['blockName'] ] ) ) {
$block_content = call_user_func(
array( $this, $this->block_ampify_methods[ $block['blockName'] ] ),
$block_content
);
} elseif ( 'core/image' === $block['blockName'] || 'core/audio' === $block['blockName'] ) {
/*
* While the video block placeholder just outputs an empty video element, the placeholders for image and
* audio blocks output empty <img> and <audio> respectively. These will result in AMP validation errors,
* so we need to empty out the block content to prevent this from happening. Note that <source> is used
* for <img> because eventually the image block could use <picture>.
*/
if ( ! preg_match( '/src=|<source/', $block_content ) ) {
$block_content = '';
}
}
return $block_content;
}
/**
* Fix rendering of categories block when displayAsDropdown.
*
* This excludes the disallowed JS scrips, adds <form> tags, and uses on:change for <select>.
*
* @see render_block_core_categories()
*
* @param string $block_content Block content.
* @return string Rendered.
*/
public function ampify_categories_block( $block_content ) {
static $block_id = 0;
$block_id++;
$form_id = "wp-block-categories-dropdown-{$block_id}-form";
// Remove output of build_dropdown_script_block_core_categories().
$block_content = preg_replace( '#<script.+?</script>#s', '', $block_content );
$form = sprintf(
'<form action="%s" method="get" target="_top" id="%s">',
esc_url( home_url() ),
esc_attr( $form_id )
);
$block_content = preg_replace(
'#(<select)(.+</select>)#s',
$form . '$1' . sprintf( ' on="change:%1$s.submit"', esc_attr( $form_id ) ) . '$2</form>',
$block_content,
1
);
return $block_content;
}
/**
* Fix rendering of archives block when displayAsDropdown.
*
* This replaces disallowed script with the use of on:change for <select>.
*
* @see render_block_core_archives()
*
* @param string $block_content Block content.
* @return string Rendered.
*/
public function ampify_archives_block( $block_content ) {
// Eliminate use of uniqid(). Core should be using wp_unique_id() here.
static $block_id = 0;
$block_id++;
$block_content = preg_replace( '/(?<="wp-block-archives-)\w+(?=")/', $block_id, $block_content );
// Replace onchange with on attribute.
$block_content = preg_replace(
'/onchange=".+?"/',
'on="change:AMP.navigateTo(url=event.value)"',
$block_content
);
return $block_content;
}
}

View File

@ -0,0 +1,164 @@
<?php
/**
* Class AMP_DailyMotion_Embed_Handler
*
* @package AMP
*/
/**
* Class AMP_DailyMotion_Embed_Handler
*
* Much of this class is borrowed from Jetpack embeds
*/
class AMP_DailyMotion_Embed_Handler extends AMP_Base_Embed_Handler {
const URL_PATTERN = '#https?:\/\/(www\.)?dailymotion\.com\/video\/.*#i';
const RATIO = 0.5625;
/**
* Default width.
*
* @var int
*/
protected $DEFAULT_WIDTH = 600;
/**
* Default height.
*
* @var int
*/
protected $DEFAULT_HEIGHT = 338;
/**
* AMP_DailyMotion_Embed_Handler constructor.
*
* @param array $args Height, width and maximum width for embed.
*/
public function __construct( $args = array() ) {
parent::__construct( $args );
if ( isset( $this->args['content_max_width'] ) ) {
$max_width = $this->args['content_max_width'];
$this->args['width'] = $max_width;
$this->args['height'] = round( $max_width * self::RATIO );
}
}
/**
* Register embed.
*/
public function register_embed() {
wp_embed_register_handler( 'amp-dailymotion', self::URL_PATTERN, array( $this, 'oembed' ), -1 );
add_shortcode( 'dailymotion', array( $this, 'shortcode' ) );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
wp_embed_unregister_handler( 'amp-dailymotion', -1 );
remove_shortcode( 'dailymotion' );
}
/**
* Gets AMP-compliant markup for the Dailymotion shortcode.
*
* @param array $attr The Dailymotion attributes.
* @return string Dailymotion shortcode markup.
*/
public function shortcode( $attr ) {
$video_id = false;
if ( isset( $attr['id'] ) ) {
$video_id = $attr['id'];
} elseif ( isset( $attr[0] ) ) {
$video_id = $attr[0];
} elseif ( function_exists( 'shortcode_new_to_old_params' ) ) {
$video_id = shortcode_new_to_old_params( $attr );
}
if ( empty( $video_id ) ) {
return '';
}
return $this->render(
array(
'video_id' => $video_id,
)
);
}
/**
* Render oEmbed.
*
* @see \WP_Embed::shortcode()
*
* @param array $matches URL pattern matches.
* @param array $attr Shortcode attribues.
* @param string $url URL.
* @param string $rawattr Unmodified shortcode attributes.
* @return string Rendered oEmbed.
*/
public function oembed( $matches, $attr, $url, $rawattr ) {
$video_id = $this->get_video_id_from_url( $url );
return $this->render(
array(
'video_id' => $video_id,
)
);
}
/**
* Render.
*
* @param array $args Args.
* @return string Rendered.
*/
public function render( $args ) {
$args = wp_parse_args(
$args,
array(
'video_id' => false,
)
);
if ( empty( $args['video_id'] ) ) {
return AMP_HTML_Utils::build_tag(
'a',
array(
'href' => esc_url( $args['url'] ),
'class' => 'amp-wp-embed-fallback',
),
esc_html( $args['url'] )
);
}
$this->did_convert_elements = true;
return AMP_HTML_Utils::build_tag(
'amp-dailymotion',
array(
'data-videoid' => $args['video_id'],
'layout' => 'responsive',
'width' => $this->args['width'],
'height' => $this->args['height'],
)
);
}
/**
* Determine the video ID from the URL.
*
* @param string $url URL.
* @return integer Video ID.
*/
private function get_video_id_from_url( $url ) {
$parsed_url = wp_parse_url( $url );
parse_str( $parsed_url['path'], $path );
$tok = explode( '/', $parsed_url['path'] );
$tok = explode( '_', $tok[2] );
$video_id = $tok[0];
return $video_id;
}
}

View File

@ -0,0 +1,238 @@
<?php
/**
* Class AMP_Facebook_Embed_Handler
*
* @package AMP
*/
/**
* Class AMP_Facebook_Embed_Handler
*/
class AMP_Facebook_Embed_Handler extends AMP_Base_Embed_Handler {
const URL_PATTERN = '#https?://(www\.)?facebook\.com/.*#i';
/**
* Default width.
*
* @var int
*/
protected $DEFAULT_WIDTH = 600;
/**
* Default height.
*
* @var int
*/
protected $DEFAULT_HEIGHT = 400;
/**
* Tag.
*
* @var string embed HTML blockquote tag to identify and replace with AMP version.
*/
protected $sanitize_tag = 'div';
/**
* Tag.
*
* @var string AMP amp-facebook tag
*/
private $amp_tag = 'amp-facebook';
/**
* Registers embed.
*/
public function register_embed() {
wp_embed_register_handler( $this->amp_tag, self::URL_PATTERN, array( $this, 'oembed' ), -1 );
}
/**
* Unregisters embed.
*/
public function unregister_embed() {
wp_embed_unregister_handler( $this->amp_tag, -1 );
}
/**
* WordPress OEmbed rendering callback.
*
* @param array $matches URL pattern matches.
* @param array $attr Matched attributes.
* @param string $url Matched URL.
* @param string $rawattr Raw attributes string.
* @return string HTML markup for rendered embed.
*/
public function oembed( $matches, $attr, $url, $rawattr ) {
return $this->render( array( 'url' => $url ) );
}
/**
* Gets the rendered embed markup.
*
* @param array $args Embed rendering arguments.
* @return string HTML markup for rendered embed.
*/
public function render( $args ) {
$args = wp_parse_args(
$args,
array(
'url' => false,
)
);
if ( empty( $args['url'] ) ) {
return '';
}
$this->did_convert_elements = true;
return AMP_HTML_Utils::build_tag(
$this->amp_tag,
array(
'data-href' => $args['url'],
'layout' => 'responsive',
'width' => $this->args['width'],
'height' => $this->args['height'],
)
);
}
/**
* Sanitized <div class="fb-video" data-href=> tags to <amp-facebook>.
*
* @param DOMDocument $dom DOM.
*/
public function sanitize_raw_embeds( $dom ) {
/**
* Node list.
*
* @var DOMNodeList $node
*/
$nodes = $dom->getElementsByTagName( $this->sanitize_tag );
$num_nodes = $nodes->length;
if ( 0 === $num_nodes ) {
return;
}
for ( $i = $num_nodes - 1; $i >= 0; $i-- ) {
$node = $nodes->item( $i );
if ( ! $node instanceof DOMElement ) {
continue;
}
$embed_type = $this->get_embed_type( $node );
if ( null !== $embed_type ) {
$this->create_amp_facebook_and_replace_node( $dom, $node, $embed_type );
}
}
/*
* Remove the fb-root div and the Facebook Connect JS script since irrelevant.
* <div id="fb-root"></div>
* <script async defer crossorigin="anonymous" src="https://connect.facebook.net/en_US/sdk.js#xfbml=1&version=v3.2"></script>
*/
$fb_root = $dom->getElementById( 'fb-root' );
if ( $fb_root ) {
$xpath = new DOMXPath( $dom );
$scripts = array();
foreach ( $xpath->query( '//script[ starts-with( @src, "https://connect.facebook.net" ) and contains( @src, "sdk.js" ) ]' ) as $script ) {
$scripts[] = $script;
}
foreach ( $scripts as $script ) {
$script->parentNode->removeChild( $script );
}
$fb_root->parentNode->removeChild( $fb_root );
}
}
/**
* Get embed type.
*
* @param DOMElement $node The DOMNode to adjust and replace.
* @return string|null Embed type or null if not detected.
*/
private function get_embed_type( $node ) {
$class_attr = $node->getAttribute( 'class' );
if ( null === $class_attr || ! $node->hasAttribute( 'data-href' ) ) {
return null;
}
if ( false !== strpos( $class_attr, 'fb-post' ) ) {
return 'post';
} elseif ( false !== strpos( $class_attr, 'fb-video' ) ) {
return 'video';
} elseif ( false !== strpos( $class_attr, 'fb-page' ) ) {
return 'page';
} elseif ( false !== strpos( $class_attr, 'fb-like' ) ) {
return 'like';
} elseif ( false !== strpos( $class_attr, 'fb-comments' ) ) {
return 'comments';
} elseif ( false !== strpos( $class_attr, 'fb-comment-embed' ) ) {
return 'comment';
}
return null;
}
/**
* Create amp-facebook and replace node.
*
* @param DOMDocument $dom The HTML Document.
* @param DOMElement $node The DOMNode to adjust and replace.
* @param string $embed_type Embed type.
*/
private function create_amp_facebook_and_replace_node( $dom, $node, $embed_type ) {
$attributes = array(
'layout' => 'responsive',
'width' => $node->hasAttribute( 'data-width' ) ? $node->getAttribute( 'data-width' ) : $this->DEFAULT_WIDTH,
'height' => $node->hasAttribute( 'data-height' ) ? $node->getAttribute( 'data-height' ) : $this->DEFAULT_HEIGHT,
);
$node->removeAttribute( 'data-width' );
$node->removeAttribute( 'data-height' );
foreach ( $node->attributes as $attribute ) {
if ( 'data-' === substr( $attribute->nodeName, 0, 5 ) ) {
$attributes[ $attribute->nodeName ] = $attribute->nodeValue;
}
}
if ( 'page' === $embed_type ) {
$amp_tag = 'amp-facebook-page';
} elseif ( 'like' === $embed_type ) {
$amp_tag = 'amp-facebook-like';
} elseif ( 'comments' === $embed_type ) {
$amp_tag = 'amp-facebook-comments';
} else {
$amp_tag = $this->amp_tag;
$attributes['data-embed-as'] = $embed_type;
}
$amp_facebook_node = AMP_DOM_Utils::create_node(
$dom,
$amp_tag,
$attributes
);
$fallback = null;
foreach ( $node->childNodes as $child_node ) {
if ( $child_node instanceof DOMElement && false !== strpos( $child_node->getAttribute( 'class' ), 'fb-xfbml-parse-ignore' ) ) {
$fallback = $child_node;
$child_node->parentNode->removeChild( $child_node );
$fallback->setAttribute( 'fallback', '' );
break;
}
}
$node->parentNode->replaceChild( $amp_facebook_node, $node );
if ( $fallback ) {
$amp_facebook_node->appendChild( $fallback );
}
$this->did_convert_elements = true;
}
}

View File

@ -0,0 +1,271 @@
<?php
/**
* Class AMP_Gallery_Embed_Handler
*
* @package AMP
*/
/**
* Class AMP_Gallery_Embed_Handler
*
* @since 0.2
*/
class AMP_Gallery_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* Register embed.
*/
public function register_embed() {
add_filter( 'post_gallery', array( $this, 'maybe_override_gallery' ), 10, 2 );
add_action( 'wp_print_styles', array( $this, 'print_styles' ) );
}
/**
* Unregister embed.
*/
public function unregister_embed() {}
/**
* Shortcode handler.
*
* @param array $attr Shortcode attributes.
* @return string Rendered gallery.
*/
public function shortcode( $attr ) {
$post = get_post();
if ( ! empty( $attr['ids'] ) ) {
// 'ids' is explicitly ordered, unless you specify otherwise.
if ( empty( $attr['orderby'] ) ) {
$attr['orderby'] = 'post__in';
}
$attr['include'] = $attr['ids'];
}
$atts = shortcode_atts(
array(
'order' => 'ASC',
'orderby' => 'menu_order ID',
'id' => $post ? $post->ID : 0,
'include' => '',
'exclude' => '',
'size' => array( $this->args['width'], $this->args['height'] ),
'link' => 'none',
),
$attr,
'gallery'
);
if ( ! empty( $attr['amp-lightbox'] ) ) {
$atts['lightbox'] = filter_var( $attr['amp-lightbox'], FILTER_VALIDATE_BOOLEAN );
}
$id = intval( $atts['id'] );
if ( ! empty( $atts['include'] ) ) {
$attachments = get_posts(
array(
'include' => $atts['include'],
'post_status' => 'inherit',
'post_type' => 'attachment',
'post_mime_type' => 'image',
'order' => $atts['order'],
'orderby' => $atts['orderby'],
'fields' => 'ids',
)
);
} elseif ( ! empty( $atts['exclude'] ) ) {
$attachments = get_children(
array(
'post_parent' => $id,
'exclude' => $atts['exclude'],
'post_status' => 'inherit',
'post_type' => 'attachment',
'post_mime_type' => 'image',
'order' => $atts['order'],
'orderby' => $atts['orderby'],
'fields' => 'ids',
)
);
} else {
$attachments = get_children(
array(
'post_parent' => $id,
'post_status' => 'inherit',
'post_type' => 'attachment',
'post_mime_type' => 'image',
'order' => $atts['order'],
'orderby' => $atts['orderby'],
'fields' => 'ids',
)
);
}
if ( empty( $attachments ) ) {
return '';
}
$urls = array();
foreach ( $attachments as $attachment_id ) {
list( $url, $width, $height ) = wp_get_attachment_image_src( $attachment_id, $atts['size'], true );
if ( ! $url ) {
continue;
}
$href = null;
if ( empty( $atts['lightbox'] ) ) {
if ( ! empty( $atts['link'] ) && 'file' === $atts['link'] ) {
$href = $url;
} elseif ( ! empty( $atts['link'] ) && 'post' === $atts['link'] ) {
$href = get_attachment_link( $attachment_id );
}
}
$urls[] = array(
'href' => $href,
'url' => $url,
'width' => $width,
'height' => $height,
);
}
$args = array(
'images' => $urls,
);
if ( ! empty( $atts['lightbox'] ) ) {
$args['lightbox'] = true;
$lightbox_tag = AMP_HTML_Utils::build_tag(
'amp-image-lightbox',
array(
'id' => AMP_Base_Sanitizer::AMP_IMAGE_LIGHTBOX_ID,
'layout' => 'nodisplay',
'data-close-button-aria-label' => __( 'Close', 'amp' ),
)
);
/* We need to add lightbox tag, too. @todo Could there be a better alternative for this? */
return $this->render( $args ) . $lightbox_tag;
}
return $this->render( $args );
}
/**
* Override the output of gallery_shortcode() if amp-carousel is not false.
*
* The 'Gallery' widget also uses this function.
* This ensures that it outputs an <amp-carousel>.
*
* @param string $html Markup to filter, possibly ''.
* @param array $attributes Shortcode attributes.
* @return string $html Markup for the gallery.
*/
public function maybe_override_gallery( $html, $attributes ) {
$is_lightbox = isset( $attributes['amp-lightbox'] ) && true === filter_var( $attributes['amp-lightbox'], FILTER_VALIDATE_BOOLEAN );
if ( isset( $attributes['amp-carousel'] ) && false === filter_var( $attributes['amp-carousel'], FILTER_VALIDATE_BOOLEAN ) ) {
if ( true === $is_lightbox ) {
remove_filter( 'post_gallery', array( $this, 'maybe_override_gallery' ), 10 );
$attributes['link'] = 'none';
$html = '<ul class="amp-lightbox">' . gallery_shortcode( $attributes ) . '</ul>';
add_filter( 'post_gallery', array( $this, 'maybe_override_gallery' ), 10, 2 );
}
return $html;
} elseif ( isset( $attributes['size'] ) && 'thumbnail' === $attributes['size'] ) {
/*
* If the 'gallery' shortcode has a 'size' attribute of 'thumbnail', prevent outputting an <amp-carousel>.
* That will often get thumbnail images around 150 x 150,
* while the <amp-carousel> usually has a width of 600 and a height of 480.
* That often means very low-resolution images.
* So fall back to the normal 'gallery' shortcode callback, gallery_shortcode().
*/
return '';
}
return $this->shortcode( $attributes );
}
/**
* Render.
*
* @param array $args Args.
* @return string Rendered.
*/
public function render( $args ) {
$this->did_convert_elements = true;
$args = wp_parse_args(
$args,
array(
'images' => false,
)
);
if ( empty( $args['images'] ) ) {
return '';
}
$images = array();
foreach ( $args['images'] as $props ) {
$image_atts = array(
'src' => $props['url'],
'width' => $props['width'],
'height' => $props['height'],
'layout' => 'responsive',
);
if ( ! empty( $args['lightbox'] ) ) {
$image_atts['lightbox'] = '';
$image_atts['on'] = 'tap:' . AMP_Img_Sanitizer::AMP_IMAGE_LIGHTBOX_ID;
$image_atts['role'] = 'button';
$image_atts['tabindex'] = 0;
}
$image = AMP_HTML_Utils::build_tag(
'amp-img',
$image_atts
);
if ( ! empty( $props['href'] ) ) {
$image = AMP_HTML_Utils::build_tag(
'a',
array(
'href' => $props['href'],
),
$image
);
}
$images[] = $image;
}
return AMP_HTML_Utils::build_tag(
'amp-carousel',
array(
'width' => $this->args['width'],
'height' => $this->args['height'],
'type' => 'slides',
'layout' => 'responsive',
),
implode( PHP_EOL, $images )
);
}
/**
* Prints the Gallery block styling.
*
* It would be better to print this in AMP_Gallery_Block_Sanitizer,
* but by the time that runs, it's too late.
* This rule is copied exactly from block-library/style.css, but the selector here has amp-img >.
* The image sanitizer normally converts the <img> from that original stylesheet <amp-img>,
* but that doesn't have the same effect as applying it to the <img>.
*
* @return void
*/
public function print_styles() {
?>
<style>
.wp-block-gallery.is-cropped .blocks-gallery-item amp-img > img {
object-fit: cover;
}
</style>
<?php
}
}

View File

@ -0,0 +1,81 @@
<?php
/**
* Class AMP_Gfycat_Embed_Handler
*
* @package AMP
* @since 1.0
*/
/**
* Class AMP_Gfycat_Embed_Handler
*/
class AMP_Gfycat_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* Regex matched to produce output amp-gfycat.
*
* @var string
*/
const URL_PATTERN = '#https?://(www\.)?gfycat\.com/.*#i';
/**
* Register embed.
*/
public function register_embed() {
add_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10, 3 );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
remove_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10 );
}
/**
* Filter oEmbed HTML for Gfycat to prepare it for AMP.
*
* @param mixed $return The shortcode callback function to call.
* @param string $url The attempted embed URL.
* @param array $attr An array of shortcode attributes.
* @return string Embed.
*/
public function filter_embed_oembed_html( $return, $url, $attr ) {
$parsed_url = wp_parse_url( $url );
if ( false !== strpos( $parsed_url['host'], 'gfycat.com' ) ) {
if ( preg_match( '/width=["\']?(\d+)/', $return, $matches ) ) {
$attr['width'] = $matches[1];
}
if ( preg_match( '/height=["\']?(\d+)/', $return, $matches ) ) {
$attr['height'] = $matches[1];
}
if ( empty( $attr['height'] ) ) {
return $return;
}
$attributes = wp_array_slice_assoc( $attr, array( 'width', 'height' ) );
if ( empty( $attr['width'] ) ) {
$attributes['layout'] = 'fixed-height';
$attributes['width'] = 'auto';
}
$pieces = explode( '/detail/', $parsed_url['path'] );
if ( ! isset( $pieces[1] ) ) {
if ( ! preg_match( '/\/([A-Za-z0-9]+)/', $parsed_url['path'], $matches ) ) {
return $return;
}
$attributes['data-gfyid'] = $matches[1];
} else {
$attributes['data-gfyid'] = $pieces[1];
}
$return = AMP_HTML_Utils::build_tag(
'amp-gfycat',
$attributes
);
}
return $return;
}
}

View File

@ -0,0 +1,87 @@
<?php
/**
* Class AMP_Hulu_Embed_Handler
*
* @package AMP
* @since 1.0
*/
/**
* Class AMP_Hulu_Embed_Handler
*/
class AMP_Hulu_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* Regex matched to produce output amp-hulu.
*
* @var string
*/
const URL_PATTERN = '#https?://(www\.)?hulu\.com/.*#i';
/**
* Default height.
*
* @var int
*/
protected $DEFAULT_HEIGHT = 600;
/**
* Register embed.
*/
public function register_embed() {
add_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10, 3 );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
remove_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10 );
}
/**
* Filter oEmbed HTML for Hulu to prepare it for AMP.
*
* @param mixed $return The shortcode callback function to call.
* @param string $url The attempted embed URL.
* @param array $attr An array of shortcode attributes.
* @return string Embed.
*/
public function filter_embed_oembed_html( $return, $url, $attr ) {
$parsed_url = wp_parse_url( $url );
if ( false !== strpos( $parsed_url['host'], 'hulu.com' ) ) {
if ( preg_match( '/width=["\']?(\d+)/', $return, $matches ) ) {
$attr['width'] = $matches[1];
}
if ( preg_match( '/height=["\']?(\d+)/', $return, $matches ) ) {
$attr['height'] = $matches[1];
}
if ( empty( $attr['height'] ) ) {
return $return;
}
$attributes = wp_array_slice_assoc( $attr, array( 'width', 'height' ) );
if ( empty( $attr['width'] ) ) {
$attributes['layout'] = 'fixed-height';
$attributes['width'] = 'auto';
}
$pieces = explode( '/watch/', $parsed_url['path'] );
if ( ! isset( $pieces[1] ) ) {
if ( ! preg_match( '/\/([A-Za-z0-9]+)/', $parsed_url['path'], $matches ) ) {
return $return;
}
$attributes['data-eid'] = $matches[1];
} else {
$attributes['data-eid'] = $pieces[1];
}
$return = AMP_HTML_Utils::build_tag(
'amp-hulu',
$attributes
);
}
return $return;
}
}

View File

@ -0,0 +1,151 @@
<?php
/**
* Class AMP_Imgur_Embed_Handler
*
* @package AMP
* @since 1.0
*/
/**
* Class AMP_Imgur_Embed_Handler
*/
class AMP_Imgur_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* Regex matched to produce output amp-imgur.
*
* @var string
*/
const URL_PATTERN = '#https?://(www\.)?imgur\.com/.*#i';
/**
* Register embed.
*/
public function register_embed() {
if ( version_compare( strtok( get_bloginfo( 'version' ), '-' ), '4.9', '<' ) ) {
// Before 4.9 the embedding Imgur is not working properly, register embed for that case.
wp_embed_register_handler( 'amp-imgur', self::URL_PATTERN, array( $this, 'oembed' ), -1 );
} else {
add_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10, 3 );
}
}
/**
* Unregister embed.
*/
public function unregister_embed() {
if ( version_compare( strtok( get_bloginfo( 'version' ), '-' ), '4.9', '<' ) ) {
wp_embed_unregister_handler( 'amp-imgur', -1 );
} else {
remove_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10 );
}
}
/**
* Oembed.
*
* @param array $matches Matches.
* @param array $attr Attributes.
* @param string $url URL.
* @param array $rawattr Raw attributes.
* @return string Embed.
*/
public function oembed( $matches, $attr, $url, $rawattr ) {
return $this->render( array( 'url' => $url ) );
}
/**
* Render embed.
*
* @param array $args Args.
* @return string Embed.
*/
public function render( $args ) {
$args = wp_parse_args(
$args,
array(
'url' => false,
)
);
if ( empty( $args['url'] ) ) {
return '';
}
$this->did_convert_elements = true;
$id = $this->get_imgur_id_from_url( $args['url'] );
if ( false === $id ) {
return '';
}
return AMP_HTML_Utils::build_tag(
'amp-imgur',
array(
'width' => $this->args['width'],
'height' => $this->args['height'],
'data-imgur-id' => $id,
)
);
}
/**
* Filter oEmbed HTML for Imgur to prepare it for AMP.
*
* @param mixed $return The shortcode callback function to call.
* @param string $url The attempted embed URL.
* @param array $attr An array of shortcode attributes.
* @return string Embed.
*/
public function filter_embed_oembed_html( $return, $url, $attr ) {
$parsed_url = wp_parse_url( $url );
if ( false !== strpos( $parsed_url['host'], 'imgur.com' ) ) {
if ( preg_match( '/width=["\']?(\d+)/', $return, $matches ) ) {
$attr['width'] = $matches[1];
}
if ( preg_match( '/height=["\']?(\d+)/', $return, $matches ) ) {
$attr['height'] = $matches[1];
}
if ( empty( $attr['height'] ) ) {
return $return;
}
$attributes = wp_array_slice_assoc( $attr, array( 'width', 'height' ) );
if ( empty( $attr['width'] ) ) {
$attributes['layout'] = 'fixed-height';
$attributes['width'] = 'auto';
}
$attributes['data-imgur-id'] = $this->get_imgur_id_from_url( $url );
if ( false === $attributes['data-imgur-id'] ) {
return $return;
}
$return = AMP_HTML_Utils::build_tag(
'amp-imgur',
$attributes
);
}
return $return;
}
/**
* Get Imgur ID from URL.
*
* @param string $url URL.
* @return bool|string ID / false.
*/
protected function get_imgur_id_from_url( $url ) {
$parsed_url = wp_parse_url( $url );
$pieces = explode( '/gallery/', $parsed_url['path'] );
if ( ! isset( $pieces[1] ) ) {
if ( ! preg_match( '/\/([A-Za-z0-9]+)/', $parsed_url['path'], $matches ) ) {
return false;
}
return $matches[1];
} else {
return $pieces[1];
}
}
}

View File

@ -0,0 +1,256 @@
<?php
/**
* Class AMP_Instagram_Embed_Handler
*
* @package AMP
*/
/**
* Class AMP_Instagram_Embed_Handler
*
* Much of this class is borrowed from Jetpack embeds
*/
class AMP_Instagram_Embed_Handler extends AMP_Base_Embed_Handler {
const SHORT_URL_HOST = 'instagr.am';
const URL_PATTERN = '#http(s?)://(www\.)?instagr(\.am|am\.com)/p/([^/?]+)#i';
/**
* Default width.
*
* @var int
*/
protected $DEFAULT_WIDTH = 600;
/**
* Default height.
*
* @var int
*/
protected $DEFAULT_HEIGHT = 600;
/**
* Tag.
*
* @var string embed HTML blockquote tag to identify and replace with AMP version.
*/
protected $sanitize_tag = 'blockquote';
/**
* Tag.
*
* @var string AMP amp-facebook tag
*/
private $amp_tag = 'amp-instagram';
/**
* Registers embed.
*/
public function register_embed() {
wp_embed_register_handler( $this->amp_tag, self::URL_PATTERN, array( $this, 'oembed' ), -1 );
add_shortcode( 'instagram', array( $this, 'shortcode' ) );
}
/**
* Unregisters embed.
*/
public function unregister_embed() {
wp_embed_unregister_handler( $this->amp_tag, -1 );
remove_shortcode( 'instagram' );
}
/**
* WordPress shortcode rendering callback.
*
* @param array $attr Shortcode attributes.
* @return string HTML markup for rendered embed.
*/
public function shortcode( $attr ) {
$url = false;
$instagram_id = false;
if ( isset( $attr['url'] ) ) {
$url = trim( $attr['url'] );
}
if ( empty( $url ) ) {
return '';
}
$instagram_id = $this->get_instagram_id_from_url( $url );
return $this->render(
array(
'url' => $url,
'instagram_id' => $instagram_id,
)
);
}
/**
* WordPress OEmbed rendering callback.
*
* @param array $matches URL pattern matches.
* @param array $attr Matched attributes.
* @param string $url Matched URL.
* @param string $rawattr Raw attributes string.
* @return string HTML markup for rendered embed.
*/
public function oembed( $matches, $attr, $url, $rawattr ) {
return $this->render(
array(
'url' => $url,
'instagram_id' => end( $matches ),
)
);
}
/**
* Gets the rendered embed markup.
*
* @param array $args Embed rendering arguments.
* @return string HTML markup for rendered embed.
*/
public function render( $args ) {
$args = wp_parse_args(
$args,
array(
'url' => false,
'instagram_id' => false,
)
);
if ( empty( $args['instagram_id'] ) ) {
return AMP_HTML_Utils::build_tag(
'a',
array(
'href' => esc_url( $args['url'] ),
'class' => 'amp-wp-embed-fallback',
),
esc_html( $args['url'] )
);
}
$this->did_convert_elements = true;
return AMP_HTML_Utils::build_tag(
$this->amp_tag,
array(
'data-shortcode' => $args['instagram_id'],
'data-captioned' => '',
'layout' => 'responsive',
'width' => $this->args['width'],
'height' => $this->args['height'],
)
);
}
/**
* Get Instagram ID from URL.
*
* @param string $url URL.
* @return string|false The ID parsed from the URL or false if not found.
*/
private function get_instagram_id_from_url( $url ) {
$found = preg_match( self::URL_PATTERN, $url, $matches );
if ( ! $found ) {
return false;
}
return end( $matches );
}
/**
* Sanitized <blockquote class="instagram-media"> tags to <amp-instagram>
*
* @param DOMDocument $dom DOM.
*/
public function sanitize_raw_embeds( $dom ) {
/**
* Node list.
*
* @var DOMNodeList $node
*/
$nodes = $dom->getElementsByTagName( $this->sanitize_tag );
$num_nodes = $nodes->length;
if ( 0 === $num_nodes ) {
return;
}
for ( $i = $num_nodes - 1; $i >= 0; $i-- ) {
$node = $nodes->item( $i );
if ( ! $node instanceof DOMElement ) {
continue;
}
if ( $node->hasAttribute( 'data-instgrm-permalink' ) ) {
$this->create_amp_instagram_and_replace_node( $dom, $node );
}
}
}
/**
* Make final modifications to DOMNode
*
* @param DOMDocument $dom The HTML Document.
* @param DOMElement $node The DOMNode to adjust and replace.
*/
private function create_amp_instagram_and_replace_node( $dom, $node ) {
$instagram_id = $this->get_instagram_id_from_url( $node->getAttribute( 'data-instgrm-permalink' ) );
$node_args = array(
'data-shortcode' => $instagram_id,
'layout' => 'responsive',
'width' => $this->DEFAULT_WIDTH,
'height' => $this->DEFAULT_HEIGHT,
);
if ( true === $node->hasAttribute( 'data-instgrm-captioned' ) ) {
$node_args['data-captioned'] = '';
}
$new_node = AMP_DOM_Utils::create_node( $dom, $this->amp_tag, $node_args );
$this->sanitize_embed_script( $node );
$node->parentNode->replaceChild( $new_node, $node );
$this->did_convert_elements = true;
}
/**
* Removes Instagram's embed <script> tag.
*
* @param DOMElement $node The DOMNode to whose sibling is the instagram script.
*/
private function sanitize_embed_script( $node ) {
$next_element_sibling = $node->nextSibling;
while ( $next_element_sibling && ! ( $next_element_sibling instanceof DOMElement ) ) {
$next_element_sibling = $next_element_sibling->nextSibling;
}
$script_src = 'instagram.com/embed.js';
// Handle case where script is wrapped in paragraph by wpautop.
if ( $next_element_sibling instanceof DOMElement && 'p' === $next_element_sibling->nodeName ) {
$children = $next_element_sibling->getElementsByTagName( '*' );
if ( 1 === $children->length && 'script' === $children->item( 0 )->nodeName && false !== strpos( $children->item( 0 )->getAttribute( 'src' ), $script_src ) ) {
$next_element_sibling->parentNode->removeChild( $next_element_sibling );
return;
}
}
// Handle case where script is immediately following.
$is_embed_script = (
$next_element_sibling
&&
'script' === strtolower( $next_element_sibling->nodeName )
&&
false !== strpos( $next_element_sibling->getAttribute( 'src' ), $script_src )
);
if ( $is_embed_script ) {
$next_element_sibling->parentNode->removeChild( $next_element_sibling );
}
}
}

View File

@ -0,0 +1,65 @@
<?php
/**
* Class AMP_Issuu_Embed_Handler
*
* @package AMP
* @since 0.7
*/
/**
* Class AMP_Issuu_Embed_Handler
*/
class AMP_Issuu_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* Regex matched to produce output amp-iframe.
*
* @const string
*/
const URL_PATTERN = '#https?://(www\.)?issuu\.com/.+/docs/.+#i';
/**
* Register embed.
*/
public function register_embed() {
add_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10, 3 );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
remove_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10 );
}
/**
* Filter oEmbed HTML for Meetup to prepare it for AMP.
*
* @param mixed $return The shortcode callback function to call.
* @param string $url The attempted embed URL.
* @param array $attr An array of shortcode attributes.
* @return string Embed.
*/
public function filter_embed_oembed_html( $return, $url, $attr ) {
$parsed_url = wp_parse_url( $url );
if ( false !== strpos( $parsed_url['host'], 'issuu.com' ) ) {
if ( preg_match( '/width\s*:\s*(\d+)px/', $return, $matches ) ) {
$attr['width'] = $matches[1];
}
if ( preg_match( '/height\s*:\s*(\d+)px/', $return, $matches ) ) {
$attr['height'] = $matches[1];
}
$return = AMP_HTML_Utils::build_tag(
'amp-iframe',
array(
'width' => $attr['width'],
'height' => $attr['height'],
'src' => $url,
'sandbox' => 'allow-scripts allow-same-origin',
)
);
}
return $return;
}
}

View File

@ -0,0 +1,45 @@
<?php
/**
* Class AMP_Meetup_Embed_Handler
*
* @package AMP
* @since 0.7
*/
/**
* Class AMP_Meetup_Embed_Handler
*/
class AMP_Meetup_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* Register embed.
*/
public function register_embed() {
add_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10, 2 );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
remove_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10 );
}
/**
* Filter oEmbed HTML for Meetup to prepare it for AMP.
*
* @param string $cache Cache for oEmbed.
* @param string $url Embed URL.
* @return string Embed.
*/
public function filter_embed_oembed_html( $cache, $url ) {
$parsed_url = wp_parse_url( $url );
if ( false !== strpos( $parsed_url['host'], 'meetup.com' ) ) {
// Supply the width/height so that we don't have to make requests to look them up later.
$cache = str_replace( '<img ', '<img width="50" height="50" ', $cache );
}
return $cache;
}
}

View File

@ -0,0 +1,91 @@
<?php
/**
* Class AMP_Pinterest_Embed_Handler
*
* @package AMP
*/
/**
* Class AMP_Pinterest_Embed_Handler
*/
class AMP_Pinterest_Embed_Handler extends AMP_Base_Embed_Handler {
const URL_PATTERN = '#https?://(www\.)?pinterest\.com/pin/.*#i';
/**
* Default width.
*
* @var int
*/
protected $DEFAULT_WIDTH = 450;
/**
* Default height.
*
* @var int
*/
protected $DEFAULT_HEIGHT = 750;
/**
* Registers embed.
*/
public function register_embed() {
wp_embed_register_handler(
'amp-pinterest',
self::URL_PATTERN,
array( $this, 'oembed' ),
-1
);
}
/**
* Unregisters embed.
*/
public function unregister_embed() {
wp_embed_unregister_handler( 'amp-pinterest', -1 );
}
/**
* WordPress OEmbed rendering callback.
*
* @param array $matches URL pattern matches.
* @param array $attr Matched attributes.
* @param string $url Matched URL.
* @param string $rawattr Raw attributes string.
* @return string HTML markup for rendered embed.
*/
public function oembed( $matches, $attr, $url, $rawattr ) {
return $this->render( array( 'url' => $url ) );
}
/**
* Gets the rendered embed markup.
*
* @param array $args Embed rendering arguments.
* @return string HTML markup for rendered embed.
*/
public function render( $args ) {
$args = wp_parse_args(
$args,
array(
'url' => false,
)
);
if ( empty( $args['url'] ) ) {
return '';
}
$this->did_convert_elements = true;
return AMP_HTML_Utils::build_tag(
'amp-pinterest',
array(
'width' => $this->args['width'],
'height' => $this->args['height'],
'data-do' => 'embedPin',
'data-url' => $args['url'],
)
);
}
}

View File

@ -0,0 +1,324 @@
<?php
/**
* Class AMP_Playlist_Embed_Handler
*
* @package AMP
* @since 0.7
*/
/**
* Class AMP_Playlist_Embed_Handler
*
* Creates AMP-compatible markup for the WordPress 'playlist' shortcode.
*
* @package AMP
*/
class AMP_Playlist_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* The tag of the shortcode.
*
* @var string
*/
const SHORTCODE = 'playlist';
/**
* The default height of the thumbnail image for 'audio' playlist tracks.
*
* @var int
*/
const DEFAULT_THUMB_HEIGHT = 64;
/**
* The default width of the thumbnail image for 'audio' playlist tracks.
*
* @var int
*/
const DEFAULT_THUMB_WIDTH = 48;
/**
* The max width of the audio thumbnail image.
*
* This corresponds to the max-width in wp-mediaelement.css:
* .wp-playlist .wp-playlist-current-item img
*
* @var int
*/
const THUMB_MAX_WIDTH = 60;
/**
* The height of the carousel.
*
* @var int
*/
const CAROUSEL_HEIGHT = 160;
/**
* The pattern to get the playlist data.
*
* @var string
*/
const PLAYLIST_REGEX = ':<script type="application/json" class="wp-playlist-script">(.+?)</script>:s';
/**
* The ID of individual playlist.
*
* @var int
*/
public static $playlist_id = 0;
/**
* The removed shortcode callback.
*
* @var callable
*/
public $removed_shortcode_callback;
/**
* Registers the playlist shortcode.
*
* @global array $shortcode_tags
* @return void
*/
public function register_embed() {
global $shortcode_tags;
if ( shortcode_exists( self::SHORTCODE ) ) {
$this->removed_shortcode_callback = $shortcode_tags[ self::SHORTCODE ];
}
add_shortcode( self::SHORTCODE, array( $this, 'shortcode' ) );
remove_action( 'wp_playlist_scripts', 'wp_playlist_scripts' );
}
/**
* Unregisters the playlist shortcode.
*
* @return void
*/
public function unregister_embed() {
if ( $this->removed_shortcode_callback ) {
add_shortcode( self::SHORTCODE, $this->removed_shortcode_callback );
$this->removed_shortcode_callback = null;
}
add_action( 'wp_playlist_scripts', 'wp_playlist_scripts' );
}
/**
* Enqueues the playlist styling.
*
* @return void
*/
public function enqueue_styles() {
wp_enqueue_style(
'amp-playlist-shortcode',
amp_get_asset_url( 'css/amp-playlist-shortcode.css' ),
array( 'wp-mediaelement' ),
AMP__VERSION
);
}
/**
* Gets AMP-compliant markup for the playlist shortcode.
*
* Uses the JSON that wp_playlist_shortcode() produces.
* Gets the markup, based on the type of playlist.
*
* @param array $attr The playlist attributes.
* @return string Playlist shortcode markup.
*/
public function shortcode( $attr ) {
$data = $this->get_data( $attr );
if ( isset( $data['type'] ) && ( 'audio' === $data['type'] ) ) {
return $this->audio_playlist( $data );
} elseif ( isset( $data['type'] ) && ( 'video' === $data['type'] ) ) {
return $this->video_playlist( $data );
}
}
/**
* Gets an AMP-compliant audio playlist.
*
* @param array $data Data.
* @return string Playlist shortcode markup, or an empty string.
*/
public function audio_playlist( $data ) {
if ( ! isset( $data['tracks'] ) ) {
return '';
}
self::$playlist_id++;
$container_id = 'wpPlaylist' . self::$playlist_id . 'Carousel';
$state_id = 'wpPlaylist' . self::$playlist_id;
$amp_state = array(
'selectedIndex' => 0,
);
$this->enqueue_styles();
ob_start();
?>
<div class="wp-playlist wp-audio-playlist wp-playlist-light">
<amp-state id="<?php echo esc_attr( $state_id ); ?>">
<script type="application/json"><?php echo wp_json_encode( $amp_state ); ?></script>
</amp-state>
<amp-carousel id="<?php echo esc_attr( $container_id ); ?>" [slide]="<?php echo esc_attr( $state_id . '.selectedIndex' ); ?>" height="<?php echo esc_attr( self::CAROUSEL_HEIGHT ); ?>" width="auto" type="slides">
<?php
foreach ( $data['tracks'] as $track ) :
$title = $this->get_title( $track );
$image_url = isset( $track['thumb']['src'] ) ? $track['thumb']['src'] : '';
$dimensions = $this->get_thumb_dimensions( $track );
?>
<div>
<div class="wp-playlist-current-item">
<?php if ( $image_url ) : ?>
<amp-img src="<?php echo esc_url( $image_url ); ?>" height="<?php echo esc_attr( $dimensions['height'] ); ?>" width="<?php echo esc_attr( $dimensions['width'] ); ?>"></amp-img>
<?php endif; ?>
<div class="wp-playlist-caption">
<span class="wp-playlist-item-meta wp-playlist-item-title"><?php echo esc_html( $title ); ?></span>
</div>
</div>
<amp-audio width="auto" height="50" src="<?php echo esc_url( $track['src'] ); ?>"></amp-audio>
</div>
<?php endforeach; ?>
</amp-carousel>
<?php $this->print_tracks( $state_id, $data['tracks'] ); ?>
</div>
<?php
return ob_get_clean();
}
/**
* Gets an AMP-compliant video playlist.
*
* This uses similar markup to the native playlist shortcode output.
* So the styles from wp-mediaelement.min.css will apply to it.
*
* @global int $content_width
* @param array $data Data.
* @return string $video_playlist Markup for the video playlist.
*/
public function video_playlist( $data ) {
global $content_width;
if ( ! isset( $data['tracks'], $data['tracks'][0]['src'] ) ) {
return '';
}
self::$playlist_id++;
$state_id = 'wpPlaylist' . self::$playlist_id;
$amp_state = array(
'selectedIndex' => 0,
);
foreach ( $data['tracks'] as $index => $track ) {
$amp_state[ $index ] = array(
'videoUrl' => $track['src'],
'thumb' => isset( $track['thumb']['src'] ) ? $track['thumb']['src'] : '',
);
}
$dimensions = isset( $data['tracks'][0]['dimensions']['resized'] ) ? $data['tracks'][0]['dimensions']['resized'] : null;
$width = isset( $dimensions['width'] ) ? $dimensions['width'] : $content_width;
$height = isset( $dimensions['height'] ) ? $dimensions['height'] : null;
$src_bound = sprintf( '%s[%s.selectedIndex].videoUrl', $state_id, $state_id );
$this->enqueue_styles();
ob_start();
?>
<div class="wp-playlist wp-video-playlist wp-playlist-light">
<amp-state id="<?php echo esc_attr( $state_id ); ?>">
<script type="application/json"><?php echo wp_json_encode( $amp_state ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></script>
</amp-state>
<amp-video id="amp-video" src="<?php echo esc_url( $data['tracks'][0]['src'] ); ?>" [src]="<?php echo esc_attr( $src_bound ); ?>" width="<?php echo esc_attr( $width ); ?>" height="<?php echo esc_attr( $height ); ?>" controls></amp-video>
<?php $this->print_tracks( $state_id, $data['tracks'] ); ?>
</div>
<?php
return ob_get_clean(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Gets the thumbnail image dimensions, including height and width.
*
* If the width is higher than the maximum width,
* reduces it to the maximum width.
* And it proportionally reduces the height.
*
* @param array $track The data for the track.
* @return array {
* Dimensions.
*
* @type int $height Image height.
* @type int $width Image width.
* }
*/
public function get_thumb_dimensions( $track ) {
$original_height = isset( $track['thumb']['height'] ) ? intval( $track['thumb']['height'] ) : self::DEFAULT_THUMB_HEIGHT;
$original_width = isset( $track['thumb']['width'] ) ? intval( $track['thumb']['width'] ) : self::DEFAULT_THUMB_WIDTH;
if ( $original_width > self::THUMB_MAX_WIDTH ) {
$ratio = $original_width / self::THUMB_MAX_WIDTH;
$height = intval( $original_height / $ratio );
} else {
$height = $original_height;
}
$width = min( self::THUMB_MAX_WIDTH, $original_width );
return compact( 'height', 'width' );
}
/**
* Outputs the playlist tracks, based on the type of playlist.
*
* These typically appear below the player.
* Clicking a track triggers the player to appear with its src.
*
* @param string $state_id The ID of the container.
* @param array $tracks Tracks.
* @return void
*/
public function print_tracks( $state_id, $tracks ) {
?>
<div class="wp-playlist-tracks">
<?php foreach ( $tracks as $index => $track ) : ?>
<?php
$on = 'tap:AMP.setState(' . wp_json_encode( array( $state_id => array( 'selectedIndex' => $index ) ) ) . ')';
$initial_class = 0 === $index ? 'wp-playlist-item wp-playlist-playing' : 'wp-playlist-item';
$bound_class = sprintf( '%d == %s.selectedIndex ? "wp-playlist-item wp-playlist-playing" : "wp-playlist-item"', $index, $state_id );
?>
<div class="<?php echo esc_attr( $initial_class ); ?>" [class]="<?php echo esc_attr( $bound_class ); ?>" >
<a class="wp-playlist-caption" on="<?php echo esc_attr( $on ); ?>">
<?php echo esc_html( strval( $index + 1 ) . '.' ); ?> <span class="wp-playlist-item-title"><?php echo esc_html( $this->get_title( $track ) ); ?></span>
</a>
<?php if ( isset( $track['meta']['length_formatted'] ) ) : ?>
<div class="wp-playlist-item-length"><?php echo esc_html( $track['meta']['length_formatted'] ); ?></div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php
}
/**
* Gets the data for the playlist.
*
* @see wp_playlist_shortcode()
* @param array $attr The shortcode attributes.
* @return array $data The data for the playlist.
*/
public function get_data( $attr ) {
$markup = wp_playlist_shortcode( $attr );
preg_match( self::PLAYLIST_REGEX, $markup, $matches );
if ( empty( $matches[1] ) ) {
return array();
}
return json_decode( $matches[1], true );
}
/**
* Gets the title for the track.
*
* @param array $track The track data.
* @return string $title The title of the track.
*/
public function get_title( $track ) {
if ( ! empty( $track['caption'] ) ) {
return $track['caption'];
} elseif ( ! empty( $track['title'] ) ) {
return $track['title'];
}
return '';
}
}

View File

@ -0,0 +1,77 @@
<?php
/**
* Class AMP_Reddit_Embed_Handler
*
* @package AMP
* @since 0.7
*/
/**
* Class AMP_Reddit_Embed_Handler
*/
class AMP_Reddit_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* Regex matched to produce output amp-reddit.
*
* @const string
*/
const URL_PATTERN = '#https?://(www\.)?reddit\.com/r/[^/]+/comments/.*#i';
/**
* Register embed.
*/
public function register_embed() {
wp_embed_register_handler( 'amp-reddit', self::URL_PATTERN, array( $this, 'oembed' ), -1 );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
wp_embed_unregister_handler( 'amp-reddit', -1 );
}
/**
* Embed found with matching URL callback.
*
* @param array $matches URL regex matches.
* @param array $attr Additional parameters.
* @param array $url URL.
* @return string Embed.
*/
public function oembed( $matches, $attr, $url ) {
return $this->render( array( 'url' => $url ) );
}
/**
* Output the Reddit amp element.
*
* @param array $args parameters used for output.
* @return string Rendered content.
*/
public function render( $args ) {
$args = wp_parse_args(
$args,
array(
'url' => false,
)
);
if ( empty( $args['url'] ) ) {
return '';
}
// @todo Sizing is not yet correct. See <https://github.com/ampproject/amphtml/issues/11869>.
return AMP_HTML_Utils::build_tag(
'amp-reddit',
array(
'layout' => 'responsive',
'data-embedtype' => 'post',
'width' => '100',
'height' => '100',
'data-src' => $args['url'],
)
);
}
}

View File

@ -0,0 +1,193 @@
<?php
/**
* Class AMP_SoundCloud_Embed_Handler
*
* @package AMP
*/
/**
* Class AMP_SoundCloud_Embed_Handler
*
* @since 0.5
*/
class AMP_SoundCloud_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* Default height.
*
* @var int
*/
protected $DEFAULT_HEIGHT = 200;
/**
* Register embed.
*/
public function register_embed() {
add_shortcode( 'soundcloud', array( $this, 'shortcode' ) );
add_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10, 2 );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
remove_shortcode( 'soundcloud' );
remove_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10 );
}
/**
* Render oEmbed.
*
* @see \WP_Embed::shortcode()
*
* @deprecated Core's oEmbed handler is now used instead, with embed_oembed_html filter used to convert to AMP.
* @param array $matches URL pattern matches.
* @param array $attr Shortcode attribues.
* @param string $url URL.
* @return string Rendered oEmbed.
*/
public function oembed( $matches, $attr, $url ) {
_deprecated_function( __METHOD__, '0.6' );
unset( $matches, $attr );
$track_id = $this->get_track_id_from_url( $url );
return $this->render( compact( 'track_id' ) );
}
/**
* Filter oEmbed HTML for SoundCloud to convert to AMP.
*
* @param string $cache Cache for oEmbed.
* @param string $url Embed URL.
* @return string Embed.
*/
public function filter_embed_oembed_html( $cache, $url ) {
$parsed_url = wp_parse_url( $url );
if ( false === strpos( $parsed_url['host'], 'soundcloud.com' ) ) {
return $cache;
}
return $this->parse_amp_component_from_iframe( $cache );
}
/**
* Parse AMP component from iframe.
*
* @param string $html HTML.
* @return string AMP component or empty if unable to determine SoundCloud ID.
*/
private function parse_amp_component_from_iframe( $html ) {
$embed = '';
if ( preg_match( '#<iframe.+?src="(?P<url>.+?)".*>#', $html, $matches ) ) {
$src = html_entity_decode( $matches['url'], ENT_QUOTES );
$query = array();
parse_str( wp_parse_url( $src, PHP_URL_QUERY ), $query );
if ( ! empty( $query['url'] ) ) {
$embed = $this->render(
array(
'track_id' => $this->get_track_id_from_url( $query['url'] ),
)
);
}
}
return $embed;
}
/**
* Render shortcode.
*
* @param array $attr Shortcode attributes.
* @param string $content Shortcode content.
* @return string Rendered shortcode.
*/
public function shortcode( $attr, $content = null ) {
$output = '';
if ( function_exists( 'soundcloud_shortcode' ) ) {
if ( empty( $attr['url'] ) && ! empty( $attr['id'] ) ) {
$attr['url'] = 'https://api.soundcloud.com/tracks/' . intval( $attr['id'] );
}
$output = soundcloud_shortcode( $attr, $content );
$output = $this->parse_amp_component_from_iframe( $output );
} else {
$url = null;
if ( isset( $attr['id'] ) ) {
$url = 'https://w.soundcloud.com/player/?url=https%3A%2F%2Fapi.soundcloud.com%2Ftracks%2F' . intval( $attr['id'] );
}
if ( isset( $attr['url'] ) ) {
$url = $attr['url'];
} elseif ( isset( $attr[0] ) ) {
$url = $attr[0];
} elseif ( function_exists( 'shortcode_new_to_old_params' ) ) {
$url = shortcode_new_to_old_params( $attr );
}
if ( $url ) {
$output = $this->render_embed_fallback( $url );
}
}
return $output;
}
/**
* Render embed.
*
* @param array $args Args.
* @return string Rendered embed.
* @global WP_Embed $wp_embed
*/
public function render( $args ) {
$args = wp_parse_args(
$args,
array(
'track_id' => false,
'url' => null,
)
);
if ( empty( $args['track_id'] ) ) {
return $this->render_embed_fallback( $args['url'] );
}
$this->did_convert_elements = true;
return AMP_HTML_Utils::build_tag(
'amp-soundcloud',
array(
'data-trackid' => $args['track_id'],
'layout' => 'fixed-height',
'height' => $this->args['height'],
)
);
}
/**
* Render embed fallback.
*
* @param string $url URL.
* @returns string
*/
private function render_embed_fallback( $url ) {
return AMP_HTML_Utils::build_tag(
'a',
array(
'href' => esc_url( $url ),
'class' => 'amp-wp-embed-fallback',
),
esc_html( $url )
);
}
/**
* Get track_id from URL.
*
* @param string $url URL.
*
* @return string Track ID or empty string if none matched.
*/
private function get_track_id_from_url( $url ) {
$parsed_url = wp_parse_url( $url );
if ( ! preg_match( '#tracks/(?P<track_id>[^/]+)#', $parsed_url['path'], $matches ) ) {
return '';
}
return $matches['track_id'];
}
}

View File

@ -0,0 +1,60 @@
<?php
/**
* Class AMP_Tumblr_Embed_Handler
*
* @package AMP
* @since 0.7
*/
/**
* Class AMP_Tumblr_Embed_Handler
*/
class AMP_Tumblr_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* Register embed.
*/
public function register_embed() {
add_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10, 2 );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
remove_filter( 'embed_oembed_html', array( $this, 'filter_embed_oembed_html' ), 10 );
}
/**
* Filter oEmbed HTML for Tumblr to prepare it for AMP.
*
* @param string $cache Cache for oEmbed.
* @param string $url Embed URL.
* @return string Embed.
*/
public function filter_embed_oembed_html( $cache, $url ) {
$parsed_url = wp_parse_url( $url );
if ( false === strpos( $parsed_url['host'], 'tumblr.com' ) ) {
return $cache;
}
// @todo The iframe will not get sized properly.
if ( preg_match( '#data-href="(?P<href>https://embed.tumblr.com/embed/post/\w+/\w+)"#', $cache, $matches ) ) {
$cache = AMP_HTML_Utils::build_tag(
'amp-iframe',
array(
'width' => $this->args['width'],
'height' => $this->args['height'],
'layout' => 'responsive',
'sandbox' => 'allow-scripts allow-popups', // The allow-scripts is needed to allow the iframe to render; allow-popups needed to allow clicking.
'src' => $matches['href'],
),
sprintf( '<a placeholder href="%s">Tumblr</a>', $url )
);
}
return $cache;
}
}

View File

@ -0,0 +1,313 @@
<?php
/**
* Class AMP_Twitter_Embed_Handler
*
* @package AMP
*/
/**
* Class AMP_Twitter_Embed_Handler
*
* Much of this class is borrowed from Jetpack embeds
*/
class AMP_Twitter_Embed_Handler extends AMP_Base_Embed_Handler {
/**
* URL pattern for a Tweet URL.
*
* @since 0.2
* @var string
*/
const URL_PATTERN = '#https?:\/\/twitter\.com(?:\/\#\!\/|\/)(?P<username>[a-zA-Z0-9_]{1,20})\/status(?:es)?\/(?P<tweet>\d+)#i';
/**
* URL pattern for a Twitter timeline.
*
* @since 1.0
* @var string
*/
const URL_PATTERN_TIMELINE = '#https?:\/\/twitter\.com(?:\/\#\!\/|\/)(?P<username>[a-zA-Z0-9_]{1,20})(?:$|\/(?P<type>likes|lists)(\/(?P<id>[a-zA-Z0-9_-]+))?)#i';
/**
* Tag.
*
* @var string embed HTML blockquote tag to identify and replace with AMP version.
*/
protected $sanitize_tag = 'blockquote';
/**
* Tag.
*
* @var string AMP amp-facebook tag
*/
private $amp_tag = 'amp-twitter';
/**
* Registers embed.
*/
public function register_embed() {
add_shortcode( 'tweet', array( $this, 'shortcode' ) ); // Note: This is a Jetpack shortcode.
wp_embed_register_handler( 'amp-twitter', self::URL_PATTERN, array( $this, 'oembed' ), -1 );
wp_embed_register_handler( 'amp-twitter-timeline', self::URL_PATTERN_TIMELINE, array( $this, 'oembed_timeline' ), -1 );
}
/**
* Unregisters embed.
*/
public function unregister_embed() {
remove_shortcode( 'tweet' ); // Note: This is a Jetpack shortcode.
wp_embed_unregister_handler( 'amp-twitter', -1 );
wp_embed_unregister_handler( 'amp-twitter-timeline', -1 );
}
/**
* Gets AMP-compliant markup for the Twitter shortcode.
*
* Note that this shortcode is is defined in Jetpack.
*
* @param array $attr The Twitter attributes.
* @return string Twitter shortcode markup.
*/
public function shortcode( $attr ) {
$attr = wp_parse_args(
$attr,
array(
'tweet' => false,
)
);
if ( empty( $attr['tweet'] ) && ! empty( $attr[0] ) ) {
$attr['tweet'] = $attr[0];
}
$id = false;
if ( is_numeric( $attr['tweet'] ) ) {
$id = $attr['tweet'];
} else {
preg_match( self::URL_PATTERN, $attr['tweet'], $matches );
if ( isset( $matches['tweet'] ) && is_numeric( $matches['tweet'] ) ) {
$id = $matches['tweet'];
}
if ( empty( $id ) ) {
return '';
}
}
$this->did_convert_elements = true;
return AMP_HTML_Utils::build_tag(
$this->amp_tag,
array(
'data-tweetid' => $id,
'layout' => 'responsive',
'width' => $this->args['width'],
'height' => $this->args['height'],
)
);
}
/**
* Render oEmbed.
*
* @see \WP_Embed::shortcode()
*
* @param array $matches URL pattern matches.
* @return string Rendered oEmbed.
*/
public function oembed( $matches ) {
$id = false;
if ( isset( $matches['tweet'] ) && is_numeric( $matches['tweet'] ) ) {
$id = $matches['tweet'];
}
if ( ! $id ) {
return '';
}
return $this->shortcode( array( 'tweet' => $id ) );
}
/**
* Render oEmbed for a timeline.
*
* @since 1.0
*
* @param array $matches URL pattern matches.
* @return string Rendered oEmbed.
*/
public function oembed_timeline( $matches ) {
if ( ! isset( $matches['username'] ) ) {
return '';
}
$attributes = array(
'data-timeline-source-type' => 'profile',
'data-timeline-screen-name' => $matches['username'],
);
if ( isset( $matches['type'] ) ) {
switch ( $matches['type'] ) {
case 'likes':
$attributes['data-timeline-source-type'] = 'likes';
break;
case 'lists':
if ( ! isset( $matches['id'] ) ) {
return '';
}
$attributes['data-timeline-source-type'] = 'list';
$attributes['data-timeline-slug'] = $matches['id'];
$attributes['data-timeline-owner-screen-name'] = $attributes['data-timeline-screen-name'];
unset( $attributes['data-timeline-screen-name'] );
break;
default:
return '';
}
}
$attributes['layout'] = 'responsive';
$attributes['width'] = $this->args['width'];
$attributes['height'] = $this->args['height'];
$this->did_convert_elements = true;
return AMP_HTML_Utils::build_tag( $this->amp_tag, $attributes );
}
/**
* Sanitized <blockquote class="twitter-tweet"> tags to <amp-twitter>.
*
* @param DOMDocument $dom DOM.
*/
public function sanitize_raw_embeds( $dom ) {
/**
* Node list.
*
* @var DOMNodeList $node
*/
$nodes = $dom->getElementsByTagName( $this->sanitize_tag );
$num_nodes = $nodes->length;
if ( 0 === $num_nodes ) {
return;
}
for ( $i = $num_nodes - 1; $i >= 0; $i-- ) {
$node = $nodes->item( $i );
if ( ! $node instanceof DOMElement ) {
continue;
}
if ( $this->is_tweet_raw_embed( $node ) ) {
$this->create_amp_twitter_and_replace_node( $dom, $node );
}
}
}
/**
* Checks whether it's a twitter blockquote or not
*
* @param DOMElement $node The DOMNode to adjust and replace.
* @return bool Whether node is for raw embed.
*/
private function is_tweet_raw_embed( $node ) {
$class_attr = $node->getAttribute( 'class' );
return null !== $class_attr && false !== strpos( $class_attr, 'twitter-tweet' );
}
/**
* Make final modifications to DOMNode
*
* @param DOMDocument $dom The HTML Document.
* @param DOMElement $node The DOMNode to adjust and replace.
*/
private function create_amp_twitter_and_replace_node( $dom, $node ) {
$tweet_id = $this->get_tweet_id( $node );
if ( ! $tweet_id ) {
return;
}
$new_node = AMP_DOM_Utils::create_node(
$dom,
$this->amp_tag,
array(
'width' => $this->DEFAULT_WIDTH,
'height' => $this->DEFAULT_HEIGHT,
'layout' => 'responsive',
'data-tweetid' => $tweet_id,
)
);
$this->sanitize_embed_script( $node );
$node->parentNode->replaceChild( $new_node, $node );
$this->did_convert_elements = true;
}
/**
* Extracts Tweet id.
*
* @param DOMElement $node The DOMNode to adjust and replace.
* @return string Tweet ID.
*/
private function get_tweet_id( $node ) {
/**
* DOMNode
*
* @var DOMNodeList $anchors
*/
$anchors = $node->getElementsByTagName( 'a' );
/**
* Anchor.
*
* @type DOMElement $anchor
*/
foreach ( $anchors as $anchor ) {
$found = preg_match( self::URL_PATTERN, $anchor->getAttribute( 'href' ), $matches );
if ( $found ) {
return $matches['tweet'];
}
}
return null;
}
/**
* Removes Twitter's embed <script> tag.
*
* @param DOMElement $node The DOMNode to whose sibling is the instagram script.
*/
private function sanitize_embed_script( $node ) {
$next_element_sibling = $node->nextSibling;
while ( $next_element_sibling && ! ( $next_element_sibling instanceof DOMElement ) ) {
$next_element_sibling = $next_element_sibling->nextSibling;
}
$script_src = 'platform.twitter.com/widgets.js';
// Handle case where script is wrapped in paragraph by wpautop.
if ( $next_element_sibling instanceof DOMElement && 'p' === $next_element_sibling->nodeName ) {
$children = $next_element_sibling->getElementsByTagName( '*' );
if ( 1 === $children->length && 'script' === $children->item( 0 )->nodeName && false !== strpos( $children->item( 0 )->getAttribute( 'src' ), $script_src ) ) {
$next_element_sibling->parentNode->removeChild( $next_element_sibling );
return;
}
}
// Handle case where script is immediately following.
$is_embed_script = (
$next_element_sibling
&&
'script' === strtolower( $next_element_sibling->nodeName )
&&
false !== strpos( $next_element_sibling->getAttribute( 'src' ), $script_src )
);
if ( $is_embed_script ) {
$next_element_sibling->parentNode->removeChild( $next_element_sibling );
}
}
}

View File

@ -0,0 +1,196 @@
<?php
/**
* Class AMP_Vimeo_Embed_Handler
*
* @package AMP
*/
/**
* Class AMP_Vimeo_Embed_Handler
*
* Much of this class is borrowed from Jetpack embeds
*/
class AMP_Vimeo_Embed_Handler extends AMP_Base_Embed_Handler {
const URL_PATTERN = '#https?:\/\/(www\.)?vimeo\.com\/.*#i';
const RATIO = 0.5625;
/**
* Default width.
*
* @var int
*/
protected $DEFAULT_WIDTH = 600;
/**
* Default height.
*
* @var int
*/
protected $DEFAULT_HEIGHT = 338;
/**
* AMP_Vimeo_Embed_Handler constructor.
*
* @param array $args Height, width and maximum width for embed.
*/
public function __construct( $args = array() ) {
parent::__construct( $args );
if ( isset( $this->args['content_max_width'] ) ) {
$max_width = $this->args['content_max_width'];
$this->args['width'] = $max_width;
$this->args['height'] = round( $max_width * self::RATIO );
}
}
/**
* Register embed.
*/
public function register_embed() {
wp_embed_register_handler( 'amp-vimeo', self::URL_PATTERN, array( $this, 'oembed' ), -1 );
add_shortcode( 'vimeo', array( $this, 'shortcode' ) );
add_filter( 'wp_video_shortcode_override', array( $this, 'video_override' ), 10, 2 );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
wp_embed_unregister_handler( 'amp-vimeo', -1 );
remove_shortcode( 'vimeo' );
}
/**
* Gets AMP-compliant markup for the Vimeo shortcode.
*
* @param array $attr The Vimeo attributes.
* @return string Vimeo shortcode markup.
*/
public function shortcode( $attr ) {
$video_id = false;
if ( isset( $attr['id'] ) ) {
$video_id = $attr['id'];
} elseif ( isset( $attr['url'] ) ) {
$video_id = $this->get_video_id_from_url( $attr['url'] );
} elseif ( isset( $attr[0] ) ) {
$video_id = $this->get_video_id_from_url( $attr[0] );
} elseif ( function_exists( 'shortcode_new_to_old_params' ) ) {
$video_id = shortcode_new_to_old_params( $attr );
}
if ( empty( $video_id ) ) {
return '';
}
return $this->render(
array(
'video_id' => $video_id,
)
);
}
/**
* Render oEmbed.
*
* @see \WP_Embed::shortcode()
*
* @param array $matches URL pattern matches.
* @param array $attr Shortcode attribues.
* @param string $url URL.
* @param string $rawattr Unmodified shortcode attributes.
* @return string Rendered oEmbed.
*/
public function oembed( $matches, $attr, $url, $rawattr ) {
$video_id = $this->get_video_id_from_url( $url );
return $this->render(
array(
'url' => $url,
'video_id' => $video_id,
)
);
}
/**
* Render.
*
* @param array $args Args.
* @return string Rendered.
*/
public function render( $args ) {
$args = wp_parse_args(
$args,
array(
'video_id' => false,
)
);
if ( empty( $args['video_id'] ) ) {
return AMP_HTML_Utils::build_tag(
'a',
array(
'href' => esc_url( $args['url'] ),
'class' => 'amp-wp-embed-fallback',
),
esc_html( $args['url'] )
);
}
$this->did_convert_elements = true;
return AMP_HTML_Utils::build_tag(
'amp-vimeo',
array(
'data-videoid' => $args['video_id'],
'layout' => 'responsive',
'width' => $this->args['width'],
'height' => $this->args['height'],
)
);
}
/**
* Determine the video ID from the URL.
*
* @param string $url URL.
* @return integer Video ID.
*/
private function get_video_id_from_url( $url ) {
$parsed_url = wp_parse_url( $url );
parse_str( $parsed_url['path'], $path );
$video_id = '';
if ( $path ) {
$tok = explode( '/', $parsed_url['path'] );
$video_id = end( $tok );
}
return $video_id;
}
/**
* Override the output of Vimeo videos.
*
* This overrides the value in wp_video_shortcode().
* The pattern matching is copied from WP_Widget_Media_Video::render().
*
* @param string $html Empty variable to be replaced with shortcode markup.
* @param array $attr The shortcode attributes.
* @return string|null $markup The markup to output.
*/
public function video_override( $html, $attr ) {
if ( ! isset( $attr['src'] ) ) {
return $html;
}
$src = $attr['src'];
$vimeo_pattern = '#^https?://(.+\.)?vimeo\.com/.*#';
if ( 1 === preg_match( $vimeo_pattern, $src ) ) {
return $this->shortcode( array( $src ) );
}
return $html;
}
}

View File

@ -0,0 +1,98 @@
<?php
/**
* Class AMP_Vine_Embed_Handler
*
* @package AMP
*/
/**
* Class AMP_Vine_Embed_Handler
*/
class AMP_Vine_Embed_Handler extends AMP_Base_Embed_Handler {
const URL_PATTERN = '#https?://vine\.co/v/([^/?]+)#i';
/**
* Default width.
*
* @var int
*/
protected $DEFAULT_WIDTH = 400;
/**
* Default height.
*
* @var int
*/
protected $DEFAULT_HEIGHT = 400;
/**
* Registers embed.
*/
public function register_embed() {
wp_embed_register_handler( 'amp-vine', self::URL_PATTERN, array( $this, 'oembed' ), -1 );
}
/**
* Unregisters embed.
*/
public function unregister_embed() {
wp_embed_unregister_handler( 'amp-vine', -1 );
}
/**
* WordPress OEmbed rendering callback.
*
* @param array $matches URL pattern matches.
* @param array $attr Matched attributes.
* @param string $url Matched URL.
* @param string $rawattr Raw attributes string.
* @return string HTML markup for rendered embed.
*/
public function oembed( $matches, $attr, $url, $rawattr ) {
return $this->render(
array(
'url' => $url,
'vine_id' => end( $matches ),
)
);
}
/**
* Gets the rendered embed markup.
*
* @param array $args Embed rendering arguments.
* @return string HTML markup for rendered embed.
*/
public function render( $args ) {
$args = wp_parse_args(
$args,
array(
'url' => false,
'vine_id' => false,
)
);
if ( empty( $args['vine_id'] ) ) {
return AMP_HTML_Utils::build_tag(
'a',
array(
'href' => esc_url( $args['url'] ),
'class' => 'amp-wp-embed-fallback',
),
esc_html( $args['url'] )
);
}
$this->did_convert_elements = true;
return AMP_HTML_Utils::build_tag(
'amp-vine',
array(
'data-vineid' => $args['vine_id'],
'layout' => 'responsive',
'width' => $this->args['width'],
'height' => $this->args['height'],
)
);
}
}

View File

@ -0,0 +1,222 @@
<?php
/**
* Class AMP_YouTube_Embed_Handler
*
* @package AMP
*/
/**
* Class AMP_YouTube_Embed_Handler
*
* Much of this class is borrowed from Jetpack embeds.
*/
class AMP_YouTube_Embed_Handler extends AMP_Base_Embed_Handler {
const SHORT_URL_HOST = 'youtu.be';
// Only handling single videos. Playlists are handled elsewhere.
const URL_PATTERN = '#https?://(?:www\.)?(?:youtube.com/(?:v/|e/|embed/|watch[/\#?])|youtu\.be/).*#i';
const RATIO = 0.5625;
/**
* Default width.
*
* @var int
*/
protected $DEFAULT_WIDTH = 600;
/**
* Default height.
*
* @var int
*/
protected $DEFAULT_HEIGHT = 338;
/**
* AMP_YouTube_Embed_Handler constructor.
*
* @param array $args Height, width and maximum width for embed.
*/
public function __construct( $args = array() ) {
parent::__construct( $args );
if ( isset( $this->args['content_max_width'] ) ) {
$max_width = $this->args['content_max_width'];
$this->args['width'] = $max_width;
$this->args['height'] = round( $max_width * self::RATIO );
}
}
/**
* Register embed.
*/
public function register_embed() {
wp_embed_register_handler( 'amp-youtube', self::URL_PATTERN, array( $this, 'oembed' ), -1 );
add_shortcode( 'youtube', array( $this, 'shortcode' ) );
add_filter( 'wp_video_shortcode_override', array( $this, 'video_override' ), 10, 2 );
}
/**
* Unregister embed.
*/
public function unregister_embed() {
wp_embed_unregister_handler( 'amp-youtube', -1 );
remove_shortcode( 'youtube' );
}
/**
* Gets AMP-compliant markup for the YouTube shortcode.
*
* @param array $attr The YouTube attributes.
* @return string YouTube shortcode markup.
*/
public function shortcode( $attr ) {
$url = false;
$video_id = false;
if ( isset( $attr[0] ) ) {
$url = ltrim( $attr[0], '=' );
} elseif ( function_exists( 'shortcode_new_to_old_params' ) ) {
$url = shortcode_new_to_old_params( $attr );
}
if ( empty( $url ) ) {
return '';
}
$video_id = $this->get_video_id_from_url( $url );
return $this->render(
array(
'url' => $url,
'video_id' => $video_id,
)
);
}
/**
* Render oEmbed.
*
* @see \WP_Embed::shortcode()
*
* @param array $matches URL pattern matches.
* @param array $attr Shortcode attribues.
* @param string $url URL.
* @param string $rawattr Unmodified shortcode attributes.
* @return string Rendered oEmbed.
*/
public function oembed( $matches, $attr, $url, $rawattr ) {
return $this->shortcode( array( $url ) );
}
/**
* Render.
*
* @param array $args Args.
* @return string Rendered.
*/
public function render( $args ) {
$args = wp_parse_args(
$args,
array(
'video_id' => false,
)
);
if ( empty( $args['video_id'] ) ) {
return AMP_HTML_Utils::build_tag(
'a',
array(
'href' => esc_url( $args['url'] ),
'class' => 'amp-wp-embed-fallback',
),
esc_html( $args['url'] )
);
}
$this->did_convert_elements = true;
return AMP_HTML_Utils::build_tag(
'amp-youtube',
array(
'data-videoid' => $args['video_id'],
'layout' => 'responsive',
'width' => $this->args['width'],
'height' => $this->args['height'],
)
);
}
/**
* Determine the video ID from the URL.
*
* @param string $url URL.
* @return integer Video ID.
*/
private function get_video_id_from_url( $url ) {
$video_id = false;
$parsed_url = wp_parse_url( $url );
if ( self::SHORT_URL_HOST === substr( $parsed_url['host'], -strlen( self::SHORT_URL_HOST ) ) ) {
/* youtu.be/{id} */
$parts = explode( '/', $parsed_url['path'] );
if ( ! empty( $parts ) ) {
$video_id = $parts[1];
}
} else {
/* phpcs:ignore Squiz.PHP.CommentedOutCode.Found
The query looks like ?v={id} or ?list={id} */
parse_str( $parsed_url['query'], $query_args );
if ( isset( $query_args['v'] ) ) {
$video_id = $this->sanitize_v_arg( $query_args['v'] );
}
}
if ( empty( $video_id ) ) {
/* The path looks like /(v|e|embed)/{id} */
$parts = explode( '/', $parsed_url['path'] );
if ( in_array( $parts[1], array( 'v', 'e', 'embed' ), true ) ) {
$video_id = $parts[2];
}
}
return $video_id;
}
/**
* Sanitize the v= argument in the URL.
*
* @param string $value query parameters.
* @return string First set of query parameters.
*/
private function sanitize_v_arg( $value ) {
// Deal with broken params like `?v=123?rel=0`.
if ( false !== strpos( $value, '?' ) ) {
$value = strtok( $value, '?' );
}
return $value;
}
/**
* Override the output of YouTube videos.
*
* This overrides the value in wp_video_shortcode().
* The pattern matching is copied from WP_Widget_Media_Video::render().
*
* @param string $html Empty variable to be replaced with shortcode markup.
* @param array $attr The shortcode attributes.
* @return string|null $markup The markup to output.
*/
public function video_override( $html, $attr ) {
if ( ! isset( $attr['src'] ) ) {
return $html;
}
$src = $attr['src'];
$youtube_pattern = '#^https?://(?:www\.)?(?:youtube\.com/watch|youtu\.be/)#';
if ( 1 === preg_match( $youtube_pattern, $src ) ) {
return $this->shortcode( array( $src ) );
}
return $html;
}
}

View File

@ -0,0 +1,92 @@
<?php
/**
* Class AMP_Analytics_Options_Submenu
*
* @package AMP
*/
/**
* Class AMP_Analytics_Options_Submenu
*/
class AMP_Analytics_Options_Submenu {
/**
* Parent menu slug for the submenu.
*
* @var string
*/
private $parent_menu_slug;
/**
* Slug for the submenu.
*
* @var string
*/
private $menu_slug;
/**
* Menu page instance for rendering the content.
*
* @var AMP_Analytics_Options_Submenu_Page
*/
private $menu_page;
/**
* Constructor.
*
* @param string $parent_menu_slug Slug of the parent menu item.
*/
public function __construct( $parent_menu_slug ) {
$this->parent_menu_slug = $parent_menu_slug;
$this->menu_slug = 'amp-analytics-options';
$this->menu_page = new AMP_Analytics_Options_Submenu_Page();
}
/**
* Adds the submenu item and adds necessary hooks.
*/
public function init() {
$this->add_submenu();
add_action(
'admin_print_styles-amp_page_' . $this->menu_slug,
array( $this, 'amp_options_styles' )
);
}
/**
* Adds the submenu page and registers the rendering callback.
*/
private function add_submenu() {
add_submenu_page(
$this->parent_menu_slug,
__( 'AMP Analytics Options', 'amp' ),
__( 'Analytics', 'amp' ),
'manage_options',
$this->menu_slug,
array( $this->menu_page, 'render' )
);
}
/**
* Prints extra styles for the page content.
*/
public function amp_options_styles() {
?>
<style>
.analytics-data-container .button.delete,
.analytics-data-container .button.delete:hover,
.analytics-data-container .button.delete:active,
.analytics-data-container .button.delete:focus {
background: red;
border-color: red;
text-shadow: 0 0 0;
margin: 0 5px;
}
.amp-analytics-options.notice {
width: 300px;
}
</style>
<?php
}
}

View File

@ -0,0 +1,665 @@
<?php
/**
* Class AMP_Options_Manager.
*
* @package AMP
*/
/**
* Class AMP_Options_Manager
*/
class AMP_Options_Manager {
/**
* Option name.
*
* @var string
*/
const OPTION_NAME = 'amp-options';
/**
* Default option values.
*
* @var array
*/
protected static $defaults = array(
'theme_support' => 'disabled',
'supported_post_types' => array( 'post' ),
'analytics' => array(),
'auto_accept_sanitization' => true,
'accept_tree_shaking' => true,
'disable_admin_bar' => false,
'all_templates_supported' => true,
'supported_templates' => array( 'is_singular' ),
'enable_response_caching' => true,
'version' => AMP__VERSION,
);
/**
* Register settings.
*/
public static function register_settings() {
register_setting(
self::OPTION_NAME,
self::OPTION_NAME,
array(
'type' => 'array',
'sanitize_callback' => array( __CLASS__, 'validate_options' ),
)
);
add_action( 'update_option_' . self::OPTION_NAME, array( __CLASS__, 'maybe_flush_rewrite_rules' ), 10, 2 );
add_action( 'admin_notices', array( __CLASS__, 'render_welcome_notice' ) );
add_action( 'admin_notices', array( __CLASS__, 'persistent_object_caching_notice' ) );
add_action( 'admin_notices', array( __CLASS__, 'render_cache_miss_notice' ) );
add_action( 'admin_notices', array( __CLASS__, 'render_php_css_parser_conflict_notice' ) );
}
/**
* Flush rewrite rules if the supported_post_types have changed.
*
* @since 0.6.2
*
* @param array $old_options Old options.
* @param array $new_options New options.
*/
public static function maybe_flush_rewrite_rules( $old_options, $new_options ) {
$old_post_types = isset( $old_options['supported_post_types'] ) ? $old_options['supported_post_types'] : array();
$new_post_types = isset( $new_options['supported_post_types'] ) ? $new_options['supported_post_types'] : array();
sort( $old_post_types );
sort( $new_post_types );
if ( $old_post_types !== $new_post_types ) {
flush_rewrite_rules( false );
}
}
/**
* Get plugin options.
*
* @return array Options.
*/
public static function get_options() {
$options = get_option( self::OPTION_NAME, array() );
if ( empty( $options ) ) {
$options = array();
}
self::$defaults['enable_response_caching'] = wp_using_ext_object_cache();
return array_merge( self::$defaults, $options );
}
/**
* Get plugin option.
*
* @param string $option Plugin option name.
* @param bool $default Default value.
*
* @return mixed Option value.
*/
public static function get_option( $option, $default = false ) {
$amp_options = self::get_options();
if ( ! isset( $amp_options[ $option ] ) ) {
return $default;
}
return $amp_options[ $option ];
}
/**
* Validate options.
*
* @param array $new_options Plugin options.
* @return array Options.
*/
public static function validate_options( $new_options ) {
$options = self::get_options();
if ( ! current_user_can( 'manage_options' ) ) {
return $options;
}
// Theme support.
$recognized_theme_supports = array(
'disabled',
'paired',
'native',
);
if ( isset( $new_options['theme_support'] ) && in_array( $new_options['theme_support'], $recognized_theme_supports, true ) ) {
$options['theme_support'] = $new_options['theme_support'];
// If this option was changed, display a notice with the new template mode.
if ( self::get_option( 'theme_support' ) !== $new_options['theme_support'] ) {
add_action( 'update_option_' . self::OPTION_NAME, array( __CLASS__, 'handle_updated_theme_support_option' ) );
}
}
$options['auto_accept_sanitization'] = ! empty( $new_options['auto_accept_sanitization'] );
$options['accept_tree_shaking'] = ! empty( $new_options['accept_tree_shaking'] );
$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'] ) ) {
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' ) );
} else {
$options['supported_post_types'][] = $post_type;
}
}
}
$theme_support_args = AMP_Theme_Support::get_theme_support_args();
$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 ) {
// Check save/delete pre-conditions and proceed if correct.
if ( empty( $data['type'] ) || empty( $data['config'] ) ) {
add_settings_error( self::OPTION_NAME, 'missing_analytics_vendor_or_config', __( 'Missing vendor type or config.', 'amp' ) );
continue;
}
// Validate JSON configuration.
$is_valid_json = AMP_HTML_Utils::is_valid_json( $data['config'] );
if ( ! $is_valid_json ) {
add_settings_error( self::OPTION_NAME, 'invalid_analytics_config_json', __( 'Invalid analytics config JSON.', 'amp' ) );
continue;
}
$entry_vendor_type = preg_replace( '/[^a-zA-Z0-9_\-]/', '', $data['type'] );
$entry_config = trim( $data['config'] );
if ( ! empty( $data['id'] ) && '__new__' !== $data['id'] ) {
$entry_id = sanitize_key( $data['id'] );
} else {
// Generate a hash string to uniquely identify this entry.
$entry_id = substr( md5( $entry_vendor_type . $entry_config ), 0, 12 );
// Avoid duplicates.
if ( isset( $options['analytics'][ $entry_id ] ) ) {
add_settings_error( self::OPTION_NAME, 'duplicate_analytics_entry', __( 'Duplicate analytics entry found.', 'amp' ) );
continue;
}
}
if ( isset( $data['delete'] ) ) {
unset( $options['analytics'][ $entry_id ] );
} else {
$options['analytics'][ $entry_id ] = array(
'type' => $entry_vendor_type,
'config' => $entry_config,
);
}
}
}
// Store the current version with the options so we know the format.
$options['version'] = AMP__VERSION;
// Handle the caching option.
$options['enable_response_caching'] = (
wp_using_ext_object_cache()
&&
! empty( $new_options['enable_response_caching'] )
);
if ( $options['enable_response_caching'] ) {
AMP_Theme_Support::reset_cache_miss_url_option();
}
return $options;
}
/**
* Check for errors with updating the supported post types.
*
* @since 0.6
* @see add_settings_error()
*/
public static function check_supported_post_type_update_errors() {
// If all templates are supported then skip check since all post types are also supported. This option only applies with native/transitional theme support.
if ( self::get_option( 'all_templates_supported', false ) && 'disabled' !== self::get_option( 'theme_support' ) ) {
return;
}
$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 ) ) {
continue;
}
$post_type_supported = post_type_supports( $post_type->name, AMP_Post_Type_Support::SLUG );
$is_support_elected = in_array( $post_type->name, $supported_types, true );
$error = null;
$code = null;
if ( $is_support_elected && ! $post_type_supported ) {
/* translators: %s: Post type name. */
$error = __( '"%s" could not be activated because support is removed by a plugin or theme', 'amp' );
$code = sprintf( '%s_activation_error', $post_type->name );
} elseif ( ! $is_support_elected && $post_type_supported ) {
/* translators: %s: Post type name. */
$error = __( '"%s" could not be deactivated because support is added by a plugin or theme', 'amp' );
$code = sprintf( '%s_deactivation_error', $post_type->name );
}
if ( isset( $error, $code ) ) {
add_settings_error(
self::OPTION_NAME,
$code,
sprintf(
$error,
isset( $post_type->label ) ? $post_type->label : $post_type->name
)
);
}
}
}
/**
* Update plugin option.
*
* @param string $option Plugin option name.
* @param mixed $value Plugin option value.
*
* @return bool Whether update succeeded.
*/
public static function update_option( $option, $value ) {
$amp_options = self::get_options();
$amp_options[ $option ] = $value;
return update_option( self::OPTION_NAME, $amp_options, false );
}
/**
* Handle analytics submission.
*/
public static function handle_analytics_submit() {
// Request must come from user with right capabilities.
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Sorry, you do not have the necessary permissions to perform this action', 'amp' ) );
}
// Ensure request is coming from analytics option form.
check_admin_referer( 'analytics-options', 'analytics-options' );
if ( isset( $_POST['amp-options']['analytics'] ) ) {
self::update_option( 'analytics', wp_unslash( $_POST['amp-options']['analytics'] ) );
$errors = get_settings_errors( self::OPTION_NAME );
if ( empty( $errors ) ) {
add_settings_error( self::OPTION_NAME, 'settings_updated', __( 'The analytics entry was successfully saved!', 'amp' ), 'updated' );
$errors = get_settings_errors( self::OPTION_NAME );
}
set_transient( 'settings_errors', $errors );
}
/*
* Redirect to keep the user in the analytics options page.
* Wrap in is_admin() to enable phpunit tests to exercise this code.
*/
wp_safe_redirect( admin_url( 'admin.php?page=amp-analytics-options&settings-updated=1' ) );
exit;
}
/**
* Update analytics options.
*
* @codeCoverageIgnore
* @deprecated
* @param array $data Unsanitized unslashed data.
* @return bool Whether options were updated.
*/
public static function update_analytics_options( $data ) {
_deprecated_function( __METHOD__, '0.6', __CLASS__ . '::update_option' );
return self::update_option( 'analytics', wp_unslash( $data ) );
}
/**
* Renders the welcome notice on the 'AMP Settings' page.
*
* Uses the user meta values for the dismissed WP pointers.
* So once the user dismisses this notice, it will never appear again.
*/
public static function render_welcome_notice() {
if ( 'toplevel_page_' . self::OPTION_NAME !== get_current_screen()->id ) {
return;
}
$notice_id = 'amp-welcome-notice-1';
$dismissed = get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true );
if ( in_array( $notice_id, explode( ',', strval( $dismissed ) ), true ) ) {
return;
}
?>
<div class="amp-welcome-notice notice notice-info is-dismissible" id="<?php echo esc_attr( $notice_id ); ?>">
<div class="notice-dismiss"></div>
<div class="amp-welcome-icon-holder">
<img class="amp-welcome-icon" src="<?php echo esc_url( amp_get_asset_url( 'images/amp-welcome-icon.svg' ) ); ?>" alt="<?php esc_html_e( 'Illustration of WordPress running AMP plugin.', 'amp' ); ?>" />
</div>
<h1><?php esc_html_e( 'Welcome to AMP for WordPress', 'amp' ); ?></h1>
<h3><?php esc_html_e( 'Bring the speed and features of the open source AMP project to your site, complete with the tools to support content authoring and website development.', 'amp' ); ?></h3>
<h3><?php esc_html_e( 'From granular controls that help you create AMP content, to Core Gutenberg support, to a sanitizer that only shows visitors error-free pages, to a full error workflow for developers, this release enables rich, performant experiences for your WordPress site.', 'amp' ); ?></h3>
<a href="https://amp-wp.org/getting-started/" target="_blank" class="button button-primary"><?php esc_html_e( 'Learn More', 'amp' ); ?></a>
</div>
<script>
jQuery( function( $ ) {
// On dismissing the notice, make a POST request to store this notice with the dismissed WP pointers so it doesn't display again.
$( <?php echo wp_json_encode( "#$notice_id" ); ?> ).on( 'click', '.notice-dismiss', function() {
$.post( ajaxurl, {
pointer: <?php echo wp_json_encode( $notice_id ); ?>,
action: 'dismiss-wp-pointer'
} );
} );
} );
</script>
<style type="text/css">
.amp-welcome-notice {
padding: 38px;
}
.amp-welcome-notice + .notice {
clear: both;
}
.amp-welcome-icon-holder {
width: 200px;
height: 200px;
float: left;
margin: 0 38px 38px 0;
}
.amp-welcome-icon {
width: 100%;
height: 100%;
display: block;
}
.amp-welcome-notice h1 {
font-weight: bold;
}
.amp-welcome-notice h3 {
font-size: 16px;
font-weight: 500;
}
</style>
<?php
}
/**
* Outputs an admin notice if persistent object cache is not present.
*
* @return void
*/
public static function persistent_object_caching_notice() {
if ( ! wp_using_ext_object_cache() && 'toplevel_page_' . self::OPTION_NAME === get_current_screen()->id ) {
printf(
'<div class="notice notice-warning"><p>%s</p></div>',
sprintf(
/* translators: %s: Persistent object cache support URL */
__( 'The AMP plugin performs at its best when persistent object cache is enabled. <a href="%s">More details</a>', 'amp' ), // phpcs:ignore WordPress.Security.EscapeOutput
esc_url( __( 'https://codex.wordpress.org/Class_Reference/WP_Object_Cache#Persistent_Caching', 'amp' ) )
)
);
}
}
/**
* Render the cache miss admin notice.
*
* @return void
*/
public static function render_cache_miss_notice() {
if ( 'toplevel_page_' . self::OPTION_NAME !== get_current_screen()->id ) {
return;
}
if ( ! self::show_response_cache_disabled_notice() ) {
return;
}
printf(
'<div class="notice notice-warning is-dismissible"><p>%s</p></div>',
sprintf(
/* translators: %s: post-processor cache support URL */
__( 'The AMP plugin&lsquo;s post-processor cache was disabled due to the detection of highly-variable content. <a href="%s">More details</a>', 'amp' ), // phpcs:ignore WordPress.Security.EscapeOutput
esc_url( __( 'https://github.com/ampproject/amp-wp/wiki/Post-Processor-Cache', 'amp' ) )
)
);
}
/**
* Render PHP-CSS-Parser conflict notice.
*
* @return void
*/
public static function render_php_css_parser_conflict_notice() {
if ( 'toplevel_page_' . self::OPTION_NAME !== get_current_screen()->id ) {
return;
}
if ( AMP_Style_Sanitizer::has_required_php_css_parser() ) {
return;
}
try {
$reflection = new ReflectionClass( 'Sabberworm\CSS\CSSList\CSSList' );
$source_dir = str_replace(
trailingslashit( WP_CONTENT_DIR ),
'',
preg_replace( '#/vendor/sabberworm/.+#', '', $reflection->getFileName() )
);
printf(
'<div class="notice notice-warning"><p>%s</p></div>',
sprintf(
/* translators: %s: path to the conflicting library */
__( 'A conflicting version of PHP-CSS-Parser appears to be installed by another plugin or theme (located in %s). Because of this, CSS processing will be limited, and tree shaking will not be available.', 'amp' ), // phpcs:ignore WordPress.Security.EscapeOutput
'<code>' . esc_html( $source_dir ) . '</code>'
)
);
} catch ( ReflectionException $e ) {
printf(
'<div class="notice notice-warning"><p>%s</p></div>',
esc_html__( 'PHP-CSS-Parser is not available so CSS processing will not be available.', 'amp' )
);
}
}
/**
* Show the response cache disabled notice.
*
* @since 1.0
*
* @return bool
*/
public static function show_response_cache_disabled_notice() {
return (
wp_using_ext_object_cache()
&&
! self::get_option( 'enable_response_caching' )
&&
AMP_Theme_Support::exceeded_cache_miss_threshold()
);
}
/**
* Adds a message for an update of the theme support setting.
*/
public static function handle_updated_theme_support_option() {
$template_mode = self::get_option( 'theme_support' );
// Make sure post type support has been added for sake of amp_admin_get_preview_permalink().
foreach ( AMP_Post_Type_Support::get_eligible_post_types() as $post_type ) {
remove_post_type_support( $post_type, AMP_Post_Type_Support::SLUG );
}
AMP_Post_Type_Support::add_post_type_support();
// Ensure theme support flags are set properly according to the new mode so that proper AMP URL can be generated.
$has_theme_support = ( 'native' === $template_mode || 'paired' === $template_mode );
if ( $has_theme_support ) {
$theme_support = current_theme_supports( AMP_Theme_Support::SLUG );
if ( ! is_array( $theme_support ) ) {
$theme_support = array();
}
$theme_support['paired'] = 'paired' === $template_mode;
add_theme_support( AMP_Theme_Support::SLUG, $theme_support );
} else {
remove_theme_support( AMP_Theme_Support::SLUG ); // So that the amp_get_permalink() will work for reader mode URL.
}
$url = amp_admin_get_preview_permalink();
$notice_type = 'updated';
$review_messages = array();
if ( $url && $has_theme_support ) {
$validation = AMP_Validation_Manager::validate_url( $url );
if ( is_wp_error( $validation ) ) {
$review_messages[] = esc_html(
sprintf(
/* translators: 1: error message. 2: error code. */
__( 'However, there was an error when checking the AMP validity for your site.', 'amp' ),
$validation->get_error_message(),
$validation->get_error_code()
)
);
$error_message = $validation->get_error_message();
if ( $error_message ) {
$review_messages[] = $error_message;
} else {
/* translators: %s is the error code */
$review_messages[] = esc_html( sprintf( __( 'Error code: %s.', 'amp' ), $validation->get_error_code() ) );
}
$notice_type = 'error';
} elseif ( is_array( $validation ) ) {
$new_errors = 0;
$rejected_errors = 0;
$errors = wp_list_pluck( $validation['results'], 'error' );
foreach ( $errors as $error ) {
$sanitization = AMP_Validation_Error_Taxonomy::get_validation_error_sanitization( $error );
$is_new_rejected = AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_REJECTED_STATUS === $sanitization['status'];
if ( $is_new_rejected || AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_ACCEPTED_STATUS === $sanitization['status'] ) {
$new_errors++;
}
if ( $is_new_rejected || AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACK_REJECTED_STATUS === $sanitization['status'] ) {
$rejected_errors++;
}
}
$invalid_url_post_id = AMP_Validated_URL_Post_Type::store_validation_errors( $errors, $url );
$invalid_url_screen_url = ! is_wp_error( $invalid_url_post_id ) ? get_edit_post_link( $invalid_url_post_id, 'raw' ) : null;
if ( $rejected_errors > 0 ) {
$notice_type = 'error';
$message = wp_kses_post(
sprintf(
/* translators: %s is count of rejected errors */
_n(
'However, AMP is not yet available due to %s validation error (for one URL at least).',
'However, AMP is not yet available due to %s validation errors (for one URL at least).',
number_format_i18n( $rejected_errors ),
'amp'
),
$rejected_errors,
esc_url( $invalid_url_screen_url )
)
);
if ( $invalid_url_screen_url ) {
$message .= ' ' . wp_kses_post(
sprintf(
/* translators: %s is URL to review issues */
_n(
'<a href="%s">Review Issue</a>.',
'<a href="%s">Review Issues</a>.',
$rejected_errors,
'amp'
),
esc_url( $invalid_url_screen_url )
)
);
}
$review_messages[] = $message;
} else {
$message = wp_kses_post(
sprintf(
/* translators: %s is an AMP URL */
__( 'View an <a href="%s">AMP version of your site</a>.', 'amp' ),
esc_url( $url )
)
);
if ( $new_errors > 0 && $invalid_url_screen_url ) {
$message .= ' ' . wp_kses_post(
sprintf(
/* translators: 1: URL to review issues. 2: count of new errors. */
_n(
'Please also <a href="%1$s">review %2$s issue</a> which may need to be fixed (for one URL at least).',
'Please also <a href="%1$s">review %2$s issues</a> which may need to be fixed (for one URL at least).',
$new_errors,
'amp'
),
esc_url( $invalid_url_screen_url ),
number_format_i18n( $new_errors )
)
);
}
$review_messages[] = $message;
}
}
}
switch ( $template_mode ) {
case 'native':
$message = esc_html__( 'Native mode activated!', 'amp' );
if ( $review_messages ) {
$message .= ' ' . join( ' ', $review_messages );
}
break;
case 'paired':
$message = esc_html__( 'Transitional mode activated!', 'amp' );
if ( $review_messages ) {
$message .= ' ' . join( ' ', $review_messages );
}
break;
case 'disabled':
$message = wp_kses_post(
sprintf(
/* translators: %s is an AMP URL */
__( 'Reader mode activated! View the <a href="%s">AMP version of a recent post</a>. It is recommended that you upgrade to Native or Transitional mode.', 'amp' ),
esc_url( $url )
)
);
break;
}
if ( isset( $message ) ) {
add_settings_error( self::OPTION_NAME, 'template_mode_updated', $message, $notice_type );
}
}
}

View File

@ -0,0 +1,616 @@
<?php
/**
* AMP Options.
*
* @package AMP
*/
/**
* AMP_Options_Menu class.
*/
class AMP_Options_Menu {
/**
* The AMP svg menu icon.
*
* @var string
*/
const ICON_BASE64_SVG = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB3aWR0aD0iNjJweCIgaGVpZ2h0PSI2MnB4IiB2aWV3Qm94PSIwIDAgNjIgNjIiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+ICAgICAgICA8dGl0bGU+QU1QLUJyYW5kLUJsYWNrLUljb248L3RpdGxlPiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4gICAgPGRlZnM+PC9kZWZzPiAgICA8ZyBpZD0iYW1wLWxvZ28taW50ZXJuYWwtc2l0ZSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+ICAgICAgICA8ZyBpZD0iQU1QLUJyYW5kLUJsYWNrLUljb24iIGZpbGw9IiMwMDAwMDAiPiAgICAgICAgICAgIDxwYXRoIGQ9Ik00MS42Mjg4NjY3LDI4LjE2MTQzMzMgTDI4LjYyNDM2NjcsNDkuODAzNTY2NyBMMjYuMjY4MzY2Nyw0OS44MDM1NjY3IEwyOC41OTc1LDM1LjcwMTY2NjcgTDIxLjM4MzgsMzUuNzEwOTY2NyBDMjEuMzgzOCwzNS43MTA5NjY3IDIxLjMxNTYsMzUuNzEzMDMzMyAyMS4yODM1NjY3LDM1LjcxMzAzMzMgQzIwLjYzMzYsMzUuNzEzMDMzMyAyMC4xMDc2MzMzLDM1LjE4NzA2NjcgMjAuMTA3NjMzMywzNC41MzcxIEMyMC4xMDc2MzMzLDM0LjI1ODEgMjAuMzY3LDMzLjc4NTg2NjcgMjAuMzY3LDMzLjc4NTg2NjcgTDMzLjMyOTEzMzMsMTIuMTY5NTY2NyBMMzUuNzI0NCwxMi4xNzk5IEwzMy4zMzYzNjY3LDI2LjMwMzUgTDQwLjU4NzI2NjcsMjYuMjk0MiBDNDAuNTg3MjY2NywyNi4yOTQyIDQwLjY2NDc2NjcsMjYuMjkzMTY2NyA0MC43MDE5NjY3LDI2LjI5MzE2NjcgQzQxLjM1MTkzMzMsMjYuMjkzMTY2NyA0MS44Nzc5LDI2LjgxOTEzMzMgNDEuODc3OSwyNy40NjkxIEM0MS44Nzc5LDI3LjczMjYgNDEuNzc0NTY2NywyNy45NjQwNjY3IDQxLjYyNzgzMzMsMjguMTYwNCBMNDEuNjI4ODY2NywyOC4xNjE0MzMzIFogTTMxLDAgQzEzLjg3ODcsMCAwLDEzLjg3OTczMzMgMCwzMSBDMCw0OC4xMjEzIDEzLjg3ODcsNjIgMzEsNjIgQzQ4LjEyMDI2NjcsNjIgNjIsNDguMTIxMyA2MiwzMSBDNjIsMTMuODc5NzMzMyA0OC4xMjAyNjY3LDAgMzEsMCBMMzEsMCBaIiBpZD0iRmlsbC0xIj48L3BhdGg+ICAgICAgICA8L2c+ICAgIDwvZz48L3N2Zz4=';
/**
* Initialize.
*/
public function init() {
add_action( 'admin_post_amp_analytics_options', 'AMP_Options_Manager::handle_analytics_submit' );
add_action( 'admin_menu', array( $this, 'add_menu_items' ), 9 );
$plugin_file = preg_replace( '#.+/(?=.+?/.+?)#', '', AMP__FILE__ );
add_filter( "plugin_action_links_{$plugin_file}", array( $this, 'add_plugin_action_links' ) );
}
/**
* Add plugin action links.
*
* @param array $links Links.
* @return array Modified links.
*/
public function add_plugin_action_links( $links ) {
return array_merge(
array(
'settings' => sprintf(
'<a href="%1$s">%2$s</a>',
esc_url( add_query_arg( 'page', AMP_Options_Manager::OPTION_NAME, admin_url( 'admin.php' ) ) ),
__( 'Settings', 'amp' )
),
),
$links
);
}
/**
* Add menu.
*/
public function add_menu_items() {
add_menu_page(
__( 'AMP Options', 'amp' ),
__( 'AMP', 'amp' ),
'edit_posts',
AMP_Options_Manager::OPTION_NAME,
array( $this, 'render_screen' ),
self::ICON_BASE64_SVG
);
add_submenu_page(
AMP_Options_Manager::OPTION_NAME,
__( 'AMP Settings', 'amp' ),
__( 'General', 'amp' ),
'edit_posts',
AMP_Options_Manager::OPTION_NAME
);
add_settings_section(
'general',
false,
'__return_false',
AMP_Options_Manager::OPTION_NAME
);
add_settings_field(
'theme_support',
__( 'Template Mode', 'amp' ),
array( $this, 'render_theme_support' ),
AMP_Options_Manager::OPTION_NAME,
'general',
array(
'class' => 'theme_support',
)
);
add_settings_field(
'validation',
__( 'Validation Handling', 'amp' ),
array( $this, 'render_validation_handling' ),
AMP_Options_Manager::OPTION_NAME,
'general',
array(
'class' => 'amp-validation-field',
)
);
add_settings_field(
'supported_templates',
__( 'Supported Templates', 'amp' ),
array( $this, 'render_supported_templates' ),
AMP_Options_Manager::OPTION_NAME,
'general',
array(
'class' => 'amp-template-support-field',
)
);
if ( wp_using_ext_object_cache() ) {
add_settings_field(
'caching',
__( 'Caching', 'amp' ),
array( $this, 'render_caching' ),
AMP_Options_Manager::OPTION_NAME,
'general',
array(
'class' => 'amp-caching-field',
)
);
}
$submenus = array(
new AMP_Analytics_Options_Submenu( AMP_Options_Manager::OPTION_NAME ),
);
// Create submenu items and calls on the Submenu Page object to render the actual contents of the page.
foreach ( $submenus as $submenu ) {
$submenu->init();
}
}
/**
* Render theme support.
*
* @since 1.0
*/
public function render_theme_support() {
$theme_support = AMP_Options_Manager::get_option( 'theme_support' );
/* translators: %s: URL to the documentation. */
$native_description = sprintf( __( 'Integrates AMP as the framework for your site by using the actives theme templates and styles to render AMP responses. This means your site is <b>AMP-first</b> and your canonical URLs are AMP! Depending on your theme/plugins, a varying level of <a href="%s">development work</a> may be required.', 'amp' ), esc_url( 'https://amp-wp.org/documentation/developing-wordpress-amp-sites/' ) );
/* translators: %s: URL to the documentation. */
$transitional_description = sprintf( __( 'Uses the active themes templates to generate non-AMP and AMP versions of your content, allowing for each canonical URL to have a corresponding (paired) AMP URL. This mode is useful to progressively transition towards a fully AMP-first site. Depending on your theme/plugins, a varying level of <a href="%s">development work</a> may be required.', 'amp' ), esc_url( 'https://amp-wp.org/documentation/developing-wordpress-amp-sites/' ) );
$reader_description = __( 'Formerly called the <b>classic mode</b>, this mode generates paired AMP content using simplified templates which may not match the look-and-feel of your site. Only posts/pages can be served as AMP in Reader mode. No redirection is performed for mobile visitors; AMP pages are served by AMP consumption platforms.', 'amp' );
/* translators: %s: URL to the ecosystem page. */
$ecosystem_description = sprintf( __( 'For a list of themes and plugins that are known to be AMP compatible, please see the <a href="%s">ecosystem page</a>.' ), esc_url( 'https://amp-wp.org/ecosystem/' ) );
$builtin_support = in_array( get_template(), AMP_Core_Theme_Sanitizer::get_supported_themes(), true );
?>
<?php if ( current_theme_supports( AMP_Theme_Support::SLUG ) && ! AMP_Theme_Support::is_support_added_via_option() ) : ?>
<div class="notice notice-info notice-alt inline">
<p><?php esc_html_e( 'Your active theme has built-in AMP support.', 'amp' ); ?></p>
</div>
<p>
<?php echo wp_kses_post( $ecosystem_description ); ?>
</p>
<p>
<?php if ( amp_is_canonical() ) : ?>
<strong><?php esc_html_e( 'Native:', 'amp' ); ?></strong>
<?php echo wp_kses_post( $native_description ); ?>
<?php else : ?>
<strong><?php esc_html_e( 'Transitional:', 'amp' ); ?></strong>
<?php echo wp_kses_post( $transitional_description ); ?>
<?php endif; ?>
</p>
<?php else : ?>
<fieldset <?php disabled( ! current_user_can( 'manage_options' ) ); ?>>
<?php if ( $builtin_support ) : ?>
<div class="notice notice-success notice-alt inline">
<p><?php esc_html_e( 'Your active theme is known to work well in transitional or native mode.', 'amp' ); ?></p>
</div>
<?php endif; ?>
<p>
<?php echo wp_kses_post( $ecosystem_description ); ?>
</p>
<dl>
<dt>
<input type="radio" id="theme_support_native" name="<?php echo esc_attr( AMP_Options_Manager::OPTION_NAME . '[theme_support]' ); ?>" value="native" <?php checked( $theme_support, 'native' ); ?>>
<label for="theme_support_native">
<strong><?php esc_html_e( 'Native', 'amp' ); ?></strong>
</label>
</dt>
<dd>
<?php echo wp_kses_post( $native_description ); ?>
</dd>
<dt>
<input type="radio" id="theme_support_transitional" name="<?php echo esc_attr( AMP_Options_Manager::OPTION_NAME . '[theme_support]' ); ?>" value="paired" <?php checked( $theme_support, 'paired' ); ?>>
<label for="theme_support_transitional">
<strong><?php esc_html_e( 'Transitional', 'amp' ); ?></strong>
</label>
</dt>
<dd>
<?php echo wp_kses_post( $transitional_description ); ?>
</dd>
<dt>
<input type="radio" id="theme_support_disabled" name="<?php echo esc_attr( AMP_Options_Manager::OPTION_NAME . '[theme_support]' ); ?>" value="disabled" <?php checked( $theme_support, 'disabled' ); ?>>
<label for="theme_support_disabled">
<strong><?php esc_html_e( 'Reader', 'amp' ); ?></strong>
</label>
</dt>
<dd>
<?php echo wp_kses_post( $reader_description ); ?>
<?php if ( ! current_theme_supports( AMP_Theme_Support::SLUG ) && wp_count_posts( AMP_Validated_URL_Post_Type::POST_TYPE_SLUG )->publish > 0 ) : ?>
<div class="notice notice-info inline notice-alt">
<p>
<?php
echo wp_kses_post(
sprintf(
/* translators: %1: link to invalid URLs. 2: link to validation errors. */
__( 'View current site compatibility results for native and transitional modes: %1$s and %2$s.', 'amp' ),
sprintf(
'<a href="%s">%s</a>',
esc_url( add_query_arg( 'post_type', AMP_Validated_URL_Post_Type::POST_TYPE_SLUG, admin_url( 'edit.php' ) ) ),
esc_html( get_post_type_object( AMP_Validated_URL_Post_Type::POST_TYPE_SLUG )->labels->name )
),
sprintf(
'<a href="%s">%s</a>',
esc_url(
add_query_arg(
array(
'taxonomy' => AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG,
'post_type' => AMP_Validated_URL_Post_Type::POST_TYPE_SLUG,
),
admin_url( 'edit-tags.php' )
)
),
esc_html( get_taxonomy( AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG )->labels->name )
)
)
);
?>
</p>
</div>
<?php endif; ?>
</dd>
</dl>
</fieldset>
<?php endif; ?>
<?php
}
/**
* Post types support section renderer.
*
* @todo If dirty AMP is ever allowed (that is, post-processed documents which can be served with non-sanitized valdation errors), then automatically forcing sanitization in native should be able to be turned off.
*
* @since 1.0
*/
public function render_validation_handling() {
?>
<fieldset <?php disabled( ! current_user_can( 'manage_options' ) ); ?>>
<?php
$auto_sanitization = AMP_Validation_Error_Taxonomy::get_validation_error_sanitization(
array(
'code' => 'non_existent',
)
);
remove_filter( 'amp_validation_error_sanitized', array( 'AMP_Validation_Manager', 'filter_tree_shaking_validation_error_as_accepted' ) );
$tree_shaking_sanitization = AMP_Validation_Error_Taxonomy::get_validation_error_sanitization(
array(
'code' => AMP_Style_Sanitizer::TREE_SHAKING_ERROR_CODE,
)
);
$forced_sanitization = 'with_filter' === $auto_sanitization['forced'];
$forced_tree_shaking = $forced_sanitization || 'with_filter' === $tree_shaking_sanitization['forced'];
?>
<?php if ( $forced_sanitization ) : ?>
<div class="notice notice-info notice-alt inline">
<p><?php esc_html_e( 'Your install is configured via a theme or plugin to automatically sanitize any AMP validation error that is encountered.', 'amp' ); ?></p>
</div>
<input type="hidden" name="<?php echo esc_attr( AMP_Options_Manager::OPTION_NAME . '[auto_accept_sanitization]' ); ?>" value="<?php echo AMP_Options_Manager::get_option( 'auto_accept_sanitization' ) ? 'on' : ''; ?>">
<?php else : ?>
<div class="amp-auto-accept-sanitize-canonical notice notice-info notice-alt inline">
<p><?php esc_html_e( 'All new validation errors are automatically accepted when in native mode.', 'amp' ); ?></p>
</div>
<div class="amp-auto-accept-sanitize">
<p>
<label for="auto_accept_sanitization">
<input id="auto_accept_sanitization" type="checkbox" name="<?php echo esc_attr( AMP_Options_Manager::OPTION_NAME . '[auto_accept_sanitization]' ); ?>" <?php checked( AMP_Options_Manager::get_option( 'auto_accept_sanitization' ) ); ?>>
<?php esc_html_e( 'Automatically accept sanitization for any newly encountered AMP validation errors.', 'amp' ); ?>
</label>
</p>
<p class="description">
<?php esc_html_e( 'This will ensure your responses are always valid AMP but some important content may get stripped out (e.g. scripts).', 'amp' ); ?>
<?php
echo wp_kses_post(
sprintf(
/* translators: %s is URL to validation errors screen */
__( 'Existing validation errors which you have already rejected will not be modified (you may want to consider <a href="%s">bulk-accepting them</a>).', 'amp' ),
esc_url(
add_query_arg(
array(
'taxonomy' => AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG,
'post_type' => AMP_Validated_URL_Post_Type::POST_TYPE_SLUG,
),
admin_url( 'edit-tags.php' )
)
)
)
)
?>
</p>
</div>
<?php endif; ?>
<?php if ( $forced_tree_shaking ) : ?>
<input type="hidden" name="<?php echo esc_attr( AMP_Options_Manager::OPTION_NAME . '[accept_tree_shaking]' ); ?>" value="<?php echo AMP_Options_Manager::get_option( 'accept_tree_shaking' ) ? 'on' : ''; ?>">
<?php else : ?>
<div class="amp-tree-shaking">
<p>
<label for="accept_tree_shaking">
<input id="accept_tree_shaking" type="checkbox" name="<?php echo esc_attr( AMP_Options_Manager::OPTION_NAME . '[accept_tree_shaking]' ); ?>" <?php checked( AMP_Options_Manager::get_option( 'accept_tree_shaking' ) ); ?>>
<?php esc_html_e( 'Automatically remove CSS rules that are not relevant to a given page (tree shaking).', 'amp' ); ?>
</label>
</p>
<p class="description">
<?php esc_html_e( 'AMP limits the total amount of CSS to no more than 50KB; any more than this will cause a validation error. The need to tree shake the CSS is not done by default because in some situations (in particular for dynamic content) it can result in CSS rules being removed that are needed.', 'amp' ); ?>
</p>
</div>
<?php endif; ?>
<script>
(function( $ ) {
var getThemeSupportMode = function() {
var checkedInput = $( 'input[type=radio][name="amp-options[theme_support]"]:checked' );
if ( 0 === checkedInput.length ) {
return <?php echo wp_json_encode( amp_is_canonical() ? 'native' : 'paired' ); ?>;
}
return checkedInput.val();
};
var updateTreeShakingHiddenClass = function() {
var checkbox = $( '#auto_accept_sanitization' );
$( '.amp-tree-shaking' ).toggleClass( 'hidden', checkbox.prop( 'checked' ) && 'native' !== getThemeSupportMode() );
};
var updateHiddenClasses = function() {
var themeSupportMode = getThemeSupportMode();
$( '.amp-auto-accept-sanitize' ).toggleClass( 'hidden', 'native' === themeSupportMode );
$( '.amp-validation-field' ).toggleClass( 'hidden', 'disabled' === themeSupportMode );
$( '.amp-auto-accept-sanitize-canonical' ).toggleClass( 'hidden', 'native' !== themeSupportMode );
updateTreeShakingHiddenClass();
};
$( 'input[type=radio][name="amp-options[theme_support]"]' ).change( updateHiddenClasses );
$( '#auto_accept_sanitization' ).change( updateTreeShakingHiddenClass );
updateHiddenClasses();
})( jQuery );
</script>
<p>
<label for="disable_admin_bar">
<input id="disable_admin_bar" type="checkbox" name="<?php echo esc_attr( AMP_Options_Manager::OPTION_NAME . '[disable_admin_bar]' ); ?>" <?php checked( AMP_Options_Manager::get_option( 'disable_admin_bar' ) ); ?>>
<?php esc_html_e( 'Disable admin bar on AMP pages.', 'amp' ); ?>
</label>
</p>
<p class="description">
<?php esc_html_e( 'An additional stylesheet is required to properly render the admin bar. If the additional stylesheet causes the total CSS to surpass 50KB then the admin bar should be disabled to prevent a validation error or an unstyled admin bar in AMP responses.', 'amp' ); ?>
</p>
</fieldset>
<?php
}
/**
* Supported templates section renderer.
*
* @since 1.0
*/
public function render_supported_templates() {
$theme_support_args = AMP_Theme_Support::get_theme_support_args();
?>
<?php if ( ! isset( $theme_support_args['available_callback'] ) ) : ?>
<fieldset id="all_templates_supported_fieldset" <?php disabled( ! current_user_can( 'manage_options' ) ); ?>>
<?php if ( isset( $theme_support_args['templates_supported'] ) && 'all' === $theme_support_args['templates_supported'] ) : ?>
<div class="notice notice-info notice-alt inline">
<p>
<?php esc_html_e( 'The current theme requires all templates to support AMP.', 'amp' ); ?>
</p>
</div>
<?php else : ?>
<p>
<label for="all_templates_supported">
<input id="all_templates_supported" type="checkbox" name="<?php echo esc_attr( AMP_Options_Manager::OPTION_NAME . '[all_templates_supported]' ); ?>" <?php checked( AMP_Options_Manager::get_option( 'all_templates_supported' ) ); ?>>
<?php esc_html_e( 'Serve all templates as AMP regardless of what is being queried.', 'amp' ); ?>
</label>
</p>
<p class="description">
<?php esc_html_e( 'This will allow all of the URLs on your site to be served as AMP by default.', 'amp' ); ?>
</p>
<?php endif; ?>
</fieldset>
<?php else : ?>
<div class="notice notice-warning notice-alt inline">
<p>
<?php
printf(
/* translators: %s: available_callback */
esc_html__( 'Your theme is using the deprecated %s argument for AMP theme support.', 'amp' ),
'available_callback'
);
?>
</p>
</div>
<?php endif; ?>
<fieldset id="supported_post_types_fieldset" <?php disabled( ! current_user_can( 'manage_options' ) ); ?>>
<?php $element_name = AMP_Options_Manager::OPTION_NAME . '[supported_post_types][]'; ?>
<h4 class="title"><?php esc_html_e( 'Content Types', 'amp' ); ?></h4>
<p>
<?php esc_html_e( 'The following content types will be available as AMP:', 'amp' ); ?>
</p>
<ul>
<?php foreach ( array_map( 'get_post_type_object', AMP_Post_Type_Support::get_eligible_post_types() ) as $post_type ) : ?>
<li>
<?php $element_id = AMP_Options_Manager::OPTION_NAME . "-supported_post_types-{$post_type->name}"; ?>
<input
type="checkbox"
id="<?php echo esc_attr( $element_id ); ?>"
name="<?php echo esc_attr( $element_name ); ?>"
value="<?php echo esc_attr( $post_type->name ); ?>"
<?php checked( post_type_supports( $post_type->name, AMP_Post_Type_Support::SLUG ) ); ?>
>
<label for="<?php echo esc_attr( $element_id ); ?>">
<?php echo esc_html( $post_type->label ); ?>
</label>
</li>
<?php endforeach; ?>
</ul>
</fieldset>
<?php if ( ! isset( $theme_support_args['available_callback'] ) ) : ?>
<fieldset id="supported_templates_fieldset" <?php disabled( ! current_user_can( 'manage_options' ) ); ?>>
<style>
#supported_templates_fieldset ul ul {
margin-left: 40px;
}
</style>
<h4 class="title"><?php esc_html_e( 'Templates', 'amp' ); ?></h4>
<?php
self::list_template_conditional_options( AMP_Theme_Support::get_supportable_templates() );
?>
<script>
// Let clicks on parent items automatically cause the children checkboxes to have same checked state applied.
(function ( $ ) {
$( '#supported_templates_fieldset input[type=checkbox]' ).on( 'click', function() {
$( this ).siblings( 'ul' ).find( 'input[type=checkbox]' ).prop( 'checked', this.checked );
} );
})( jQuery );
</script>
</fieldset>
<script>
// Update the visibility of the fieldsets based on the selected template mode and then whether all templates are indicated to be supported.
(function ( $ ) {
var templateModeInputs, themeSupportDisabledInput, allTemplatesSupportedInput, supportForced;
templateModeInputs = $( 'input[type=radio][name="amp-options[theme_support]"]' );
themeSupportDisabledInput = $( '#theme_support_disabled' );
allTemplatesSupportedInput = $( '#all_templates_supported' );
supportForced = <?php echo wp_json_encode( current_theme_supports( AMP_Theme_Support::SLUG ) && ! AMP_Theme_Support::is_support_added_via_option() ); ?>;
function isThemeSupportDisabled() {
return ! supportForced && themeSupportDisabledInput.prop( 'checked' );
}
function updateFieldsetVisibility() {
var allTemplatesSupported = 0 === allTemplatesSupportedInput.length || allTemplatesSupportedInput.prop( 'checked' );
$( '#all_templates_supported_fieldset, #supported_post_types_fieldset > .title' ).toggleClass(
'hidden',
isThemeSupportDisabled()
);
$( '#supported_post_types_fieldset' ).toggleClass(
'hidden',
allTemplatesSupported && ! isThemeSupportDisabled()
);
$( '#supported_templates_fieldset' ).toggleClass(
'hidden',
allTemplatesSupported || isThemeSupportDisabled()
);
}
templateModeInputs.on( 'change', updateFieldsetVisibility );
allTemplatesSupportedInput.on( 'click', updateFieldsetVisibility );
updateFieldsetVisibility();
})( jQuery );
</script>
<?php endif; ?>
<?php
}
/**
* Render the caching settings section.
*
* @since 1.0
*
* @todo Change the messaging and description to be user-friendly and helpful.
*/
public function render_caching() {
?>
<fieldset <?php disabled( ! current_user_can( 'manage_options' ) ); ?>>
<?php if ( AMP_Options_Manager::show_response_cache_disabled_notice() ) : ?>
<div class="notice notice-info notice-alt inline">
<p><?php esc_html_e( 'The post-processor cache was disabled due to detecting randomly generated content found on', 'amp' ); ?> <a href="<?php echo esc_url( get_option( AMP_Theme_Support::CACHE_MISS_URL_OPTION, '' ) ); ?>"><?php esc_html_e( 'on this web page.', 'amp' ); ?></a></p>
<p><?php esc_html_e( 'Randomly generated content was detected on this web page. To avoid filling up the cache with unusable content, the AMP plugin\'s post-processor cache was automatically disabled.', 'amp' ); ?>
<a href="<?php echo esc_url( 'https://github.com/ampproject/amp-wp/wiki/Post-Processor-Cache' ); ?>"><?php esc_html_e( 'Read more', 'amp' ); ?></a>.</p>
</div>
<?php endif; ?>
<p>
<label for="enable_response_caching">
<input id="enable_response_caching" type="checkbox" name="<?php echo esc_attr( AMP_Options_Manager::OPTION_NAME . '[enable_response_caching]' ); ?>" <?php checked( AMP_Options_Manager::get_option( 'enable_response_caching' ) ); ?>>
<?php esc_html_e( 'Enable post-processor caching.', 'amp' ); ?>
</label>
</p>
<p class="description"><?php esc_html_e( 'This will enable post-processor caching to speed up processing an AMP response after WordPress renders a template.', 'amp' ); ?></p>
</fieldset>
<?php
}
/**
* List template conditional options.
*
* @param array $options Options.
* @param string|null $parent ID of the parent option.
*/
private function list_template_conditional_options( $options, $parent = null ) {
$element_name = AMP_Options_Manager::OPTION_NAME . '[supported_templates][]';
?>
<ul>
<?php foreach ( $options as $id => $option ) : ?>
<?php
$element_id = AMP_Options_Manager::OPTION_NAME . '-supported-templates-' . $id;
if ( $parent ? empty( $option['parent'] ) || $parent !== $option['parent'] : ! empty( $option['parent'] ) ) {
continue;
}
// Skip showing an option if it doesn't have a label.
if ( empty( $option['label'] ) ) {
continue;
}
?>
<li>
<?php if ( empty( $option['immutable'] ) ) : ?>
<input
type="checkbox"
id="<?php echo esc_attr( $element_id ); ?>"
name="<?php echo esc_attr( $element_name ); ?>"
value="<?php echo esc_attr( $id ); ?>"
<?php checked( ! empty( $option['user_supported'] ) ); ?>
>
<?php else : // Persist user selection even when checkbox disabled, when selection forced by theme/filter. ?>
<input
type="checkbox"
id="<?php echo esc_attr( $element_id ); ?>"
<?php checked( ! empty( $option['supported'] ) ); ?>
<?php disabled( true ); ?>
>
<?php if ( ! empty( $option['user_supported'] ) ) : ?>
<input type="hidden" name="<?php echo esc_attr( $element_name ); ?>" value="<?php echo esc_attr( $id ); ?>">
<?php endif; ?>
<?php endif; ?>
<label for="<?php echo esc_attr( $element_id ); ?>">
<?php echo esc_html( $option['label'] ); ?>
</label>
<?php if ( ! empty( $option['description'] ) ) : ?>
<span class="description">
&mdash; <?php echo wp_kses_post( $option['description'] ); ?>
</span>
<?php endif; ?>
<?php self::list_template_conditional_options( $options, $id ); ?>
</li>
<?php endforeach; ?>
</ul>
<?php
}
/**
* Display Settings.
*
* @since 0.6
*/
public function render_screen() {
if ( ! empty( $_GET['settings-updated'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
AMP_Options_Manager::check_supported_post_type_update_errors();
}
?>
<?php if ( ! current_user_can( 'manage_options' ) ) : ?>
<div class="notice notice-info">
<p><?php esc_html_e( 'You do not have permission to modify these settings. They are shown here for your reference. Please contact your administrator to make changes.', 'amp' ); ?></p>
</div>
<?php endif; ?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<?php settings_errors(); ?>
<form id="amp-settings" action="options.php" method="post">
<?php
settings_fields( AMP_Options_Manager::OPTION_NAME );
do_settings_sections( AMP_Options_Manager::OPTION_NAME );
if ( current_user_can( 'manage_options' ) ) {
submit_button();
}
?>
</form>
</div>
<?php
}
}

View File

@ -0,0 +1,203 @@
<?php
/**
* Class AMP_Analytics_Options_Submenu_Page
*
* @package AMP
*/
/**
* Class AMP_Analytics_Options_Submenu_Page
*/
class AMP_Analytics_Options_Submenu_Page {
/**
* Render entry.
*
* @param string $id Entry ID.
* @param string $type Entry type.
* @param string $config Entry config as serialized JSON.
*/
private function render_entry( $id = '', $type = '', $config = '{}' ) {
$is_existing_entry = ! empty( $id );
if ( $is_existing_entry ) {
$entry_slug = sprintf( '%s%s', ( $type ? $type . '-' : '' ), substr( $id, - 6 ) );
/* translators: %s: the entry slug. */
$analytics_title = sprintf( __( 'Analytics: %s', 'amp' ), $entry_slug );
} else {
$analytics_title = __( 'Add new entry:', 'amp' );
$id = '__new__';
}
// Tidy-up the JSON for display.
if ( $config ) {
$options = ( 128 /* JSON_PRETTY_PRINT */ | 64 /* JSON_UNESCAPED_SLASHES */ );
$config = wp_json_encode( json_decode( $config ), $options );
}
$id_base = sprintf( '%s[analytics][%s]', AMP_Options_Manager::OPTION_NAME, $id );
?>
<div class="analytics-data-container">
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<h2>
<?php echo esc_html( $analytics_title ); ?>
</h2>
<div class="options">
<p>
<label>
<?php esc_html_e( 'Type:', 'amp' ); ?>
<input class="option-input" type="text" required name="<?php echo esc_attr( $id_base . '[type]' ); ?>" placeholder="<?php esc_attr_e( 'e.g. googleanalytics', 'amp' ); ?>" value="<?php echo esc_attr( $type ); ?>" />
</label>
<label>
<?php esc_html_e( 'ID:', 'amp' ); ?>
<input type="text" value="<?php echo esc_attr( $is_existing_entry ? $id : '' ); ?>" readonly />
</label>
<input type="hidden" name="<?php echo esc_attr( $id_base . '[id]' ); ?>" value="<?php echo esc_attr( $id ); ?>" />
</p>
<p>
<label>
<?php esc_html_e( 'JSON Configuration:', 'amp' ); ?>
<br />
<textarea
rows="10"
cols="100"
name="<?php echo esc_attr( $id_base . '[config]' ); ?>"
class="amp-analytics-input"
placeholder="{...}"
required
><?php echo esc_textarea( $config ); ?></textarea>
</label>
</p>
<input type="hidden" name="action" value="amp_analytics_options">
</div>
<p>
<?php
wp_nonce_field( 'analytics-options', 'analytics-options' );
submit_button( esc_html__( 'Save', 'amp' ), 'primary', 'save', false );
if ( $is_existing_entry ) {
submit_button( esc_html__( 'Delete', 'amp' ), 'delete button-primary', esc_attr( $id_base . '[delete]' ), false );
}
?>
</p>
</form>
</div><!-- #analytics-data-container -->
<?php
}
/**
* Render title.
*
* @param bool $has_entries Whether there are entries.
*/
public function render_title( $has_entries = false ) {
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<?php settings_errors(); ?>
<details <?php echo ! $has_entries ? 'open' : ''; ?>>
<summary>
<?php esc_html_e( 'Learn about analytics for AMP.', 'amp' ); ?>
</summary>
<p>
<?php
echo wp_kses_post(
sprintf(
/* translators: 1: AMP Analytics docs URL. 2: AMP for WordPress analytics docs URL. 3: AMP analytics code reference. 4: amp-analytics, 5: {. 6: }. 7: <script>, 8: googleanalytics. 9: AMP analytics vendor docs URL. 10: UA-XXXXX-Y. */
__( 'For Google Analytics, please see <a href="%1$s" target="_blank">Adding Analytics to your AMP pages</a>; see also the <a href="%2$s" target="_blank">Analytics wiki page</a> and the AMP project\'s <a href="%3$s" target="_blank">%4$s documentation</a>. The analytics configuration supplied below must take the form of JSON objects, which begin with a %5$s and end with a %6$s. Do not include any HTML tags like %4$s or %7$s. A common entry would have the type %8$s (see <a href="%9$s" target="_blank">available vendors</a>) and a configuration that looks like the following (where %10$s is replaced with your own site\'s account number):', 'amp' ),
__( 'https://developers.google.com/analytics/devguides/collection/amp-analytics/', 'amp' ),
__( 'https://amp-wp.org/documentation/playbooks/analytics/', 'amp' ),
__( 'https://www.ampproject.org/docs/reference/components/amp-analytics', 'amp' ),
'<code>amp-analytics</code>',
'<code>{</code>',
'<code>}</code>',
'<code>&lt;script&gt;</code>',
'<code>googleanalytics</code>',
__( 'https://www.ampproject.org/docs/analytics/analytics-vendors', 'amp' ),
'<code>UA-XXXXX-Y</code>'
)
);
?>
<pre>{
"vars": {
"account": "UA-XXXXX-Y"
},
"triggers": {
"trackPageview": {
"on": "visible",
"request": "pageview"
}
}
}</pre>
</p>
</details>
</div><!-- .wrap -->
<?php
}
/**
* Render styles.
*/
protected function render_styles() {
?>
<style>
.amp-analytics-input {
font-family: monospace;
}
.amp-analytics-input:invalid {
border-color: red;
}
</style>
<?php
}
/**
* Render scripts.
*/
protected function render_scripts() {
?>
<script>
Array.prototype.forEach.call( document.querySelectorAll( '.amp-analytics-input' ), function( textarea ) {
textarea.addEventListener( 'input', function() {
if ( ! this.value ) {
this.setCustomValidity( '' );
return;
}
try {
var value = JSON.parse( this.value );
if ( null === value || typeof value !== 'object' || Array.isArray( value ) ) {
this.setCustomValidity( <?php echo wp_json_encode( __( 'A JSON object is required, e.g. {...}', 'amp' ) ); ?> )
} else {
this.setCustomValidity( '' );
}
} catch ( e ) {
this.setCustomValidity( e.message )
}
} );
} );
</script>
<?php
}
/**
* Render.
*/
public function render() {
$this->render_styles();
$analytics_entries = AMP_Options_Manager::get_option( 'analytics', array() );
$this->render_title( ! empty( $analytics_entries ) );
// Render entries stored in the DB.
foreach ( $analytics_entries as $entry_id => $entry ) {
$this->render_entry( $entry_id, $entry['type'], $entry['config'] );
}
// Empty form for adding more entries.
$this->render_entry();
$this->render_scripts();
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,222 @@
<?php
/**
* Class AMP_Audio_Sanitizer
*
* @package AMP
*/
/**
* Class AMP_Audio_Sanitizer
*
* Converts <audio> tags to <amp-audio>
*/
class AMP_Audio_Sanitizer extends AMP_Base_Sanitizer {
use AMP_Noscript_Fallback;
/**
* Tag.
*
* @var string HTML audio tag to identify and replace with AMP version.
* @since 0.2
*/
public static $tag = 'audio';
/**
* Default args.
*
* @var array
*/
protected $DEFAULT_ARGS = array(
'add_noscript_fallback' => true,
);
/**
* Get mapping of HTML selectors to the AMP component selectors which they may be converted into.
*
* @return array Mapping.
*/
public function get_selector_conversion_mapping() {
return array(
'audio' => array( 'amp-audio' ),
);
}
/**
* Sanitize the <audio> elements from the HTML contained in this instance's DOMDocument.
*
* @since 0.2
*/
public function sanitize() {
$nodes = $this->dom->getElementsByTagName( self::$tag );
$num_nodes = $nodes->length;
if ( 0 === $num_nodes ) {
return;
}
if ( $this->args['add_noscript_fallback'] ) {
$this->initialize_noscript_allowed_attributes( self::$tag );
}
for ( $i = $num_nodes - 1; $i >= 0; $i-- ) {
$node = $nodes->item( $i );
// Skip element if already inside of an AMP element as a noscript fallback.
if ( $this->is_inside_amp_noscript( $node ) ) {
continue;
}
$old_attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node );
// For amp-audio, the default width and height are inferred from browser.
$sources = array();
$new_attributes = $this->filter_attributes( $old_attributes );
if ( ! empty( $new_attributes['src'] ) ) {
$sources[] = $new_attributes['src'];
}
/**
* Original node.
*
* @var DOMElement $old_node
*/
$old_node = $node->cloneNode( false );
// Gather all child nodes and supply empty video dimensions from sources.
$fallback = null;
$child_nodes = array();
while ( $node->firstChild ) {
$child_node = $node->removeChild( $node->firstChild );
if ( $child_node instanceof DOMElement && 'source' === $child_node->nodeName && $child_node->hasAttribute( 'src' ) ) {
$src = $this->maybe_enforce_https_src( $child_node->getAttribute( 'src' ), true );
if ( ! $src ) {
// @todo $this->remove_invalid_child( $child_node ), but this will require refactoring the while loop since it uses firstChild.
continue; // Skip adding source.
}
$sources[] = $src;
$child_node->setAttribute( 'src', $src );
$new_attributes = $this->filter_attributes( $new_attributes );
}
if ( ! $fallback && $child_node instanceof DOMElement && ! ( 'source' === $child_node->nodeName || 'track' === $child_node->nodeName ) ) {
$fallback = $child_node;
$fallback->setAttribute( 'fallback', '' );
}
$child_nodes[] = $child_node;
}
/*
* Add fallback for audio shortcode which is not present by default since wp_mediaelement_fallback()
* is not called when wp_audio_shortcode_library is filtered from mediaelement to amp.
*/
if ( ! $fallback && ! empty( $sources ) ) {
$fallback = $this->dom->createElement( 'a' );
$fallback->setAttribute( 'href', $sources[0] );
$fallback->setAttribute( 'fallback', '' );
$fallback->appendChild( $this->dom->createTextNode( $sources[0] ) );
$child_nodes[] = $fallback;
}
/*
* Audio in WordPress is responsive with 100% width, so this infers fixed-layout.
* In AMP, the amp-audio's default height is inferred from the browser.
*/
$new_attributes['width'] = 'auto';
// @todo Make sure poster and artwork attributes are HTTPS.
$new_node = AMP_DOM_Utils::create_node( $this->dom, 'amp-audio', $new_attributes );
foreach ( $child_nodes as $child_node ) {
$new_node->appendChild( $child_node );
if ( ! ( $child_node instanceof DOMElement ) || ! $child_node->hasAttribute( 'fallback' ) ) {
$old_node->appendChild( $child_node->cloneNode( true ) );
}
}
// Make sure the updated src and poster are applied to the original.
foreach ( array( 'src', 'poster', 'artwork' ) as $attr_name ) {
if ( $new_node->hasAttribute( $attr_name ) ) {
$old_node->setAttribute( $attr_name, $new_node->getAttribute( $attr_name ) );
}
}
/*
* If the node has at least one valid source, replace the old node with it.
* Otherwise, just remove the node.
*
* @todo Add a fallback handler.
* See: https://github.com/ampproject/amphtml/issues/2261
*/
if ( empty( $sources ) ) {
$this->remove_invalid_child( $node );
} else {
$node->parentNode->replaceChild( $new_node, $node );
if ( $this->args['add_noscript_fallback'] ) {
// Preserve original node in noscript for no-JS environments.
$this->append_old_node_noscript( $new_node, $old_node, $this->dom );
}
}
$this->did_convert_elements = true;
}
}
/**
* "Filter" HTML attributes for <amp-audio> elements.
*
* @since 0.2
*
* @param string[] $attributes {
* Attributes.
*
* @type string $src Audio URL - Empty if HTTPS required per $this->args['require_https_src']
* @type int $width <audio> attribute - Set to numeric value if px or %
* @type int $height <audio> attribute - Set to numeric value if px or %
* @type string $class <audio> attribute - Pass along if found
* @type bool $loop <audio> attribute - Convert 'false' to empty string ''
* @type bool $muted <audio> attribute - Convert 'false' to empty string ''
* @type bool $autoplay <audio> attribute - Convert 'false' to empty string ''
* }
* @return array Returns HTML attributes; removes any not specifically declared above from input.
*/
private function filter_attributes( $attributes ) {
$out = array();
foreach ( $attributes as $name => $value ) {
switch ( $name ) {
case 'src':
$out[ $name ] = $this->maybe_enforce_https_src( $value );
break;
case 'width':
case 'height':
$out[ $name ] = $this->sanitize_dimension( $value, $name );
break;
case 'class':
$out[ $name ] = $value;
break;
case 'loop':
case 'muted':
case 'autoplay':
if ( 'false' !== $value ) {
$out[ $name ] = '';
}
break;
case 'data-amp-layout':
$out['layout'] = $value;
break;
case 'data-amp-noloading':
$out['noloading'] = $value;
break;
default:
$out[ $name ] = $value;
}
}
return $out;
}
}

View File

@ -0,0 +1,587 @@
<?php
/**
* Class AMP_Base_Sanitizer
*
* @package AMP
*/
/**
* Class AMP_Base_Sanitizer
*/
abstract class AMP_Base_Sanitizer {
/**
* Value used with the height attribute in an $attributes parameter is empty.
*
* @since 0.3.3
*
* @const int
*/
const FALLBACK_HEIGHT = 400;
/**
* Value for <amp-image-lightbox> ID.
*
* @since 1.0
*
* @const string
*/
const AMP_IMAGE_LIGHTBOX_ID = 'amp-image-lightbox';
/**
* Placeholder for default args, to be set in child classes.
*
* @since 0.2
*
* @var array
*/
protected $DEFAULT_ARGS = array();
/**
* DOM.
*
* @var DOMDocument A standard PHP representation of an HTML document in object form.
*
* @since 0.2
*/
protected $dom;
/**
* Array of flags used to control sanitization.
*
* @var array {
* @type int $content_max_width
* @type bool $add_placeholder
* @type bool $use_document_element
* @type bool $require_https_src
* @type string[] $amp_allowed_tags
* @type string[] $amp_globally_allowed_attributes
* @type string[] $amp_layout_allowed_attributes
* @type array $amp_allowed_tags
* @type array $amp_globally_allowed_attributes
* @type array $amp_layout_allowed_attributes
* @type array $amp_bind_placeholder_prefix
* @type bool $allow_dirty_styles
* @type bool $allow_dirty_scripts
* @type bool $should_locate_sources
* @type callable $validation_error_callback
* }
*/
protected $args;
/**
* Flag to be set in child class' sanitize() method indicating if the
* HTML contained in the DOMDocument has been sanitized yet or not.
*
* @since 0.2
*
* @var bool
*/
protected $did_convert_elements = false;
/**
* The root element used for sanitization. Either html or body.
*
* @var DOMElement
*/
protected $root_element;
/**
* Keep track of nodes that should not be removed to prevent duplicated validation errors since sanitization is rejected.
*
* @var array
*/
private $should_not_removed_nodes = array();
/**
* AMP_Base_Sanitizer constructor.
*
* @since 0.2
*
* @param DOMDocument $dom Represents the HTML document to sanitize.
* @param array $args {
* Args.
*
* @type int $content_max_width
* @type bool $add_placeholder
* @type bool $require_https_src
* @type string[] $amp_allowed_tags
* @type string[] $amp_globally_allowed_attributes
* @type string[] $amp_layout_allowed_attributes
* }
*/
public function __construct( $dom, $args = array() ) {
$this->dom = $dom;
$this->args = array_merge( $this->DEFAULT_ARGS, $args );
if ( ! empty( $this->args['use_document_element'] ) ) {
$this->root_element = $this->dom->documentElement;
} else {
$this->root_element = $this->dom->getElementsByTagName( 'body' )->item( 0 );
}
}
/**
* Add filters to manipulate output during output buffering before the DOM is constructed.
*
* Add actions and filters before the page is rendered so that the sanitizer can fix issues during output buffering.
* This provides an alternative to manipulating the DOM in the sanitize method. This is a static function because
* it is invoked before the class is instantiated, as the DOM is not available yet. This method is only called
* when 'amp' theme support is present. It is conceptually similar to the AMP_Base_Embed_Handler class's register_embed
* method.
*
* @since 1.0
* @see \AMP_Base_Embed_Handler::register_embed()
*
* @param array $args Args.
*/
public static function add_buffering_hooks( $args = array() ) {}
/**
* Get mapping of HTML selectors to the AMP component selectors which they may be converted into.
*
* @return array Mapping.
*/
public function get_selector_conversion_mapping() {
return array();
}
/**
* Run logic before any sanitizers are run.
*
* After the sanitizers are instantiated but before calling sanitize on each of them, this
* method is called with list of all the instantiated sanitizers.
*
* @param AMP_Base_Sanitizer[] $sanitizers Sanitizers.
*/
public function init( $sanitizers ) {}
/**
* Sanitize the HTML contained in the DOMDocument received by the constructor
*/
abstract public function sanitize();
/**
* Return array of values that would be valid as an HTML `script` element.
*
* Array keys are AMP element names and array values are their respective
* Javascript URLs from https://cdn.ampproject.org
*
* @since 0.2
*
* @return string[] Returns component name as array key and JavaScript URL as array value,
* respectively. Will return an empty array if sanitization has yet to be run
* or if it did not find any HTML elements to convert to AMP equivalents.
*/
public function get_scripts() {
return array();
}
/**
* Return array of values that would be valid as an HTML `style` attribute.
*
* @since 0.4
* @deprecated As of 1.0, use get_stylesheets().
*
* @return array[][] Mapping of CSS selectors to arrays of properties.
*/
public function get_styles() {
return array();
}
/**
* Get stylesheets.
*
* @since 0.7
* @returns array Values are the CSS stylesheets. Keys are MD5 hashes of the stylesheets.
*/
public function get_stylesheets() {
$stylesheets = array();
foreach ( $this->get_styles() as $selector => $properties ) {
$stylesheet = sprintf( '%s { %s }', $selector, join( '; ', $properties ) . ';' );
$stylesheets[ md5( $stylesheet ) ] = $stylesheet;
}
return $stylesheets;
}
/**
* Get HTML body as DOMElement from DOMDocument received by the constructor.
*
* @deprecated Just reference $root_element instead.
* @return DOMElement The body element.
*/
protected function get_body_node() {
return $this->dom->getElementsByTagName( 'body' )->item( 0 );
}
/**
* Sanitizes a CSS dimension specifier while being sensitive to dimension context.
*
* @param string $value A valid CSS dimension specifier; e.g. 50, 50px, 50%.
* @param string $dimension 'width' or ignored. 'width' only affects $values ending in '%'.
*
* @return float|int|string Returns a numeric dimension value, or an empty string.
*/
public function sanitize_dimension( $value, $dimension ) {
// Allows 0 to be used as valid dimension.
if ( null === $value ) {
return '';
}
// Accepts both integers and floats & prevents negative values.
if ( is_numeric( $value ) ) {
return max( 0, floatval( $value ) );
}
if ( AMP_String_Utils::endswith( $value, 'px' ) ) {
return absint( $value );
}
if ( AMP_String_Utils::endswith( $value, '%' ) ) {
if ( 'width' === $dimension && isset( $this->args['content_max_width'] ) ) {
$percentage = absint( $value ) / 100;
return round( $percentage * $this->args['content_max_width'] );
}
}
return '';
}
/**
* Sets the layout, and possibly the 'height' and 'width' attributes.
*
* @param string[] $attributes {
* Attributes.
*
* @type int $height
* @type int $width
* @type string $sizes
* @type string $class
* @type string $layout
* }
* @return array Attributes.
*/
public function set_layout( $attributes ) {
if ( isset( $attributes['layout'] ) && ( 'fill' === $attributes['layout'] || 'flex-item' !== $attributes['layout'] ) ) {
return $attributes;
}
if ( empty( $attributes['height'] ) ) {
unset( $attributes['width'] );
$attributes['height'] = self::FALLBACK_HEIGHT;
}
if ( empty( $attributes['width'] ) ) {
$attributes['layout'] = 'fixed-height';
}
return $attributes;
}
/**
* Adds or appends key and value to list of attributes
*
* Adds key and value to list of attributes, or if the key already exists in the array
* it concatenates to existing attribute separator by a space or other supplied separator.
*
* @param string[] $attributes {
* Attributes.
*
* @type int $height
* @type int $width
* @type string $sizes
* @type string $class
* @type string $layout
* }
* @param string $key Valid associative array index to add.
* @param string $value Value to add or append to array indexed at the key.
* @param string $separator Optional; defaults to space but some other separator if needed.
*/
public function add_or_append_attribute( &$attributes, $key, $value, $separator = ' ' ) {
if ( isset( $attributes[ $key ] ) ) {
$attributes[ $key ] = trim( $attributes[ $key ] . $separator . $value );
} else {
$attributes[ $key ] = $value;
}
}
/**
* Decide if we should remove a src attribute if https is required.
*
* If not required, the implementing class may want to try and force https instead.
*
* @param string $src URL to convert to HTTPS if forced, or made empty if $args['require_https_src'].
* @param boolean $force_https Force setting of HTTPS if true.
* @return string URL which may have been updated with HTTPS, or may have been made empty.
*/
public function maybe_enforce_https_src( $src, $force_https = false ) {
$protocol = strtok( $src, ':' ); // @todo What about relative URLs? This should use wp_parse_url( $src, PHP_URL_SCHEME )
if ( 'https' !== $protocol ) {
// Check if https is required.
if ( isset( $this->args['require_https_src'] ) && true === $this->args['require_https_src'] ) {
// Remove the src. Let the implementing class decide what do from here.
$src = '';
} elseif ( ( ! isset( $this->args['require_https_src'] ) || false === $this->args['require_https_src'] )
&& true === $force_https ) {
// Don't remove the src, but force https instead.
$src = set_url_scheme( $src, 'https' );
}
}
return $src;
}
/**
* Removes an invalid child of a node.
*
* Also, calls the mutation callback for it.
* This tracks all the nodes that were removed.
*
* @since 0.7
*
* @param DOMNode|DOMElement $node The node to remove.
* @param array $validation_error Validation error details.
* @return bool Whether the node should have been removed, that is, that the node was sanitized for validity.
*/
public function remove_invalid_child( $node, $validation_error = array() ) {
// Prevent double-reporting nodes that are rejected for sanitization.
if ( isset( $this->should_not_removed_nodes[ $node->nodeName ] ) && in_array( $node, $this->should_not_removed_nodes[ $node->nodeName ], true ) ) {
return false;
}
$should_remove = $this->should_sanitize_validation_error( $validation_error, compact( 'node' ) );
if ( $should_remove ) {
$node->parentNode->removeChild( $node );
} else {
$this->should_not_removed_nodes[ $node->nodeName ][] = $node;
}
return $should_remove;
}
/**
* Removes an invalid attribute of a node.
*
* Also, calls the mutation callback for it.
* This tracks all the attributes that were removed.
*
* @since 0.7
*
* @param DOMElement $element The node for which to remove the attribute.
* @param DOMAttr|string $attribute The attribute to remove from the element.
* @param array $validation_error Validation error details.
* @return bool Whether the node should have been removed, that is, that the node was sanitized for validity.
*/
public function remove_invalid_attribute( $element, $attribute, $validation_error = array() ) {
if ( is_string( $attribute ) ) {
$node = $element->getAttributeNode( $attribute );
} else {
$node = $attribute;
}
$should_remove = $this->should_sanitize_validation_error( $validation_error, compact( 'node' ) );
if ( $should_remove ) {
$element->removeAttributeNode( $node );
}
return $should_remove;
}
/**
* Check whether or not sanitization should occur in response to validation error.
*
* @since 1.0
*
* @param array $validation_error Validation error.
* @param array $data Data including the node.
* @return bool Whether to sanitize.
*/
public function should_sanitize_validation_error( $validation_error, $data = array() ) {
if ( empty( $this->args['validation_error_callback'] ) || ! is_callable( $this->args['validation_error_callback'] ) ) {
return true;
}
$validation_error = $this->prepare_validation_error( $validation_error, $data );
return false !== call_user_func( $this->args['validation_error_callback'], $validation_error, $data );
}
/**
* Prepare validation error.
*
* @param array $error {
* Error.
*
* @type string $code Error code.
* }
* @param array $data {
* Data.
*
* @type DOMElement|DOMNode $node The removed node.
* }
* @return array Error.
*/
public function prepare_validation_error( array $error = array(), array $data = array() ) {
$node = null;
$matches = null;
if ( isset( $data['node'] ) && $data['node'] instanceof DOMNode ) {
$node = $data['node'];
$error['node_name'] = $node->nodeName;
if ( $node->parentNode ) {
$error['parent_name'] = $node->parentNode->nodeName;
}
}
if ( $node instanceof DOMElement ) {
if ( ! isset( $error['code'] ) ) {
$error['code'] = AMP_Validation_Error_Taxonomy::INVALID_ELEMENT_CODE;
}
if ( ! isset( $error['type'] ) ) {
$error['type'] = 'script' === $node->nodeName ? AMP_Validation_Error_Taxonomy::JS_ERROR_TYPE : AMP_Validation_Error_Taxonomy::HTML_ELEMENT_ERROR_TYPE;
}
if ( ! isset( $error['node_attributes'] ) ) {
$error['node_attributes'] = array();
foreach ( $node->attributes as $attribute ) {
$error['node_attributes'][ $attribute->nodeName ] = $attribute->nodeValue;
}
}
// Capture script contents.
if ( 'script' === $node->nodeName && ! $node->hasAttribute( 'src' ) ) {
$error['text'] = $node->textContent;
}
// Suppress 'ver' param from enqueued scripts and styles.
if ( 'script' === $node->nodeName && isset( $error['node_attributes']['src'] ) && false !== strpos( $error['node_attributes']['src'], 'ver=' ) ) {
$error['node_attributes']['src'] = add_query_arg( 'ver', '__normalized__', $error['node_attributes']['src'] );
} elseif ( 'link' === $node->nodeName && isset( $error['node_attributes']['href'] ) && false !== strpos( $error['node_attributes']['href'], 'ver=' ) ) {
$error['node_attributes']['href'] = add_query_arg( 'ver', '__normalized__', $error['node_attributes']['href'] );
}
} elseif ( $node instanceof DOMAttr ) {
if ( ! isset( $error['code'] ) ) {
$error['code'] = AMP_Validation_Error_Taxonomy::INVALID_ATTRIBUTE_CODE;
}
if ( ! isset( $error['type'] ) ) {
// If this is an attribute that begins with on, like onclick, it should be a js_error.
$error['type'] = preg_match( '/^on\w+/', $node->nodeName ) ? AMP_Validation_Error_Taxonomy::JS_ERROR_TYPE : AMP_Validation_Error_Taxonomy::HTML_ATTRIBUTE_ERROR_TYPE;
}
if ( ! isset( $error['element_attributes'] ) ) {
$error['element_attributes'] = array();
if ( $node->parentNode && $node->parentNode->hasAttributes() ) {
foreach ( $node->parentNode->attributes as $attribute ) {
$error['element_attributes'][ $attribute->nodeName ] = $attribute->nodeValue;
}
}
}
}
return $error;
}
/**
* Get data-amp-* values from the parent node 'figure' added by editor block.
*
* @param DOMElement $node Base node.
* @return array AMP data array.
*/
public function get_data_amp_attributes( $node ) {
$attributes = array();
// Editor blocks add 'figure' as the parent node for images. If this node has data-amp-layout then we should add this as the layout attribute.
$parent_node = $node->parentNode;
if ( 'figure' === $parent_node->tagName ) {
$parent_attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $parent_node );
if ( isset( $parent_attributes['data-amp-layout'] ) ) {
$attributes['layout'] = $parent_attributes['data-amp-layout'];
}
if ( isset( $parent_attributes['data-amp-noloading'] ) && true === filter_var( $parent_attributes['data-amp-noloading'], FILTER_VALIDATE_BOOLEAN ) ) {
$attributes['noloading'] = $parent_attributes['data-amp-noloading'];
}
}
return $attributes;
}
/**
* Set AMP attributes.
*
* @param array $attributes Array of attributes.
* @param array $amp_data Array of AMP attributes.
* @return array Updated attributes.
*/
public function filter_data_amp_attributes( $attributes, $amp_data ) {
if ( isset( $amp_data['layout'] ) ) {
$attributes['data-amp-layout'] = $amp_data['layout'];
}
if ( isset( $amp_data['noloading'] ) ) {
$attributes['data-amp-noloading'] = '';
}
return $attributes;
}
/**
* Set attributes to node's parent element according to layout.
*
* @param DOMElement $node Node.
* @param array $new_attributes Attributes array.
* @param string $layout Layout.
* @return array New attributes.
*/
public function filter_attachment_layout_attributes( $node, $new_attributes, $layout ) {
// The width has to be unset / auto in case of fixed-height.
if ( 'fixed-height' === $layout ) {
if ( ! isset( $new_attributes['height'] ) ) {
$new_attributes['height'] = self::FALLBACK_HEIGHT;
}
$new_attributes['width'] = 'auto';
$node->parentNode->setAttribute( 'style', 'height: ' . $new_attributes['height'] . 'px; width: auto;' );
// The parent element should have width/height set and position set in case of 'fill'.
} elseif ( 'fill' === $layout ) {
if ( ! isset( $new_attributes['height'] ) ) {
$new_attributes['height'] = self::FALLBACK_HEIGHT;
}
$node->parentNode->setAttribute( 'style', 'position:relative; width: 100%; height: ' . $new_attributes['height'] . 'px;' );
unset( $new_attributes['width'] );
unset( $new_attributes['height'] );
} elseif ( 'responsive' === $layout ) {
$node->parentNode->setAttribute( 'style', 'position:relative; width: 100%; height: auto' );
} elseif ( 'fixed' === $layout ) {
if ( ! isset( $new_attributes['height'] ) ) {
$new_attributes['height'] = self::FALLBACK_HEIGHT;
}
}
return $new_attributes;
}
/**
* Add <amp-image-lightbox> element to body tag if it doesn't exist yet.
*/
public function maybe_add_amp_image_lightbox_node() {
$nodes = $this->dom->getElementById( self::AMP_IMAGE_LIGHTBOX_ID );
if ( null !== $nodes ) {
return;
}
$nodes = $this->dom->getElementsByTagName( 'body' );
if ( ! $nodes->length ) {
return;
}
$body_node = $nodes->item( 0 );
$amp_image_lightbox = AMP_DOM_Utils::create_node(
$this->dom,
'amp-image-lightbox',
array(
'id' => self::AMP_IMAGE_LIGHTBOX_ID,
'layout' => 'nodisplay',
'data-close-button-aria-label' => __( 'Close', 'amp' ),
)
);
$body_node->appendChild( $amp_image_lightbox );
}
}

View File

@ -0,0 +1,310 @@
<?php
/**
* Class AMP_Blacklist_Sanitizer
*
* @package AMP
*/
/**
* Strips blacklisted tags and attributes from content.
*
* See following for blacklist:
* https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#html-tags
*
* @since 0.5 This has been replaced by AMP_Tag_And_Attribute_Sanitizer but is kept around for back-compat.
* @deprecated
*/
class AMP_Blacklist_Sanitizer extends AMP_Base_Sanitizer {
const PATTERN_REL_WP_ATTACHMENT = '#wp-att-([\d]+)#';
/**
* Default args.
*
* @var array
*/
protected $DEFAULT_ARGS = array(
'add_blacklisted_protocols' => array(),
'add_blacklisted_tags' => array(),
'add_blacklisted_attributes' => array(),
);
/**
* Sanitize.
*/
public function sanitize() {
_deprecated_function( __METHOD__, '0.7', 'AMP_Tag_And_Attribute_Sanitizer::sanitize' );
$blacklisted_tags = $this->get_blacklisted_tags();
$blacklisted_attributes = $this->get_blacklisted_attributes();
$blacklisted_protocols = $this->get_blacklisted_protocols();
$body = $this->root_element;
$this->strip_tags( $body, $blacklisted_tags );
$this->strip_attributes_recursive( $body, $blacklisted_attributes, $blacklisted_protocols );
}
/**
* Strip attributes recursively.
*
* @param DOMNode $node DOM Node.
* @param array $bad_attributes Bad attributes.
* @param array $bad_protocols Bad protocols.
*/
private function strip_attributes_recursive( $node, $bad_attributes, $bad_protocols ) {
if ( XML_ELEMENT_NODE !== $node->nodeType ) {
return;
}
$node_name = $node->nodeName;
// Some nodes may contain valid content but are themselves invalid.
// Remove the node but preserve the children.
if ( 'font' === $node_name ) {
$this->replace_node_with_children( $node, $bad_attributes, $bad_protocols );
return;
} elseif ( 'a' === $node_name && false === $this->validate_a_node( $node ) ) {
$this->replace_node_with_children( $node, $bad_attributes, $bad_protocols );
return;
}
if ( $node->hasAttributes() ) {
$length = $node->attributes->length;
for ( $i = $length - 1; $i >= 0; $i-- ) {
$attribute = $node->attributes->item( $i );
$attribute_name = strtolower( $attribute->name );
if ( in_array( $attribute_name, $bad_attributes, true ) ) {
$this->remove_invalid_attribute( $node, $attribute_name );
continue;
}
// The on* attributes (like onclick) are a special case.
if ( 0 === stripos( $attribute_name, 'on' ) && 'on' !== $attribute_name ) {
$this->remove_invalid_attribute( $node, $attribute_name );
continue;
} elseif ( 'a' === $node_name ) {
$this->sanitize_a_attribute( $node, $attribute );
}
}
}
$length = $node->childNodes->length;
for ( $i = $length - 1; $i >= 0; $i-- ) {
$child_node = $node->childNodes->item( $i );
$this->strip_attributes_recursive( $child_node, $bad_attributes, $bad_protocols );
}
}
/**
* Strip tags.
*
* @param DOMElement $node Node.
* @param string[] $tag_names Tag names.
*/
private function strip_tags( $node, $tag_names ) {
foreach ( $tag_names as $tag_name ) {
$elements = $node->getElementsByTagName( $tag_name );
$length = $elements->length;
if ( 0 === $length ) {
continue;
}
for ( $i = $length - 1; $i >= 0; $i-- ) {
$element = $elements->item( $i );
$parent_node = $element->parentNode;
$this->remove_invalid_child( $element );
if ( 'body' !== $parent_node->nodeName && AMP_DOM_Utils::is_node_empty( $parent_node ) ) {
$this->remove_invalid_child( $parent_node );
}
}
}
}
/**
* Sanitize attribute.
*
* @param DOMElement $node Node.
* @param DOMAttr $attribute Attribute.
*/
private function sanitize_a_attribute( $node, $attribute ) {
$attribute_name = strtolower( $attribute->name );
if ( 'rel' === $attribute_name ) {
$old_value = $attribute->value;
$new_value = trim( preg_replace( self::PATTERN_REL_WP_ATTACHMENT, '', $old_value ) );
if ( empty( $new_value ) ) {
$this->remove_invalid_attribute( $node, $attribute_name );
} elseif ( $old_value !== $new_value ) {
$node->setAttribute( $attribute_name, $new_value );
}
} elseif ( 'rev' === $attribute_name ) {
// rev removed from HTML5 spec, which was used by Jetpack Markdown.
$this->remove_invalid_attribute( $node, $attribute_name );
} elseif ( 'target' === $attribute_name ) {
// _blank is the only allowed value and it must be lowercase.
// replace _new with _blank and others should simply be removed.
$old_value = strtolower( $attribute->value );
if ( '_blank' === $old_value || '_new' === $old_value ) {
// _new is not allowed; swap with _blank
$node->setAttribute( $attribute_name, '_blank' );
} else {
// Only _blank is allowed.
$this->remove_invalid_attribute( $node, $attribute_name );
}
}
}
/**
* Validate node.
*
* @param DOMElement $node Node.
* @return bool
*/
private function validate_a_node( $node ) {
// Get the href attribute.
$href = $node->getAttribute( 'href' );
if ( empty( $href ) ) {
/*
* If no href, check that a is an anchor or not.
* We don't need to validate anchors any further.
*/
return $node->hasAttribute( 'name' ) || $node->hasAttribute( 'id' );
}
// If this is an anchor link, just return true.
if ( 0 === strpos( $href, '#' ) ) {
return true;
}
// If the href starts with a '/', append the home_url to it for validation purposes.
if ( 0 === stripos( $href, '/' ) ) {
$href = untrailingslashit( get_home_url() ) . $href;
}
$valid_protocols = array( 'http', 'https', 'mailto', 'sms', 'tel', 'viber', 'whatsapp' );
$special_protocols = array( 'tel', 'sms' ); // These ones don't valid with `filter_var+FILTER_VALIDATE_URL`.
$protocol = strtok( $href, ':' );
if ( false === filter_var( $href, FILTER_VALIDATE_URL )
&& ! in_array( $protocol, $special_protocols, true ) ) {
return false;
}
if ( ! in_array( $protocol, $valid_protocols, true ) ) {
return false;
}
return true;
}
/**
* Replace node with children.
*
* @param DOMElement $node Node.
* @param array $bad_attributes Bad attributes.
* @param array $bad_protocols Bad protocols.
*/
private function replace_node_with_children( $node, $bad_attributes, $bad_protocols ) {
// If the node has children and also has a parent node,
// clone and re-add all the children just before current node.
if ( $node->hasChildNodes() && $node->parentNode ) {
foreach ( $node->childNodes as $child_node ) {
$new_child = $child_node->cloneNode( true );
$this->strip_attributes_recursive( $new_child, $bad_attributes, $bad_protocols );
$node->parentNode->insertBefore( $new_child, $node );
}
}
// Remove the node from the parent, if defined.
if ( $node->parentNode ) {
$this->remove_invalid_child( $node );
}
}
/**
* Merge defaults with args.
*
* @param string $key Key.
* @param array $values Values.
* @return array Merged args.
*/
private function merge_defaults_with_args( $key, $values ) {
// Merge default values with user specified args.
if ( ! empty( $this->args[ $key ] )
&& is_array( $this->args[ $key ] ) ) {
$values = array_merge( $values, $this->args[ $key ] );
}
return $values;
}
/**
* Get blacklisted protocols.
*
* @return array Protocols.
*/
private function get_blacklisted_protocols() {
return $this->merge_defaults_with_args(
'add_blacklisted_protocols',
array(
'javascript',
)
);
}
/**
* Get blacklisted tags.
*
* @return array Tags.
*/
private function get_blacklisted_tags() {
return $this->merge_defaults_with_args(
'add_blacklisted_tags',
array(
'script',
'noscript',
'style',
'frame',
'frameset',
'object',
'param',
'applet',
'form',
'label',
'input',
'textarea',
'select',
'option',
'link',
'picture',
// Sanitizers run after embed handlers, so if anything wasn't matched, it needs to be removed.
'embed',
'embedvideo',
// Other weird ones.
'comments-count',
)
);
}
/**
* Get blacklisted attributes.
*
* @return array Attributes.
*/
private function get_blacklisted_attributes() {
return $this->merge_defaults_with_args(
'add_blacklisted_attributes',
array(
'style',
'size',
'clear',
'align',
'valign',
)
);
}
}

View File

@ -0,0 +1,137 @@
<?php
/**
* Class AMP_Block_Sanitizer.
*
* @package AMP
*/
/**
* Class AMP_Block_Sanitizer
*
* Modifies elements created as blocks to match the blocks' AMP-specific configuration.
*/
class AMP_Block_Sanitizer extends AMP_Base_Sanitizer {
/**
* Tag.
*
* @var string Figure tag to identify wrapper around AMP elements.
* @since 1.0
*/
public static $tag = 'figure';
/**
* Sanitize the AMP elements contained by <figure> element where necessary.
*
* @since 0.2
*/
public function sanitize() {
$nodes = $this->dom->getElementsByTagName( self::$tag );
$num_nodes = $nodes->length;
if ( 0 === $num_nodes ) {
return;
}
for ( $i = $num_nodes - 1; $i >= 0; $i-- ) {
$node = $nodes->item( $i );
// We are only looking for <figure> elements which have wp-block-embed as class.
$class = (string) $node->getAttribute( 'class' );
if ( false === strpos( $class, 'wp-block-embed' ) ) {
continue;
}
// Remove classes like wp-embed-aspect-16-9 since responsive layout is handled by AMP's layout system.
$node->setAttribute( 'class', preg_replace( '/(?<=^|\s)wp-embed-aspect-\d+-\d+(?=\s|$)/', '', $class ) );
// We're looking for <figure> elements that have one child node only.
if ( 1 !== count( $node->childNodes ) ) {
continue;
}
$attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node );
// We are looking for <figure> elements with layout attribute only.
if (
! isset( $attributes['data-amp-layout'] ) &&
! isset( $attributes['data-amp-noloading'] ) &&
! isset( $attributes['data-amp-lightbox'] )
) {
continue;
}
$amp_el_found = false;
foreach ( $node->childNodes as $child_node ) {
// We are looking for child elements which start with 'amp-'.
if ( 0 !== strpos( $child_node->tagName, 'amp-' ) ) {
continue;
}
$amp_el_found = true;
$this->set_attributes( $child_node, $node, $attributes );
}
if ( false === $amp_el_found ) {
continue;
}
$this->did_convert_elements = true;
}
}
/**
* Sets necessary attributes to both parent and AMP element node.
*
* @param DOMNode $node AMP element node.
* @param DOMNode $parent_node <figure> node.
* @param array $attributes Current attributes of the AMP element.
*/
protected function set_attributes( $node, $parent_node, $attributes ) {
if ( isset( $attributes['data-amp-layout'] ) ) {
$node->setAttribute( 'layout', $attributes['data-amp-layout'] );
}
if ( isset( $attributes['data-amp-noloading'] ) && true === filter_var( $attributes['data-amp-noloading'], FILTER_VALIDATE_BOOLEAN ) ) {
$node->setAttribute( 'noloading', '' );
}
$layout = $node->getAttribute( 'layout' );
// The width has to be unset / auto in case of fixed-height.
if ( 'fixed-height' === $layout ) {
if ( ! isset( $attributes['height'] ) ) {
$node->setAttribute( 'height', self::FALLBACK_HEIGHT );
}
$node->setAttribute( 'width', 'auto' );
$height = $node->getAttribute( 'height' );
if ( is_numeric( $height ) ) {
$height .= 'px';
}
$parent_node->setAttribute( 'style', "height: $height; width: auto;" );
// The parent element should have width/height set and position set in case of 'fill'.
} elseif ( 'fill' === $layout ) {
if ( ! isset( $attributes['height'] ) ) {
$attributes['height'] = self::FALLBACK_HEIGHT;
}
$parent_node->setAttribute( 'style', 'position:relative; width: 100%; height: ' . $attributes['height'] . 'px;' );
$node->removeAttribute( 'width' );
$node->removeAttribute( 'height' );
} elseif ( 'responsive' === $layout ) {
$parent_node->setAttribute( 'style', 'position:relative; width: 100%; height: auto' );
} elseif ( 'fixed' === $layout ) {
if ( ! isset( $attributes['height'] ) ) {
$node->setAttribute( 'height', self::FALLBACK_HEIGHT );
}
}
// Set the fallback layout in case needed.
$attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node );
$attributes = $this->set_layout( $attributes );
if ( $layout !== $attributes['layout'] ) {
$node->setAttribute( 'layout', $attributes['layout'] );
}
}
}

View File

@ -0,0 +1,218 @@
<?php
/**
* Class AMP_Comments_Sanitizer.
*
* @package AMP
*/
/**
* Class AMP_Comments_Sanitizer
*
* Strips and corrects attributes in forms.
*/
class AMP_Comments_Sanitizer extends AMP_Base_Sanitizer {
/**
* Default args.
*
* @since 1.1
*
* @var array
*/
protected $DEFAULT_ARGS = array(
'comment_live_list' => false,
);
/**
* Pre-process the comment form and comment list for AMP.
*
* @since 0.7
*/
public function sanitize() {
foreach ( $this->dom->getElementsByTagName( 'form' ) as $comment_form ) {
/**
* Comment form.
*
* @var DOMElement $comment_form
*/
$action = $comment_form->getAttribute( 'action-xhr' );
if ( ! $action ) {
$action = $comment_form->getAttribute( 'action' );
}
$action_path = wp_parse_url( $action, PHP_URL_PATH );
if ( preg_match( '#/wp-comments-post\.php$#', $action_path ) ) {
$this->process_comment_form( $comment_form );
}
}
if ( ! empty( $this->args['comments_live_list'] ) ) {
$xpath = new DOMXPath( $this->dom );
$comments = $xpath->query( '//amp-live-list/*[ @items ]/*[ starts-with( @id, "comment-" ) ]' );
foreach ( $comments as $comment ) {
$this->add_amp_live_list_comment_attributes( $comment );
}
}
}
/**
* Comment form.
*
* @since 0.7
*
* @param DOMElement $comment_form Comment form.
*/
protected function process_comment_form( $comment_form ) {
/**
* Element.
*
* @var DOMElement $element
*/
/**
* Named input elements.
*
* @var DOMElement[][] $form_fields
*/
$form_fields = array();
foreach ( $comment_form->getElementsByTagName( 'input' ) as $element ) {
$name = $element->getAttribute( 'name' );
if ( $name ) {
$form_fields[ $name ][] = $element;
}
}
foreach ( $comment_form->getElementsByTagName( 'textarea' ) as $element ) {
$name = $element->getAttribute( 'name' );
if ( $name ) {
$form_fields[ $name ][] = $element;
}
}
if ( empty( $form_fields['comment_post_ID'] ) ) {
return;
}
$post_id = (int) $form_fields['comment_post_ID'][0]->getAttribute( 'value' );
$state_id = AMP_Theme_Support::get_comment_form_state_id( $post_id );
$form_state = array(
'values' => array(),
'submitting' => false,
'replyToName' => '',
);
if ( ! empty( $form_fields['comment_parent'] ) ) {
$comment_id = (int) $form_fields['comment_parent'][0]->getAttribute( 'value' );
if ( $comment_id ) {
$reply_comment = get_comment( $comment_id );
if ( $reply_comment ) {
$form_state['replyToName'] = $reply_comment->comment_author;
}
}
}
$amp_bind_attr_format = AMP_DOM_Utils::get_amp_bind_placeholder_prefix() . '%s';
foreach ( $form_fields as $name => $form_field ) {
foreach ( $form_field as $element ) {
// @todo Radio and checkbox inputs are not supported yet.
if ( in_array( strtolower( $element->getAttribute( 'type' ) ), array( 'checkbox', 'radio' ), true ) ) {
continue;
}
$element->setAttribute( sprintf( $amp_bind_attr_format, 'disabled' ), "$state_id.submitting" );
if ( 'textarea' === strtolower( $element->nodeName ) ) {
$form_state['values'][ $name ] = $element->textContent;
$element->setAttribute( sprintf( $amp_bind_attr_format, 'text' ), "$state_id.values.$name" );
} else {
$form_state['values'][ $name ] = $element->hasAttribute( 'value' ) ? $element->getAttribute( 'value' ) : '';
$element->setAttribute( sprintf( $amp_bind_attr_format, 'value' ), "$state_id.values.$name" );
}
// Update the state in response to changing the input.
$element->setAttribute(
'on',
sprintf(
'change:AMP.setState( { %s: { values: { %s: event.value } } } )',
$state_id,
wp_json_encode( $name )
)
);
}
}
// Add amp-state to the document.
$amp_state = $this->dom->createElement( 'amp-state' );
$amp_state->setAttribute( 'id', $state_id );
$script = $this->dom->createElement( 'script' );
$script->setAttribute( 'type', 'application/json' );
$amp_state->appendChild( $script );
$script->appendChild( $this->dom->createTextNode( wp_json_encode( $form_state ) ) );
$comment_form->insertBefore( $amp_state, $comment_form->firstChild );
// Update state when submitting form.
$form_reset_state = $form_state;
unset(
$form_reset_state['values']['author'],
$form_reset_state['values']['email'],
$form_reset_state['values']['url']
);
$on = array(
// Disable the form when submitting.
sprintf(
'submit:AMP.setState( { %s: { submitting: true } } )',
wp_json_encode( $state_id )
),
// Re-enable the form fields when the submission fails.
sprintf(
'submit-error:AMP.setState( { %s: { submitting: false } } )',
wp_json_encode( $state_id )
),
// Reset the form to its initial state (with enabled form fields), except for the author, email, and url.
sprintf(
'submit-success:AMP.setState( { %s: %s } )',
$state_id,
wp_json_encode( $form_reset_state )
),
);
$comment_form->setAttribute( 'on', implode( ';', $on ) );
}
/**
* Add attributes to comment elements when comments are being presented in amp-live-list, when comments_live_list theme support flag is present.
*
* @since 1.1
*
* @param DOMElement $comment_element Comment element.
*/
protected function add_amp_live_list_comment_attributes( $comment_element ) {
$comment_id = (int) str_replace( 'comment-', '', $comment_element->getAttribute( 'id' ) );
if ( ! $comment_id ) {
return;
}
$comment_object = get_comment( $comment_id );
// Skip if the comment is not valid or the comment has a parent, since in that case it is not relevant for amp-live-list.
if ( ! ( $comment_object instanceof WP_Comment ) || $comment_object->comment_parent ) {
return;
}
$comment_element->setAttribute( 'data-sort-time', strtotime( $comment_object->comment_date ) );
$update_time = strtotime( $comment_object->comment_date );
// Ensure the top-level data-update-time reflects the max time of the comments in the thread.
$children = $comment_object->get_children(
array(
'format' => 'flat',
'hierarchical' => 'flat',
'orderby' => 'none',
)
);
foreach ( $children as $child_comment ) {
$update_time = max( strtotime( $child_comment->comment_date ), $update_time );
}
$comment_element->setAttribute( 'data-update-time', $update_time );
}
}

View File

@ -0,0 +1,47 @@
<?php
/**
* Class AMP_Embed_Sanitizer
*
* @package AMP
*/
/**
* Class AMP_Embed_Sanitizer
*
* Calls sanitize_raw_embeds method on embed handlers.
*/
class AMP_Embed_Sanitizer extends AMP_Base_Sanitizer {
/**
* Embed handlers.
*
* @var AMP_Base_Embed_Handler[] AMP_Base_Embed_Handler[]
*/
private $embed_handlers = array();
/**
* AMP_Embed_Sanitizer constructor.
*
* @param DOMDocument $dom DOM.
* @param array $args Args.
*/
public function __construct( $dom, $args = array() ) {
parent::__construct( $dom, $args );
if ( ! empty( $this->args['embed_handlers'] ) ) {
$this->embed_handlers = $this->args['embed_handlers'];
}
}
/**
* Checks if each embed_handler has sanitize_raw_method and calls it.
*/
public function sanitize() {
foreach ( $this->embed_handlers as $embed_handler ) {
if ( method_exists( $embed_handler, 'sanitize_raw_embeds' ) ) {
$embed_handler->sanitize_raw_embeds( $this->dom );
}
}
}
}

View File

@ -0,0 +1,146 @@
<?php
/**
* Class AMP_Form_Sanitizer.
*
* @package AMP
* @since 0.7
*/
/**
* Class AMP_Form_Sanitizer
*
* Strips and corrects attributes in forms.
*
* @since 0.7
*/
class AMP_Form_Sanitizer extends AMP_Base_Sanitizer {
/**
* Tag.
*
* @var string HTML <form> tag to identify and process.
*
* @since 0.7
*/
public static $tag = 'form';
/**
* Sanitize the <form> elements from the HTML contained in this instance's DOMDocument.
*
* @link https://www.ampproject.org/docs/reference/components/amp-form
* @since 0.7
*/
public function sanitize() {
/**
* Node list.
*
* @var DOMNodeList $node
*/
$nodes = $this->dom->getElementsByTagName( self::$tag );
$num_nodes = $nodes->length;
if ( 0 === $num_nodes ) {
return;
}
for ( $i = $num_nodes - 1; $i >= 0; $i-- ) {
$node = $nodes->item( $i );
if ( ! $node instanceof DOMElement ) {
continue;
}
// In HTML, the default method is 'get'.
$method = 'get';
if ( $node->getAttribute( 'method' ) ) {
$method = strtolower( $node->getAttribute( 'method' ) );
} else {
$node->setAttribute( 'method', $method );
}
/*
* In HTML, the default action is just the current URL that the page is served from.
* The action "specifies a server endpoint to handle the form input. The value must be an
* https URL and must not be a link to a CDN".
*/
if ( ! $node->getAttribute( 'action' ) ) {
$action_url = esc_url_raw( '//' . $_SERVER['HTTP_HOST'] . wp_unslash( $_SERVER['REQUEST_URI'] ) );
} else {
$action_url = $node->getAttribute( 'action' );
// Check if action_url is a relative path and add the host to it.
if ( ! preg_match( '#^(https?:)?//#', $action_url ) ) {
$action_url = esc_url_raw( '//' . $_SERVER['HTTP_HOST'] . $action_url );
}
}
$xhr_action = $node->getAttribute( 'action-xhr' );
// Make HTTP URLs protocol-less, since HTTPS is required for forms.
if ( 'http://' === strtolower( substr( $action_url, 0, 7 ) ) ) {
$action_url = substr( $action_url, 5 );
}
/*
* According to the AMP spec:
* For GET submissions, provide at least one of action or action-xhr.
* This attribute is required for method=GET. For method=POST, the
* action attribute is invalid, use action-xhr instead.
*/
if ( 'get' === $method ) {
if ( $action_url !== $node->getAttribute( 'action' ) ) {
$node->setAttribute( 'action', $action_url );
}
} elseif ( 'post' === $method ) {
$node->removeAttribute( 'action' );
if ( ! $xhr_action ) {
// record that action was converted tp action-xhr.
$action_url = add_query_arg( '_wp_amp_action_xhr_converted', 1, $action_url );
$node->setAttribute( 'action-xhr', $action_url );
// Append error handler if not found.
$this->ensure_submit_error_element( $node );
} elseif ( 'http://' === substr( $xhr_action, 0, 7 ) ) {
$node->setAttribute( 'action-xhr', substr( $xhr_action, 5 ) );
}
}
/*
* The target "indicates where to display the form response after submitting the form.
* The value must be _blank or _top". The _self and _parent values are treated
* as synonymous with _top, and anything else is treated like _blank.
*/
$target = $node->getAttribute( 'target' );
if ( '_top' !== $target ) {
if ( ! $target || in_array( $target, array( '_self', '_parent' ), true ) ) {
$node->setAttribute( 'target', '_top' );
} elseif ( '_blank' !== $target ) {
$node->setAttribute( 'target', '_blank' );
}
}
}
}
/**
* Checks if the form has an error handler else create one if not.
*
* @link https://www.ampproject.org/docs/reference/components/amp-form#success/error-response-rendering
* @since 0.7
*
* @param DOMElement $form The form node to check.
*/
public function ensure_submit_error_element( $form ) {
$templates = $form->getElementsByTagName( 'template' );
for ( $i = $templates->length - 1; $i >= 0; $i-- ) {
if ( $templates->item( $i )->parentNode->hasAttribute( 'submit-error' ) ) {
return; // Found error template, do nothing.
}
}
$div = $this->dom->createElement( 'div' );
$template = $this->dom->createElement( 'template' );
$mustache = $this->dom->createTextNode( '{{{error}}}' );
$div->setAttribute( 'submit-error', '' );
$template->setAttribute( 'type', 'amp-mustache' );
$template->appendChild( $mustache );
$div->appendChild( $template );
$form->appendChild( $div );
}
}

View File

@ -0,0 +1,211 @@
<?php
/**
* Class AMP_Gallery_Block_Sanitizer.
*
* @package AMP
*/
/**
* Class AMP_Gallery_Block_Sanitizer
*
* Modifies gallery block to match the block's AMP-specific configuration.
*/
class AMP_Gallery_Block_Sanitizer extends AMP_Base_Sanitizer {
/**
* Value used for width of amp-carousel.
*
* @since 1.0
*
* @const int
*/
const FALLBACK_WIDTH = 600;
/**
* Value used for height of amp-carousel.
*
* @since 1.0
*
* @const int
*/
const FALLBACK_HEIGHT = 480;
/**
* Tag.
*
* @since 1.0
*
* @var string Ul tag to identify wrapper around gallery block.
*/
public static $tag = 'ul';
/**
* Expected class of the wrapper around the gallery block.
*
* @since 1.0
*
* @var string
*/
public static $class = 'wp-block-gallery';
/**
* Array of flags used to control sanitization.
*
* @var array {
* @type int $content_max_width Max width of content.
* @type bool $carousel_required Whether carousels are required. This is used when amp theme support is not present, for back-compat.
* }
*/
protected $args;
/**
* Default args.
*
* @var array
*/
protected $DEFAULT_ARGS = array(
'carousel_required' => false,
);
/**
* Sanitize the gallery block contained by <ul> element where necessary.
*
* @since 0.2
*/
public function sanitize() {
$nodes = $this->dom->getElementsByTagName( self::$tag );
$num_nodes = $nodes->length;
if ( 0 === $num_nodes ) {
return;
}
for ( $i = $num_nodes - 1; $i >= 0; $i-- ) {
$node = $nodes->item( $i );
// We're looking for <ul> elements that have at least one child and the proper class.
if ( 0 === count( $node->childNodes ) || false === strpos( $node->getAttribute( 'class' ), self::$class ) ) {
continue;
}
$attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node );
$is_amp_lightbox = isset( $attributes['data-amp-lightbox'] ) && true === filter_var( $attributes['data-amp-lightbox'], FILTER_VALIDATE_BOOLEAN );
$is_amp_carousel = ! empty( $this->args['carousel_required'] ) || ( isset( $attributes['data-amp-carousel'] ) && true === filter_var( $attributes['data-amp-carousel'], FILTER_VALIDATE_BOOLEAN ) );
// We are only looking for <ul> elements which have amp-carousel / amp-lightbox true.
if ( ! $is_amp_carousel && ! $is_amp_lightbox ) {
continue;
}
// If lightbox is set, we should add lightbox feature to the gallery images.
if ( $is_amp_lightbox ) {
$this->add_lightbox_attributes_to_image_nodes( $node );
$this->maybe_add_amp_image_lightbox_node();
}
// If amp-carousel is not set, nothing else to do here.
if ( ! $is_amp_carousel ) {
continue;
}
$images = array();
// If it's not AMP lightbox, look for links first.
if ( ! $is_amp_lightbox ) {
foreach ( $node->getElementsByTagName( 'a' ) as $element ) {
$images[] = $element;
}
}
// If not linking to anything then look for <amp-img>.
if ( empty( $images ) ) {
foreach ( $node->getElementsByTagName( 'amp-img' ) as $element ) {
$images[] = $element;
}
}
// Skip if no images found.
if ( empty( $images ) ) {
continue;
}
$amp_carousel = AMP_DOM_Utils::create_node(
$this->dom,
'amp-carousel',
array(
'height' => $this->get_carousel_height( $node ),
'type' => 'slides',
'layout' => 'fixed-height',
)
);
foreach ( $images as $image ) {
$amp_carousel->appendChild( $image );
}
$node->parentNode->replaceChild( $amp_carousel, $node );
}
$this->did_convert_elements = true;
}
/**
* Get carousel height by containing images.
*
* @param DOMElement $element The UL element.
* @return int Height.
*/
protected function get_carousel_height( $element ) {
$images = $element->getElementsByTagName( 'amp-img' );
$num_images = $images->length;
$max_height = 0;
$max_width = 0;
if ( 0 === $num_images ) {
return self::FALLBACK_HEIGHT;
}
foreach ( $images as $image ) {
/**
* Image.
*
* @var DOMElement $image
*/
$image_height = $image->getAttribute( 'height' );
if ( is_numeric( $image_height ) ) {
$max_height = max( $max_height, $image_height );
}
$image_width = $image->getAttribute( 'height' );
if ( is_numeric( $image_width ) ) {
$max_width = max( $max_width, $image_width );
}
}
if ( ! empty( $this->args['content_max_width'] ) && $max_height > 0 && $max_width > $this->args['content_max_width'] ) {
$max_height = ( $max_width * $this->args['content_max_width'] ) / $max_height;
}
return ! $max_height ? self::FALLBACK_HEIGHT : $max_height;
}
/**
* Set lightbox related attributes to <amp-img> within gallery.
*
* @param DOMElement $element The UL element.
*/
protected function add_lightbox_attributes_to_image_nodes( $element ) {
$images = $element->getElementsByTagName( 'amp-img' );
$num_images = $images->length;
if ( 0 === $num_images ) {
return;
}
$attributes = array(
'data-amp-lightbox' => '',
'on' => 'tap:' . self::AMP_IMAGE_LIGHTBOX_ID,
'role' => 'button',
'tabindex' => 0,
);
for ( $j = $num_images - 1; $j >= 0; $j-- ) {
$image_node = $images->item( $j );
foreach ( $attributes as $att => $value ) {
$image_node->setAttribute( $att, $value );
}
}
}
}

View File

@ -0,0 +1,218 @@
<?php
/**
* Class AMP_Iframe_Sanitizer
*
* @package AMP
*/
/**
* Class AMP_Iframe_Sanitizer
*
* Converts <iframe> tags to <amp-iframe>
*/
class AMP_Iframe_Sanitizer extends AMP_Base_Sanitizer {
use AMP_Noscript_Fallback;
/**
* Value used for height attribute when $attributes['height'] is empty.
*
* @since 0.2
*
* @const int
*/
const FALLBACK_HEIGHT = 400;
/**
* Default values for sandboxing IFrame.
*
* @since 0.2
*
* @const int
*/
const SANDBOX_DEFAULTS = 'allow-scripts allow-same-origin';
/**
* Tag.
*
* @var string HTML <iframe> tag to identify and replace with AMP version.
*
* @since 0.2
*/
public static $tag = 'iframe';
/**
* Default args.
*
* @var array
*/
protected $DEFAULT_ARGS = array(
'add_placeholder' => false,
'add_noscript_fallback' => true,
);
/**
* Get mapping of HTML selectors to the AMP component selectors which they may be converted into.
*
* @return array Mapping.
*/
public function get_selector_conversion_mapping() {
return array(
'iframe' => array(
'amp-iframe',
),
);
}
/**
* Sanitize the <iframe> elements from the HTML contained in this instance's DOMDocument.
*
* @since 0.2
*/
public function sanitize() {
$nodes = $this->dom->getElementsByTagName( self::$tag );
$num_nodes = $nodes->length;
if ( 0 === $num_nodes ) {
return;
}
if ( $this->args['add_noscript_fallback'] ) {
$this->initialize_noscript_allowed_attributes( self::$tag );
}
for ( $i = $num_nodes - 1; $i >= 0; $i-- ) {
$node = $nodes->item( $i );
// Skip element if already inside of an AMP element as a noscript fallback.
if ( $this->is_inside_amp_noscript( $node ) ) {
continue;
}
$normalized_attributes = $this->normalize_attributes( AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node ) );
/**
* If the src doesn't exist, remove the node. Either it never
* existed or was invalidated while filtering attributes above.
*
* @todo: add a filter to allow for a fallback element in this instance.
* @see: https://github.com/ampproject/amphtml/issues/2261
*/
if ( empty( $normalized_attributes['src'] ) ) {
$this->remove_invalid_child( $node );
continue;
}
$this->did_convert_elements = true;
$normalized_attributes = $this->set_layout( $normalized_attributes );
if ( empty( $normalized_attributes['layout'] ) && ! empty( $normalized_attributes['width'] ) && ! empty( $normalized_attributes['height'] ) ) {
$normalized_attributes['layout'] = 'intrinsic';
$this->add_or_append_attribute( $normalized_attributes, 'class', 'amp-wp-enforced-sizes' );
}
$new_node = AMP_DOM_Utils::create_node( $this->dom, 'amp-iframe', $normalized_attributes );
if ( true === $this->args['add_placeholder'] ) {
$placeholder_node = $this->build_placeholder( $normalized_attributes );
$new_node->appendChild( $placeholder_node );
}
$node->parentNode->replaceChild( $new_node, $node );
if ( $this->args['add_noscript_fallback'] ) {
$node->setAttribute( 'src', $normalized_attributes['src'] );
// Preserve original node in noscript for no-JS environments.
$this->append_old_node_noscript( $new_node, $node, $this->dom );
}
}
}
/**
* Normalize HTML attributes for <amp-iframe> elements.
*
* @param string[] $attributes {
* Attributes.
*
* @type string $src IFrame URL - Empty if HTTPS required per $this->args['require_https_src']
* @type int $width <iframe> width attribute - Set to numeric value if px or %
* @type int $height <iframe> width attribute - Set to numeric value if px or %
* @type string $sandbox <iframe> `sandbox` attribute - Pass along if found; default to value of self::SANDBOX_DEFAULTS
* @type string $class <iframe> `class` attribute - Pass along if found
* @type string $sizes <iframe> `sizes` attribute - Pass along if found
* @type string $id <iframe> `id` attribute - Pass along if found
* @type int $frameborder <iframe> `frameborder` attribute - Filter to '0' or '1'; default to '0'
* @type bool $allowfullscreen <iframe> `allowfullscreen` attribute - Convert 'false' to empty string ''
* @type bool $allowtransparency <iframe> `allowtransparency` attribute - Convert 'false' to empty string ''
* }
* @return array Returns HTML attributes; normalizes src, dimensions, frameborder, sandox, allowtransparency and allowfullscreen
*/
private function normalize_attributes( $attributes ) {
$out = array();
foreach ( $attributes as $name => $value ) {
switch ( $name ) {
case 'src':
$out[ $name ] = $this->maybe_enforce_https_src( $value, true );
break;
case 'width':
case 'height':
$out[ $name ] = $this->sanitize_dimension( $value, $name );
break;
case 'frameborder':
if ( '0' !== $value && '1' !== $value ) {
$value = '0';
}
$out[ $name ] = $value;
break;
case 'allowfullscreen':
case 'allowtransparency':
if ( 'false' !== $value ) {
$out[ $name ] = '';
}
break;
default:
$out[ $name ] = $value;
break;
}
}
if ( ! isset( $out['sandbox'] ) ) {
$out['sandbox'] = self::SANDBOX_DEFAULTS;
}
return $out;
}
/**
* Builds a DOMElement to use as a placeholder for an <iframe>.
*
* Important: The element returned must not be block-level (e.g. div) as the PHP DOM parser
* will move it out from inside any containing paragraph. So this is why a span is used.
*
* @since 0.2
*
* @param string[] $parent_attributes {
* Attributes.
*
* @type string $placeholder AMP HTML <amp-iframe> `placeholder` attribute; default to 'amp-wp-iframe-placeholder'
* @type string $class AMP HTML <amp-iframe> `class` attribute; default to 'amp-wp-iframe-placeholder'
* }
* @return DOMElement|false
*/
private function build_placeholder( $parent_attributes ) {
$placeholder_node = AMP_DOM_Utils::create_node(
$this->dom,
'span',
array(
'placeholder' => '',
'class' => 'amp-wp-iframe-placeholder',
)
);
return $placeholder_node;
}
}

View File

@ -0,0 +1,357 @@
<?php
/**
* Class AMP_Img_Sanitizer.
*
* @package AMP
*/
/**
* Class AMP_Img_Sanitizer
*
* Converts <img> tags to <amp-img> or <amp-anim>
*/
class AMP_Img_Sanitizer extends AMP_Base_Sanitizer {
use AMP_Noscript_Fallback;
/**
* Value used for width attribute when $attributes['width'] is empty.
*
* @since 0.2
*
* @const int
*/
const FALLBACK_WIDTH = 600;
/**
* Value used for height attribute when $attributes['height'] is empty.
*
* @since 0.2
*
* @const int
*/
const FALLBACK_HEIGHT = 400;
/**
* Tag.
*
* @var string HTML <img> tag to identify and replace with AMP version.
*
* @since 0.2
*/
public static $tag = 'img';
/**
* Default args.
*
* @var array
*/
protected $DEFAULT_ARGS = array(
'add_noscript_fallback' => true,
);
/**
* Animation extension.
*
* @var string
*/
private static $anim_extension = '.gif';
/**
* Get mapping of HTML selectors to the AMP component selectors which they may be converted into.
*
* @return array Mapping.
*/
public function get_selector_conversion_mapping() {
return array(
'img' => array(
'amp-img',
'amp-anim',
),
);
}
/**
* Sanitize the <img> elements from the HTML contained in this instance's DOMDocument.
*
* @since 0.2
*/
public function sanitize() {
/**
* Node list.
*
* @var DOMNodeList $node
*/
$nodes = $this->dom->getElementsByTagName( self::$tag );
$need_dimensions = array();
$num_nodes = $nodes->length;
if ( 0 === $num_nodes ) {
return;
}
if ( $this->args['add_noscript_fallback'] ) {
$this->initialize_noscript_allowed_attributes( self::$tag );
}
for ( $i = $num_nodes - 1; $i >= 0; $i-- ) {
$node = $nodes->item( $i );
if ( ! $node instanceof DOMElement ) {
continue;
}
// Skip element if already inside of an AMP element as a noscript fallback.
if ( $this->is_inside_amp_noscript( $node ) ) {
continue;
}
if ( ! $node->hasAttribute( 'src' ) || '' === trim( $node->getAttribute( 'src' ) ) ) {
$this->remove_invalid_child( $node );
continue;
}
// Determine which images need their dimensions determined/extracted.
if ( ! is_numeric( $node->getAttribute( 'width' ) ) || ! is_numeric( $node->getAttribute( 'height' ) ) ) {
$need_dimensions[ $node->getAttribute( 'src' ) ][] = $node;
} else {
$this->adjust_and_replace_node( $node );
}
}
$this->determine_dimensions( $need_dimensions );
$this->adjust_and_replace_nodes_in_array_map( $need_dimensions );
/*
* Opt-in to amp-img-auto-sizes experiment.
* This is needed because the sizes attribute is removed from all img elements converted to amp-img
* in order to prevent the undesirable setting of the width. This $meta tag can be removed once the
* experiment ends (and the feature has been fully launched).
* See <https://github.com/ampproject/amphtml/issues/21371> and <https://github.com/ampproject/amp-wp/pull/2036>.
*/
$head = $this->dom->getElementsByTagName( 'head' )->item( 0 );
if ( $head ) {
$meta = $this->dom->createElement( 'meta' );
$meta->setAttribute( 'name', 'amp-experiments-opt-in' );
$meta->setAttribute( 'content', 'amp-img-auto-sizes' );
$head->insertBefore( $meta, $head->firstChild );
}
}
/**
* "Filter" HTML attributes for <amp-anim> elements.
*
* @since 0.2
*
* @param string[] $attributes {
* Attributes.
*
* @type string $src Image URL - Pass along if found
* @type string $alt <img> `alt` attribute - Pass along if found
* @type string $class <img> `class` attribute - Pass along if found
* @type string $srcset <img> `srcset` attribute - Pass along if found
* @type string $sizes <img> `sizes` attribute - Pass along if found
* @type string $on <img> `on` attribute - Pass along if found
* @type string $attribution <img> `attribution` attribute - Pass along if found
* @type int $width <img> width attribute - Set to numeric value if px or %
* @type int $height <img> width attribute - Set to numeric value if px or %
* }
* @return array Returns HTML attributes; removes any not specifically declared above from input.
*/
private function filter_attributes( $attributes ) {
$out = array();
foreach ( $attributes as $name => $value ) {
switch ( $name ) {
case 'width':
case 'height':
$out[ $name ] = $this->sanitize_dimension( $value, $name );
break;
case 'data-amp-layout':
$out['layout'] = $value;
break;
case 'data-amp-noloading':
$out['noloading'] = $value;
break;
default:
$out[ $name ] = $value;
break;
}
}
return $out;
}
/**
* Determine width and height attribute values for images without them.
*
* Attempt to determine actual dimensions, otherwise set reasonable defaults.
*
* @param DOMElement[][] $need_dimensions Map <img> @src URLs to node for images with missing dimensions.
*/
private function determine_dimensions( $need_dimensions ) {
$dimensions_by_url = AMP_Image_Dimension_Extractor::extract( array_keys( $need_dimensions ) );
foreach ( $dimensions_by_url as $url => $dimensions ) {
foreach ( $need_dimensions[ $url ] as $node ) {
if ( ! $node instanceof DOMElement ) {
continue;
}
$class = $node->getAttribute( 'class' );
if ( ! $class ) {
$class = '';
}
if ( ! $dimensions ) {
$class .= ' amp-wp-unknown-size';
}
$width = isset( $this->args['content_max_width'] ) ? $this->args['content_max_width'] : self::FALLBACK_WIDTH;
$height = self::FALLBACK_HEIGHT;
if ( isset( $dimensions['width'] ) ) {
$width = $dimensions['width'];
}
if ( isset( $dimensions['height'] ) ) {
$height = $dimensions['height'];
}
if ( ! is_numeric( $node->getAttribute( 'width' ) ) ) {
// Let width have the right aspect ratio based on the height attribute.
if ( is_numeric( $node->getAttribute( 'height' ) ) && isset( $dimensions['height'] ) && isset( $dimensions['width'] ) ) {
$width = ( floatval( $node->getAttribute( 'height' ) ) * $dimensions['width'] ) / $dimensions['height'];
}
$node->setAttribute( 'width', $width );
if ( ! isset( $dimensions['width'] ) ) {
$class .= ' amp-wp-unknown-width';
}
}
if ( ! is_numeric( $node->getAttribute( 'height' ) ) ) {
// Let height have the right aspect ratio based on the width attribute.
if ( is_numeric( $node->getAttribute( 'width' ) ) && isset( $dimensions['width'] ) && isset( $dimensions['height'] ) ) {
$height = ( floatval( $node->getAttribute( 'width' ) ) * $dimensions['height'] ) / $dimensions['width'];
}
$node->setAttribute( 'height', $height );
if ( ! isset( $dimensions['height'] ) ) {
$class .= ' amp-wp-unknown-height';
}
}
$node->setAttribute( 'class', trim( $class ) );
}
}
}
/**
* Now that all images have width and height attributes, make final tweaks and replace original image nodes
*
* @param DOMNodeList[] $node_lists Img DOM nodes (now with width and height attributes).
*/
private function adjust_and_replace_nodes_in_array_map( $node_lists ) {
foreach ( $node_lists as $node_list ) {
foreach ( $node_list as $node ) {
$this->adjust_and_replace_node( $node );
}
}
}
/**
* Make final modifications to DOMNode
*
* @param DOMElement $node The img element to adjust and replace.
*/
private function adjust_and_replace_node( $node ) {
$amp_data = $this->get_data_amp_attributes( $node );
$old_attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node );
$old_attributes = $this->filter_data_amp_attributes( $old_attributes, $amp_data );
$old_attributes = $this->maybe_add_lightbox_attributes( $old_attributes, $node );
$new_attributes = $this->filter_attributes( $old_attributes );
$layout = isset( $amp_data['layout'] ) ? $amp_data['layout'] : false;
$new_attributes = $this->filter_attachment_layout_attributes( $node, $new_attributes, $layout );
$this->add_or_append_attribute( $new_attributes, 'class', 'amp-wp-enforced-sizes' );
if ( empty( $new_attributes['layout'] ) && ! empty( $new_attributes['height'] ) && ! empty( $new_attributes['width'] ) ) {
// Use responsive images when a theme supports wide and full-bleed images.
if ( ! empty( $this->args['align_wide_support'] ) && $node->parentNode && 'figure' === $node->parentNode->nodeName && preg_match( '/(^|\s)(alignwide|alignfull)(\s|$)/', $node->parentNode->getAttribute( 'class' ) ) ) {
$new_attributes['layout'] = 'responsive';
} else {
$new_attributes['layout'] = 'intrinsic';
}
}
// Remove sizes attribute since it causes headaches in AMP and because AMP will generate it for us. See <https://github.com/ampproject/amphtml/issues/21371>.
unset( $new_attributes['sizes'] );
if ( $this->is_gif_url( $new_attributes['src'] ) ) {
$this->did_convert_elements = true;
$new_tag = 'amp-anim';
} else {
$new_tag = 'amp-img';
}
$img_node = AMP_DOM_Utils::create_node( $this->dom, $new_tag, $new_attributes );
$node->parentNode->replaceChild( $img_node, $node );
$can_include_noscript = (
$this->args['add_noscript_fallback']
&&
( $node->hasAttribute( 'src' ) && ! preg_match( '/^http:/', $node->getAttribute( 'src' ) ) )
&&
( ! $node->hasAttribute( 'srcset' ) || ! preg_match( '/http:/', $node->getAttribute( 'srcset' ) ) )
);
if ( $can_include_noscript ) {
// Preserve original node in noscript for no-JS environments.
$this->append_old_node_noscript( $img_node, $node, $this->dom );
}
}
/**
* Set lightbox attributes.
*
* @param array $attributes Array of attributes.
* @param DomNode $node Array of AMP attributes.
* @return array Updated attributes.
*/
private function maybe_add_lightbox_attributes( $attributes, $node ) {
$parent_node = $node->parentNode;
if ( ! ( $parent_node instanceof DOMElement ) || 'figure' !== $parent_node->tagName ) {
return $attributes;
}
$parent_attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $parent_node );
if ( isset( $parent_attributes['data-amp-lightbox'] ) && true === filter_var( $parent_attributes['data-amp-lightbox'], FILTER_VALIDATE_BOOLEAN ) ) {
$attributes['data-amp-lightbox'] = '';
$attributes['on'] = 'tap:' . self::AMP_IMAGE_LIGHTBOX_ID;
$attributes['role'] = 'button';
$attributes['tabindex'] = 0;
$this->maybe_add_amp_image_lightbox_node();
}
return $attributes;
}
/**
* Determines is a URL is considered a GIF URL
*
* @since 0.2
*
* @param string $url URL to inspect for GIF vs. JPEG or PNG.
*
* @return bool Returns true if $url ends in `.gif`
*/
private function is_gif_url( $url ) {
$ext = self::$anim_extension;
$path = wp_parse_url( $url, PHP_URL_PATH );
return substr( $path, -strlen( $ext ) ) === $ext;
}
}

View File

@ -0,0 +1,164 @@
<?php
/**
* Class AMP_Nav_Menu_Dropdown_Sanitizer
*
* @package AMP
*/
/**
* Class AMP_Nav_Menu_Dropdown_Sanitizer
*
* Handles state for navigation menu dropdown toggles, based on theme support.
*
* @since 1.1.0
*/
class AMP_Nav_Menu_Dropdown_Sanitizer extends AMP_Base_Sanitizer {
/**
* Default args.
*
* @since 1.1.0
* @var array
*/
protected $DEFAULT_ARGS = array(
'sub_menu_button_class' => '',
'sub_menu_button_toggle_class' => '',
'expand_text' => '',
'collapse_text' => '',
'icon' => null, // Optional.
'sub_menu_item_state_id' => 'navMenuItemExpanded',
);
/**
* AMP_Nav_Menu_Dropdown_Sanitizer constructor.
*
* @since 1.1.0
*
* @param DOMDocument $dom DOM.
* @param array $args Args.
*/
public function __construct( $dom, $args = array() ) {
parent::__construct( $dom, $args );
$this->args = self::ensure_defaults( $this->args );
}
/**
* Add filter to manipulate output during output buffering to add AMP-compatible dropdown toggles.
*
* @since 1.0
*
* @param array $args Args.
*/
public static function add_buffering_hooks( $args = array() ) {
if ( empty( $args['sub_menu_button_class'] ) || empty( $args['sub_menu_button_toggle_class'] ) ) {
return;
}
$args = self::ensure_defaults( $args );
/**
* Filter the HTML output of a nav menu item to add the AMP dropdown button to reveal the sub-menu.
*
* @param string $item_output Nav menu item HTML.
* @param object $item Nav menu item.
* @return string Modified nav menu item HTML.
*/
add_filter(
'walker_nav_menu_start_el',
function( $item_output, $item, $depth, $nav_menu_args ) use ( $args ) {
unset( $depth );
// Skip adding buttons to nav menu widgets for now.
if ( empty( $nav_menu_args->theme_location ) ) {
return $item_output;
}
if ( ! in_array( 'menu-item-has-children', $item->classes, true ) ) {
return $item_output;
}
static $nav_menu_item_number = 0;
$nav_menu_item_number++;
$expanded = in_array( 'current-menu-ancestor', $item->classes, true );
$expanded_state_id = $args['nav_menu_item_state_id'] . $nav_menu_item_number;
// Create new state for managing storing the whether the sub-menu is expanded.
$item_output .= sprintf(
'<amp-state id="%s"><script type="application/json">%s</script></amp-state>',
esc_attr( $expanded_state_id ),
wp_json_encode( $expanded )
);
$dropdown_button = '<button';
$dropdown_button .= sprintf(
' class="%s" [class]="%s"',
esc_attr( $args['sub_menu_button_class'] . ( $expanded ? ' ' . $args['sub_menu_button_toggle_class'] : '' ) ),
esc_attr( sprintf( "%s + ( $expanded_state_id ? %s : '' )", wp_json_encode( $args['sub_menu_button_class'] ), wp_json_encode( ' ' . $args['sub_menu_button_toggle_class'] ) ) )
);
$dropdown_button .= sprintf(
' aria-expanded="%s" [aria-expanded]="%s"',
esc_attr( wp_json_encode( $expanded ) ),
esc_attr( "$expanded_state_id ? 'true' : 'false'" )
);
$dropdown_button .= sprintf(
' on="%s"',
esc_attr( "tap:AMP.setState( { $expanded_state_id: ! $expanded_state_id } )" )
);
$dropdown_button .= '>';
if ( isset( $args['icon'] ) ) {
$dropdown_button .= $args['icon'];
}
if ( isset( $args['expand_text'] ) && isset( $args['collapse_text'] ) ) {
$dropdown_button .= sprintf(
'<span class="screen-reader-text" [text]="%s">%s</span>',
esc_attr( sprintf( "$expanded_state_id ? %s : %s", wp_json_encode( $args['collapse_text'] ), wp_json_encode( $args['expand_text'] ) ) ),
esc_html( $expanded ? $args['collapse_text'] : $args['expand_text'] )
);
}
$dropdown_button .= '</button>';
$item_output .= $dropdown_button;
return $item_output;
},
10,
4
);
}
/**
* Method needs to be stubbed to fulfill base class requirements.
*
* @since 1.1.0
*/
public function sanitize() {
// Empty method body.
}
/**
* Ensure that some defaults are always set as fallback.
*
* @param array $args Arguments to set the defaults in as necessary.
* @return array Arguments with defaults filled.
*/
protected static function ensure_defaults( $args ) {
// Ensure accessibility labels are always set.
if ( empty( $args['expand_text'] ) ) {
$args['expand_text'] = __( 'expand child menu', 'amp' );
}
if ( empty( $args['collapse_text'] ) ) {
$args['collapse_text'] = __( 'collapse child menu', 'amp' );
}
// Ensure the state ID is always set.
if ( empty( $args['nav_menu_item_state_id'] ) ) {
$args['nav_menu_item_state_id'] = 'navMenuItemExpanded';
}
return $args;
}
}

View File

@ -0,0 +1,152 @@
<?php
/**
* Class AMP_Nav_Menu_Toggle_Sanitizer
*
* @package AMP
*/
/**
* Class AMP_Nav_Menu_Toggle_Sanitizer
*
* Handles state for navigation menu toggles, based on theme support.
*
* @since 1.1.0
*/
class AMP_Nav_Menu_Toggle_Sanitizer extends AMP_Base_Sanitizer {
/**
* Default args.
*
* @since 1.1.0
* @var array
*/
protected $DEFAULT_ARGS = array(
'nav_container_id' => '',
'nav_container_xpath' => '', // Alternative for 'nav_container_id', if no ID available.
'menu_button_id' => '',
'menu_button_xpath' => '', // Alternative for 'menu_button_id', if no ID available.
'nav_container_toggle_class' => '',
'menu_button_toggle_class' => '', // Optional.
'nav_menu_toggle_state_id' => 'navMenuToggledOn',
);
/**
* XPath.
*
* @since 1.1.0
* @var DOMXPath
*/
protected $xpath;
/**
* AMP_Nav_Menu_Toggle_Sanitizer constructor.
*
* @since 1.1.0
*
* @param DOMDocument $dom DOM.
* @param array $args Args.
*/
public function __construct( $dom, $args = array() ) {
parent::__construct( $dom, $args );
// Ensure the state ID is always set.
if ( empty( $this->args['nav_menu_toggle_state_id'] ) ) {
$this->args['nav_menu_toggle_state_id'] = $this->DEFAULT_ARGS['nav_menu_toggle_state_id'];
}
}
/**
* If supported per the constructor arguments, inject `amp-state` and bind dynamic classes accordingly.
*
* @since 1.1.0
*/
public function sanitize() {
$this->xpath = new DOMXPath( $this->dom );
$nav_el = $this->get_nav_container();
$button_el = $this->get_menu_button();
// If no navigation element or no toggle class provided, bail.
if ( ! $nav_el || empty( $this->args['nav_container_toggle_class'] ) ) {
if ( $button_el ) {
// Remove the button since it won't be used.
$button_el->parentNode->removeChild( $button_el );
}
return;
}
if ( ! $button_el ) {
return;
}
$state_id = 'navMenuToggledOn';
$expanded = false;
$nav_el->setAttribute(
AMP_DOM_Utils::get_amp_bind_placeholder_prefix() . 'class',
sprintf(
"%s + ( $state_id ? %s : '' )",
wp_json_encode( $nav_el->getAttribute( 'class' ) ),
wp_json_encode( ' ' . $this->args['nav_container_toggle_class'] )
)
);
$state_el = $this->dom->createElement( 'amp-state' );
$state_el->setAttribute( 'id', $state_id );
$script_el = $this->dom->createElement( 'script' );
$script_el->setAttribute( 'type', 'application/json' );
$script_el->appendChild( $this->dom->createTextNode( wp_json_encode( $expanded ) ) );
$state_el->appendChild( $script_el );
$nav_el->parentNode->insertBefore( $state_el, $nav_el );
$button_on = sprintf( "tap:AMP.setState({ $state_id: ! $state_id })" );
$button_el->setAttribute( 'on', $button_on );
$button_el->setAttribute( 'aria-expanded', 'false' );
$button_el->setAttribute( AMP_DOM_Utils::get_amp_bind_placeholder_prefix() . 'aria-expanded', "$state_id ? 'true' : 'false'" );
if ( ! empty( $this->args['menu_button_toggle_class'] ) ) {
$button_el->setAttribute(
AMP_DOM_Utils::get_amp_bind_placeholder_prefix() . 'class',
sprintf( "%s + ( $state_id ? %s : '' )", wp_json_encode( $button_el->getAttribute( 'class' ) ), wp_json_encode( ' ' . $this->args['menu_button_toggle_class'] ) )
);
}
}
/**
* Retrieves the navigation container element.
*
* @since 1.1.0
*
* @return DOMElement|null Navigation container element, or null if not provided or found.
*/
protected function get_nav_container() {
if ( ! empty( $this->args['nav_container_id'] ) ) {
return $this->dom->getElementById( $this->args['nav_container_id'] );
}
if ( ! empty( $this->args['nav_container_xpath'] ) ) {
return $this->xpath->query( $this->args['nav_container_xpath'] )->item( 0 );
}
return null;
}
/**
* Retrieves the navigation menu button element.
*
* @since 1.1.0
*
* @return DOMElement|null Navigation menu button element, or null if not provided or found.
*/
protected function get_menu_button() {
if ( ! empty( $this->args['menu_button_id'] ) ) {
return $this->dom->getElementById( $this->args['menu_button_id'] );
}
if ( ! empty( $this->args['menu_button_xpath'] ) ) {
return $this->xpath->query( $this->args['menu_button_xpath'] )->item( 0 );
}
return null;
}
}

View File

@ -0,0 +1,141 @@
<?php
/**
* Class AMP_O2_Player_Sanitizer
*
* @package AMP
*/
/**
* Class AMP_O2_Player_Sanitizer
*
* Converts <div class="vdb_player><script></script></div> embed to <amp-o2-player>
*
* @since 1.0
* @see https://www.ampproject.org/docs/reference/components/amp-o2-player
*/
class AMP_O2_Player_Sanitizer extends AMP_Base_Sanitizer {
/**
* Pattern to extract the information required for amp-o2-player element: data-pid, data-vid, data-bcid.
*
* @since 1.0
*/
const URL_PATTERN = '#.*delivery.vidible.tv\/jsonp\/pid=(?<data_pid>.*)\/vid=(?<data_vid>.*)\/(?<data_bcid>.*).js.*#i';
/**
* AMP Tag.
*
* @since 1.0
* @var string AMP Tag.
*/
private static $amp_tag = 'amp-o2-player';
/**
* Amp O2 Player class.
*
* @since 1.0
* @var string CSS class to identify O2 Player <div> to replace with AMP version.
*/
private static $xpath_selector = '//div[ contains( @class, \'vdb_player\' ) ]/script';
/**
* Height to set for O2 Player elements.
*
* @since 1.0
* @var string
*/
private static $height = '270';
/**
* Width to set for O2 Player elements.
*
* @since 1.0
* @var string
*/
private static $width = '480';
/**
* Sanitize the O2 Player elements from the HTML contained in this instance's DOMDocument.
*
* @since 1.0
*/
public function sanitize() {
/**
* XPath.
*
* @var DOMXPath $xpath
*/
$xpath = new DOMXPath( $this->dom );
/**
* Node list.
*
* @var DOMNodeList $nodes
*/
$nodes = $xpath->query( self::$xpath_selector );
$num_nodes = $nodes->length;
if ( 0 === $num_nodes ) {
return;
}
for ( $i = $num_nodes - 1; $i >= 0; $i-- ) {
$node = $nodes->item( $i );
$this->create_amp_o2_player( $this->dom, $node );
}
}
/**
* Replaces node with amp-o2-player
*
* @since 1.0
* @param DOMDocument $dom The HTML Document.
* @param DOMElement $node The DOMNode to adjust and replace.
*/
private function create_amp_o2_player( $dom, $node ) {
$o2_attributes = $this->get_o2_player_attributes( $node->getAttribute( 'src' ) );
if ( ! empty( $o2_attributes ) ) {
$component_attributes = array_merge(
$o2_attributes,
array(
'data-macros' => 'm.playback=click',
'layout' => 'responsive',
'width' => self::$width,
'height' => self::$height,
)
);
$amp_o2_player = AMP_DOM_Utils::create_node( $dom, self::$amp_tag, $component_attributes );
$parent_node = $node->parentNode;
// replaces the wrapper that contains the script with amp-o2-player element.
$parent_node->parentNode->replaceChild( $amp_o2_player, $parent_node );
$this->did_convert_elements = true;
}
}
/**
* Gets O2 Player's required attributes from script src
*
* @since 1.0
* @param string $src Script src.
*
* @return array The data-* attributes for o2 player.
*/
private function get_o2_player_attributes( $src ) {
$found = preg_match( self::URL_PATTERN, $src, $matches );
if ( $found ) {
return array(
'data-pid' => $matches['data_pid'],
'data-vid' => $matches['data_vid'],
'data-bcid' => $matches['data_bcid'],
);
}
return array();
}
}

View File

@ -0,0 +1,146 @@
<?php
/**
* Class AMP_Playbuzz_Sanitizer
*
* @package AMP
*/
/**
* Class AMP_Playbuzz_Sanitizer
*
* Converts Playbuzz embed to <amp-playbuzz>
*
* @see https://www.playbuzz.com/
*/
class AMP_Playbuzz_Sanitizer extends AMP_Base_Sanitizer {
/**
* Tag.
*
* @var string HTML tag to identify and replace with AMP version.
* @since 0.2
*/
public static $tag = 'div';
/**
* PlayBuzz class.
*
* @var string CSS class to identify Playbuzz <div> to replace with AMP version.
*
* @since 0.2
*/
public static $pb_class = 'pb_feed';
/**
* Hardcoded height to set for Playbuzz elements.
*
* @var string
*
* @since 0.2
*/
private static $height = '500';
/**
* Get mapping of HTML selectors to the AMP component selectors which they may be converted into.
*
* @return array Mapping.
*/
public function get_selector_conversion_mapping() {
return array(
'div.pb_feed' => array( 'amp-playbuzz.pb_feed' ),
);
}
/**
* Sanitize the Playbuzz elements from the HTML contained in this instance's DOMDocument.
*
* @since 0.2
*/
public function sanitize() {
$nodes = $this->dom->getElementsByTagName( self::$tag );
$num_nodes = $nodes->length;
if ( 0 === $num_nodes ) {
return;
}
for ( $i = $num_nodes - 1; $i >= 0; $i-- ) {
$node = $nodes->item( $i );
if ( self::$pb_class !== $node->getAttribute( 'class' ) ) {
continue;
}
$old_attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node );
$new_attributes = $this->filter_attributes( $old_attributes );
if ( ! isset( $new_attributes['data-item'] ) && ! isset( $new_attributes['src'] ) ) {
continue;
}
$new_node = AMP_DOM_Utils::create_node( $this->dom, 'amp-playbuzz', $new_attributes );
$node->parentNode->replaceChild( $new_node, $node );
$this->did_convert_elements = true;
}
}
/**
* "Filter" HTML attributes for <amp-audio> elements.
*
* @since 0.2
*
* @param string[] $attributes {
* Attributes.
*
* @type string $data-item Playbuzz <div> attribute - Pass along if found and not empty.
* @type string $data-game Playbuzz <div> attribute - Assign to its value to $attributes['src'] if found and not empty.
* @type string $data-game-info Playbuzz <div> attribute - Assign to its value to $attributes['data-item-info'] if found.
* @type string $data-shares Playbuzz <div> attribute - Assign to its value to $attributes['data-share-buttons'] if found.
* @type string $data-comments Playbuzz <div> attribute - Pass along if found.
* @type int $height Playbuzz <div> attribute - Set to hardcoded value of 500.
* }
* @return array Returns HTML attributes; removes any not specifically declared above from input.
*/
private function filter_attributes( $attributes ) {
$out = array();
foreach ( $attributes as $name => $value ) {
switch ( $name ) {
case 'data-item':
if ( ! empty( $value ) ) {
$out['data-item'] = $value;
}
break;
case 'data-game':
if ( ! empty( $value ) ) {
$out['src'] = $value;
}
break;
case 'data-shares':
$out['data-share-buttons'] = $value;
break;
case 'data-game-info':
case 'data-comments':
case 'class':
$out[ $name ] = $value;
break;
default:
break;
}
}
$out['height'] = self::$height;
return $out;
}
}

View File

@ -0,0 +1,141 @@
<?php
/**
* Class AMP_Rule_Spec
*
* @package AMP
*/
/**
* Class AMP_Rule_Spec
*
* Set of constants used throughout the sanitizer.
*/
abstract class AMP_Rule_Spec {
/**
* AMP rule_spec types
*/
const ATTR_SPEC_LIST = 'attr_spec_list';
const TAG_SPEC = 'tag_spec';
const CDATA = 'cdata';
/**
* AMP attr_spec value check results.
*
* In 0.7 these changed from strings to integers to speed up comparisons.
*/
const PASS = 1;
const FAIL = 0;
const NOT_APPLICABLE = -1;
/**
* HTML Element Tag rule names
*/
const DISALLOWED_ANCESTOR = 'disallowed_ancestor';
const MANDATORY_ANCESTOR = 'mandatory_ancestor';
const MANDATORY_PARENT = 'mandatory_parent';
const DESCENDANT_TAG_LIST = 'descendant_tag_list';
const CHILD_TAGS = 'child_tags';
/**
* HTML Element Attribute rule names
*/
const ALLOW_EMPTY = 'allow_empty';
const ALLOW_RELATIVE = 'allow_relative';
const ALLOWED_PROTOCOL = 'protocol';
const ALTERNATIVE_NAMES = 'alternative_names';
const BLACKLISTED_VALUE_REGEX = 'blacklisted_value_regex';
const DISALLOWED_DOMAIN = 'disallowed_domain';
const MANDATORY = 'mandatory';
const VALUE = 'value';
const VALUE_CASEI = 'value_casei';
const VALUE_REGEX = 'value_regex';
const VALUE_REGEX_CASEI = 'value_regex_casei';
const VALUE_PROPERTIES = 'value_properties';
const VALUE_URL = 'value_url';
/**
* Supported layout values.
*
* @since 1.0
* @var array
*/
public static $layout_enum = array(
1 => 'nodisplay',
2 => 'fixed',
3 => 'fixed-height',
4 => 'responsive',
5 => 'container',
6 => 'fill',
7 => 'flex-item',
8 => 'fluid',
9 => 'intrinsic',
);
/**
* List of boolean attributes.
*
* @since 0.7
* @var array
*/
public static $boolean_attributes = array(
'allowfullscreen',
'async',
'autofocus',
'autoplay',
'checked',
'compact',
'controls',
'declare',
'default',
'defaultchecked',
'defaultmuted',
'defaultselected',
'defer',
'disabled',
'draggable',
'enabled',
'formnovalidate',
'hidden',
'indeterminate',
'inert',
'ismap',
'itemscope',
'loop',
'multiple',
'muted',
'nohref',
'noresize',
'noshade',
'novalidate',
'nowrap',
'open',
'pauseonexit',
'readonly',
'required',
'reversed',
'scoped',
'seamless',
'selected',
'sortable',
'spellcheck',
'translate',
'truespeed',
'typemustmatch',
'visible',
);
/**
* Additional allowed tags.
*
* @var array
*/
public static $additional_allowed_tags = array(
// An experimental tag with no protoascii.
'amp-share-tracking' => array(
'attr_spec_list' => array(),
'tag_spec' => array(),
),
);
}

View File

@ -0,0 +1,54 @@
<?php
/**
* Class AMP_Script_Sanitizer
*
* @since 1.0
* @package AMP
*/
/**
* Class AMP_Script_Sanitizer
*
* @since 1.0
*/
class AMP_Script_Sanitizer extends AMP_Base_Sanitizer {
/**
* Sanitize noscript elements.
*
* Eventually this should also handle script elements, if there is a known AMP equivalent.
* If nothing is done with script elements, the whitelist sanitizer will deal with them ultimately.
*
* @todo Eventually this try to automatically convert script tags to AMP when they are recognized. See <https://github.com/ampproject/amp-wp/issues/1032>.
* @todo When a script has an adjacent noscript, consider removing the script here to prevent validation error later. See <https://github.com/ampproject/amp-wp/issues/1213>.
*
* @since 1.0
*/
public function sanitize() {
$noscripts = $this->dom->getElementsByTagName( 'noscript' );
for ( $i = $noscripts->length - 1; $i >= 0; $i-- ) {
$noscript = $noscripts->item( $i );
// Skip AMP boilerplate.
if ( $noscript->firstChild instanceof DOMElement && $noscript->firstChild->hasAttribute( 'amp-boilerplate' ) ) {
continue;
}
// Skip noscript elements inside of amp-img or other AMP components for fallbacks. See \AMP_Img_Sanitizer::adjust_and_replace_node().
if ( 'amp-' === substr( $noscript->parentNode->nodeName, 0, 4 ) ) {
continue;
}
$fragment = $this->dom->createDocumentFragment();
$fragment->appendChild( $this->dom->createComment( 'noscript' ) );
while ( $noscript->firstChild ) {
$fragment->appendChild( $noscript->firstChild );
}
$fragment->appendChild( $this->dom->createComment( '/noscript' ) );
$noscript->parentNode->replaceChild( $fragment, $noscript );
$this->did_convert_elements = true;
}
}
}

View File

@ -0,0 +1,290 @@
<?php
/**
* Class AMP_Video_Sanitizer.
*
* @package AMP
*/
/**
* Class AMP_Video_Sanitizer
*
* @since 0.2
*
* Converts <video> tags to <amp-video>
*/
class AMP_Video_Sanitizer extends AMP_Base_Sanitizer {
use AMP_Noscript_Fallback;
/**
* Value used for height attribute when $attributes['height'] is empty.
*
* @since 0.2
*
* @const int
*/
const FALLBACK_HEIGHT = 400;
/**
* Tag.
*
* @var string HTML <video> tag to identify and replace with AMP version.
*
* @since 0.2
*/
public static $tag = 'video';
/**
* Default args.
*
* @var array
*/
protected $DEFAULT_ARGS = array(
'add_noscript_fallback' => true,
);
/**
* Get mapping of HTML selectors to the AMP component selectors which they may be converted into.
*
* @return array Mapping.
*/
public function get_selector_conversion_mapping() {
return array(
'video' => array( 'amp-video', 'amp-youtube' ),
);
}
/**
* Sanitize the <video> elements from the HTML contained in this instance's DOMDocument.
*
* @since 0.2
* @since 1.0 Set the filtered child node's src attribute.
*/
public function sanitize() {
$nodes = $this->dom->getElementsByTagName( self::$tag );
$num_nodes = $nodes->length;
if ( 0 === $num_nodes ) {
return;
}
if ( $this->args['add_noscript_fallback'] ) {
$this->initialize_noscript_allowed_attributes( self::$tag );
}
for ( $i = $num_nodes - 1; $i >= 0; $i-- ) {
/**
* Node.
*
* @var DOMElement $node
*/
$node = $nodes->item( $i );
// Skip element if already inside of an AMP element as a noscript fallback.
if ( $this->is_inside_amp_noscript( $node ) ) {
continue;
}
$amp_data = $this->get_data_amp_attributes( $node );
$old_attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node );
$old_attributes = $this->filter_data_amp_attributes( $old_attributes, $amp_data );
$sources = array();
$new_attributes = $this->filter_attributes( $old_attributes );
$layout = isset( $amp_data['layout'] ) ? $amp_data['layout'] : false;
if ( isset( $new_attributes['src'] ) ) {
$new_attributes = $this->filter_video_dimensions( $new_attributes, $new_attributes['src'] );
if ( $new_attributes['src'] ) {
$sources[] = $new_attributes['src'];
}
}
/**
* Original node.
*
* @var DOMElement $old_node
*/
$old_node = $node->cloneNode( false );
// Gather all child nodes and supply empty video dimensions from sources.
$fallback = null;
$child_nodes = array();
while ( $node->firstChild ) {
$child_node = $node->removeChild( $node->firstChild );
if ( $child_node instanceof DOMElement && 'source' === $child_node->nodeName && $child_node->hasAttribute( 'src' ) ) {
$src = $this->maybe_enforce_https_src( $child_node->getAttribute( 'src' ), true );
if ( ! $src ) {
// @todo $this->remove_invalid_child( $child_node ), but this will require refactoring the while loop since it uses firstChild.
continue; // Skip adding source.
}
$sources[] = $src;
$child_node->setAttribute( 'src', $src );
$new_attributes = $this->filter_video_dimensions( $new_attributes, $src );
}
if ( ! $fallback && $child_node instanceof DOMElement && ! ( 'source' === $child_node->nodeName || 'track' === $child_node->nodeName ) ) {
$fallback = $child_node;
$fallback->setAttribute( 'fallback', '' );
}
$child_nodes[] = $child_node;
}
/*
* Add fallback for audio shortcode which is not present by default since wp_mediaelement_fallback()
* is not called when wp_audio_shortcode_library is filtered from mediaelement to amp.
*/
if ( ! $fallback && ! empty( $sources ) ) {
$fallback = $this->dom->createElement( 'a' );
$fallback->setAttribute( 'href', $sources[0] );
$fallback->setAttribute( 'fallback', '' );
$fallback->appendChild( $this->dom->createTextNode( $sources[0] ) );
$child_nodes[] = $fallback;
}
$new_attributes = $this->filter_attachment_layout_attributes( $node, $new_attributes, $layout );
if ( empty( $new_attributes['layout'] ) && ! empty( $new_attributes['width'] ) && ! empty( $new_attributes['height'] ) ) {
$new_attributes['layout'] = 'responsive';
}
$new_attributes = $this->set_layout( $new_attributes );
// @todo Make sure poster and artwork attributes are HTTPS.
$new_node = AMP_DOM_Utils::create_node( $this->dom, 'amp-video', $new_attributes );
foreach ( $child_nodes as $child_node ) {
$new_node->appendChild( $child_node );
if ( ! ( $child_node instanceof DOMElement ) || ! $child_node->hasAttribute( 'fallback' ) ) {
$old_node->appendChild( $child_node->cloneNode( true ) );
}
}
// Make sure the updated src and poster are applied to the original.
foreach ( array( 'src', 'poster', 'artwork' ) as $attr_name ) {
if ( $new_node->hasAttribute( $attr_name ) ) {
$old_node->setAttribute( $attr_name, $new_node->getAttribute( $attr_name ) );
}
}
/*
* If the node has at least one valid source, replace the old node with it.
* Otherwise, just remove the node.
*
* @todo Add a fallback handler.
* See: https://github.com/ampproject/amphtml/issues/2261
*/
if ( empty( $sources ) ) {
$this->remove_invalid_child( $node );
} else {
$node->parentNode->replaceChild( $new_node, $node );
if ( $this->args['add_noscript_fallback'] ) {
// Preserve original node in noscript for no-JS environments.
$this->append_old_node_noscript( $new_node, $old_node, $this->dom );
}
}
$this->did_convert_elements = true;
}
}
/**
* Filter video dimensions, try to get width and height from original file if missing.
*
* @param array $new_attributes Attributes.
* @param string $src Video URL.
* @return array Modified attributes.
*/
protected function filter_video_dimensions( $new_attributes, $src ) {
if ( empty( $new_attributes['width'] ) || empty( $new_attributes['height'] ) ) {
// Get the width and height from the file.
$path = wp_parse_url( $src, PHP_URL_PATH );
$ext = pathinfo( $path, PATHINFO_EXTENSION );
$name = sanitize_title( wp_basename( $path, ".$ext" ) );
$args = array(
'name' => $name,
'post_type' => 'attachment',
'post_status' => 'inherit',
'numberposts' => 1,
);
$attachment = get_posts( $args );
if ( ! empty( $attachment ) ) {
$meta_data = wp_get_attachment_metadata( $attachment[0]->ID );
if ( empty( $new_attributes['width'] ) && ! empty( $meta_data['width'] ) ) {
$new_attributes['width'] = $meta_data['width'];
}
if ( empty( $new_attributes['height'] ) && ! empty( $meta_data['height'] ) ) {
$new_attributes['height'] = $meta_data['height'];
}
}
}
return $new_attributes;
}
/**
* "Filter" HTML attributes for <amp-audio> elements.
*
* @since 0.2
* @since 1.0 Force HTTPS for the src attribute.
*
* @param string[] $attributes {
* Attributes.
*
* @type string $src Video URL - Empty if HTTPS required per $this->args['require_https_src']
* @type int $width <video> attribute - Set to numeric value if px or %
* @type int $height <video> attribute - Set to numeric value if px or %
* @type string $poster <video> attribute - Pass along if found
* @type string $class <video> attribute - Pass along if found
* @type bool $controls <video> attribute - Convert 'false' to empty string ''
* @type bool $loop <video> attribute - Convert 'false' to empty string ''
* @type bool $muted <video> attribute - Convert 'false' to empty string ''
* @type bool $autoplay <video> attribute - Convert 'false' to empty string ''
* }
* @return array Returns HTML attributes; removes any not specifically declared above from input.
*/
private function filter_attributes( $attributes ) {
$out = array();
foreach ( $attributes as $name => $value ) {
switch ( $name ) {
case 'src':
$out[ $name ] = $this->maybe_enforce_https_src( $value, true );
break;
case 'width':
case 'height':
$out[ $name ] = $this->sanitize_dimension( $value, $name );
break;
// @todo Convert to HTTPS when is_ssl().
case 'poster':
case 'artwork':
$out[ $name ] = $value;
break;
case 'controls':
case 'loop':
case 'muted':
case 'autoplay':
if ( 'false' !== $value ) {
$out[ $name ] = '';
}
break;
case 'data-amp-layout':
$out['layout'] = $value;
break;
case 'data-amp-noloading':
$out['noloading'] = $value;
break;
default:
$out[ $name ] = $value;
}
}
return $out;
}
}

View File

@ -0,0 +1,79 @@
<?php
/**
* Trait AMP_Noscript_Fallback.
*
* @package AMP
*/
/**
* Trait AMP_Noscript_Fallback
*
* @since 1.1
*
* Used for sanitizers that place <noscript> tags with the original nodes on error.
*/
trait AMP_Noscript_Fallback {
/**
* Attributes allowed on noscript fallback elements.
*
* This is used to prevent duplicated validation errors.
*
* @since 1.1
* @var array
*/
private $noscript_fallback_allowed_attributes = array();
/**
* Initializes the internal allowed attributes array.
*
* @since 1.1
*
* @param string $tag Tag name to get allowed attributes for.
*/
protected function initialize_noscript_allowed_attributes( $tag ) {
$this->noscript_fallback_allowed_attributes = array_fill_keys(
array_merge(
array_keys( current( AMP_Allowed_Tags_Generated::get_allowed_tag( $tag ) )['attr_spec_list'] ),
array_keys( AMP_Allowed_Tags_Generated::get_allowed_attributes() )
),
true
);
}
/**
* Checks whether the given node is within an AMP-specific <noscript> element.
*
* @since 1.1
*
* @param \DOMNode $node DOM node to check.
* @return bool True if in an AMP noscript element, false otherwise.
*/
protected function is_inside_amp_noscript( \DOMNode $node ) {
return 'noscript' === $node->parentNode->nodeName && $node->parentNode->parentNode && 'amp-' === substr( $node->parentNode->parentNode->nodeName, 0, 4 );
}
/**
* Appends the given old node in a <noscript> element to the new node.
*
* @since 1.1
*
* @param \DOMNode $new_node New node to append a noscript with the old node to.
* @param \DOMNode $old_node Old node to append in a noscript.
* @param \DOMDocument $dom DOM document instance.
*/
protected function append_old_node_noscript( \DOMNode $new_node, \DOMNode $old_node, \DOMDocument $dom ) {
$noscript = $dom->createElement( 'noscript' );
$noscript->appendChild( $old_node );
$new_node->appendChild( $noscript );
// Remove all non-allowed attributes preemptively to prevent doubled validation errors.
for ( $i = $old_node->attributes->length - 1; $i >= 0; $i-- ) {
$attribute = $old_node->attributes->item( $i );
if ( isset( $this->noscript_fallback_allowed_attributes[ $attribute->nodeName ] ) ) {
continue;
}
$old_node->removeAttribute( $attribute->nodeName );
}
}
}

View File

@ -0,0 +1,372 @@
<?php
/**
* Class AMP_Customizer_Design_Settings
*
* @package AMP
*/
/**
* Class AMP_Customizer_Design_Settings
*/
class AMP_Customizer_Design_Settings {
/**
* Default header color.
*
* @var string
*/
const DEFAULT_HEADER_COLOR = '#fff';
/**
* Default header background color.
*
* @var string
*/
const DEFAULT_HEADER_BACKGROUND_COLOR = '#0a89c0';
/**
* Default color scheme.
*
* @var string
*/
const DEFAULT_COLOR_SCHEME = 'light';
/**
* Returns whether the AMP design settings are enabled.
*
* @since 1.1 This always return false when AMP theme support is present.
* @since 0.6
*
* @return bool AMP Customizer design settings enabled.
*/
public static function is_amp_customizer_enabled() {
if ( current_theme_supports( 'amp' ) ) {
return false;
}
/**
* Filter whether to enable the AMP default template design settings.
*
* @since 0.4
* @since 0.6 This filter now controls whether or not the default settings, controls, and sections are registered for the Customizer. The AMP panel will be registered regardless.
* @param bool $enable Whether to enable the AMP default template design settings. Default true.
*/
return apply_filters( 'amp_customizer_is_enabled', true );
}
/**
* Init.
*/
public static function init() {
add_action( 'amp_customizer_init', array( __CLASS__, 'init_customizer' ) );
if ( self::is_amp_customizer_enabled() ) {
add_filter( 'amp_customizer_get_settings', array( __CLASS__, 'append_settings' ) );
}
}
/**
* Init customizer.
*/
public static function init_customizer() {
if ( self::is_amp_customizer_enabled() ) {
add_action( 'amp_customizer_register_settings', array( __CLASS__, 'register_customizer_settings' ) );
add_action( 'amp_customizer_register_ui', array( __CLASS__, 'register_customizer_ui' ) );
add_action( 'amp_customizer_enqueue_preview_scripts', array( __CLASS__, 'enqueue_customizer_preview_scripts' ) );
}
}
/**
* Register default Customizer settings for AMP.
*
* @param WP_Customize_Manager $wp_customize Manager.
*/
public static function register_customizer_settings( $wp_customize ) {
// Header text color setting.
$wp_customize->add_setting(
'amp_customizer[header_color]',
array(
'type' => 'option',
'default' => self::DEFAULT_HEADER_COLOR,
'sanitize_callback' => 'sanitize_hex_color',
'transport' => 'postMessage',
)
);
// Header background color.
$wp_customize->add_setting(
'amp_customizer[header_background_color]',
array(
'type' => 'option',
'default' => self::DEFAULT_HEADER_BACKGROUND_COLOR,
'sanitize_callback' => 'sanitize_hex_color',
'transport' => 'postMessage',
)
);
// Background color scheme.
$wp_customize->add_setting(
'amp_customizer[color_scheme]',
array(
'type' => 'option',
'default' => self::DEFAULT_COLOR_SCHEME,
'sanitize_callback' => array( __CLASS__, 'sanitize_color_scheme' ),
'transport' => 'postMessage',
)
);
// Display exit link.
$wp_customize->add_setting(
'amp_customizer[display_exit_link]',
array(
'type' => 'option',
'default' => false,
'sanitize_callback' => 'rest_sanitize_boolean',
'transport' => 'postMessage',
)
);
}
/**
* Register default Customizer sections and controls for AMP.
*
* @param WP_Customize_Manager $wp_customize Manager.
*/
public static function register_customizer_ui( $wp_customize ) {
$wp_customize->add_section(
'amp_design',
array(
'title' => __( 'Design', 'amp' ),
'panel' => AMP_Template_Customizer::PANEL_ID,
)
);
// Header text color control.
$wp_customize->add_control(
new WP_Customize_Color_Control(
$wp_customize,
'amp_header_color',
array(
'settings' => 'amp_customizer[header_color]',
'label' => __( 'Header Text Color', 'amp' ),
'section' => 'amp_design',
'priority' => 10,
)
)
);
// Header background color control.
$wp_customize->add_control(
new WP_Customize_Color_Control(
$wp_customize,
'amp_header_background_color',
array(
'settings' => 'amp_customizer[header_background_color]',
'label' => __( 'Header Background & Link Color', 'amp' ),
'section' => 'amp_design',
'priority' => 20,
)
)
);
// Background color scheme.
$wp_customize->add_control(
'amp_color_scheme',
array(
'settings' => 'amp_customizer[color_scheme]',
'label' => __( 'Color Scheme', 'amp' ),
'section' => 'amp_design',
'type' => 'radio',
'priority' => 30,
'choices' => self::get_color_scheme_names(),
)
);
// Display exit link.
$wp_customize->add_control(
'amp_display_exit_link',
array(
'settings' => 'amp_customizer[display_exit_link]',
'label' => __( 'Display link to exit reader mode?', 'amp' ),
'section' => 'amp_design',
'type' => 'checkbox',
'priority' => 40,
)
);
// Header.
$wp_customize->selective_refresh->add_partial(
'amp-wp-header',
array(
'selector' => '.amp-wp-header',
'settings' => array( 'blogname', 'amp_customizer[display_exit_link]' ), // @todo Site Icon.
'render_callback' => array( __CLASS__, 'render_header_bar' ),
'fallback_refresh' => false,
)
);
// Header.
$wp_customize->selective_refresh->add_partial(
'amp-wp-footer',
array(
'selector' => '.amp-wp-footer',
'settings' => array( 'blogname' ),
'render_callback' => array( __CLASS__, 'render_footer' ),
'fallback_refresh' => false,
'container_inclusive' => true,
)
);
}
/**
* Render header bar template.
*/
public static function render_header_bar() {
if ( is_singular() ) {
$post_template = new AMP_Post_Template( get_post() );
$post_template->load_parts( array( 'header-bar' ) );
}
}
/**
* Render footer template.
*/
public static function render_footer() {
if ( is_singular() ) {
$post_template = new AMP_Post_Template( get_post() );
$post_template->load_parts( array( 'footer' ) );
}
}
/**
* Enqueue scripts for default AMP Customizer preview.
*
* @global WP_Customize_Manager $wp_customize
*/
public static function enqueue_customizer_preview_scripts() {
global $wp_customize;
// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NoExplicitVersion
wp_enqueue_script(
'amp-customizer-design-preview',
amp_get_asset_url( 'js/amp-customizer-design-preview.js' ),
array( 'amp-customize-preview' ),
false,
true
);
wp_localize_script(
'amp-customizer-design-preview',
'amp_customizer_design',
array(
'color_schemes' => self::get_color_schemes(),
)
);
// Prevent a theme's registered blogname partial from causing full page refreshes.
$blogname_partial = $wp_customize->selective_refresh->get_partial( 'blogname' );
if ( $blogname_partial ) {
$blogname_partial->fallback_refresh = false;
}
}
/**
* Merge default Customizer settings on top of settings for merging into AMP post template.
*
* @see AMP_Post_Template::build_customizer_settings()
*
* @param array $settings Settings.
* @return array Merged settings.
*/
public static function append_settings( $settings ) {
$settings = wp_parse_args(
$settings,
array(
'header_color' => self::DEFAULT_HEADER_COLOR,
'header_background_color' => self::DEFAULT_HEADER_BACKGROUND_COLOR,
'color_scheme' => self::DEFAULT_COLOR_SCHEME,
'display_exit_link' => false,
)
);
$theme_colors = self::get_colors_for_color_scheme( $settings['color_scheme'] );
return array_merge(
$settings,
$theme_colors,
array(
'link_color' => $settings['header_background_color'],
)
);
}
/**
* Get color scheme names.
*
* @return array Color scheme names.
*/
protected static function get_color_scheme_names() {
return array(
'light' => __( 'Light', 'amp' ),
'dark' => __( 'Dark', 'amp' ),
);
}
/**
* Get color schemes.
*
* @return array Color schemes.
*/
protected static function get_color_schemes() {
return array(
'light' => array(
// Convert colors to greyscale for light theme color; see <http://goo.gl/2gDLsp>.
'theme_color' => '#fff',
'text_color' => '#353535',
'muted_text_color' => '#696969',
'border_color' => '#c2c2c2',
),
'dark' => array(
// Convert and invert colors to greyscale for dark theme color; see <http://goo.gl/uVB2cO>.
'theme_color' => '#0a0a0a',
'text_color' => '#dedede',
'muted_text_color' => '#b1b1b1',
'border_color' => '#707070',
),
);
}
/**
* Get colors for color scheme.
*
* @param string $scheme Color scheme.
* @return array Colors.
*/
protected static function get_colors_for_color_scheme( $scheme ) {
$color_schemes = self::get_color_schemes();
if ( isset( $color_schemes[ $scheme ] ) ) {
return $color_schemes[ $scheme ];
}
return $color_schemes[ self::DEFAULT_COLOR_SCHEME ];
}
/**
* Sanitize color scheme.
*
* @param string $value Color scheme name.
* @return string Sanitized name.
*/
public static function sanitize_color_scheme( $value ) {
$schemes = self::get_color_scheme_names();
$scheme_slugs = array_keys( $schemes );
if ( ! in_array( $value, $scheme_slugs, true ) ) {
$value = self::DEFAULT_COLOR_SCHEME;
}
return $value;
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* Class AMP_Customizer_Settings
*
* @package AMP
*/
/**
* Class AMP_Customizer_Settings
*/
class AMP_Customizer_Settings {
/**
* Gets the AMP Customizer settings directly from the option.
*
* @since 0.6
*
* @return array Associative array of $setting => $value pairs.
*/
private static function get_stored_options() {
return get_option( 'amp_customizer', array() );
}
/**
* Gets the AMP Customizer settings.
*
* @since 0.6
*
* @return array Associative array of $setting => $value pairs.
*/
public static function get_settings() {
$settings = self::get_stored_options();
/**
* Filters the AMP Customizer settings.
*
* @since 0.6
*
* @param array $settings Associative array of $setting => $value pairs.
*/
return apply_filters( 'amp_customizer_get_settings', $settings );
}
}

View File

@ -0,0 +1,132 @@
<?php
/**
* Class AMP_Content_Sanitizer
*
* @package AMP
*/
/**
* Class AMP_Content_Sanitizer
*
* @since 0.4.1
*/
class AMP_Content_Sanitizer {
/**
* Sanitize _content_.
*
* @since 0.4.1
* @since 0.7 Passing return_styles=false in $global_args causes stylesheets to be returned instead of styles.
* @deprecated Since 1.0
*
* @param string $content HTML content string or DOM document.
* @param string[] $sanitizer_classes Sanitizer classes.
* @param array $global_args Global args.
* @return array Tuple containing sanitized HTML, scripts array, and styles array (or stylesheets, if return_styles=false is passed in $global_args).
*/
public static function sanitize( $content, array $sanitizer_classes, $global_args = array() ) {
$dom = AMP_DOM_Utils::get_dom_from_content( $content );
// For back-compat.
if ( ! isset( $global_args['return_styles'] ) ) {
$global_args['return_styles'] = true;
}
$results = self::sanitize_document( $dom, $sanitizer_classes, $global_args );
return array(
AMP_DOM_Utils::get_content_from_dom( $dom ),
$results['scripts'],
empty( $global_args['return_styles'] ) ? $results['stylesheets'] : $results['styles'],
);
}
/**
* Sanitize document.
*
* @since 0.7
*
* @param DOMDocument $dom HTML document.
* @param string[] $sanitizer_classes Sanitizer classes.
* @param array $args Global args passed into sanitizers.
* @return array {
* Scripts and stylesheets needed by sanitizers.
*
* @type array $scripts Scripts.
* @type array $stylesheets Stylesheets. If $args['return_styles'] is empty.
* @type array $styles Styles. If $args['return_styles'] is not empty. For legacy purposes.
* }
*/
public static function sanitize_document( &$dom, $sanitizer_classes, $args ) {
$scripts = array();
$stylesheets = array();
$styles = array();
$return_styles = ! empty( $args['return_styles'] );
unset( $args['return_styles'] );
/**
* Sanitizers.
*
* @var AMP_Base_Sanitizer[] $sanitizers
*/
$sanitizers = array();
// Instantiate the sanitizers.
foreach ( $sanitizer_classes as $sanitizer_class => $sanitizer_args ) {
if ( ! class_exists( $sanitizer_class ) ) {
/* translators: %s is sanitizer class */
_doing_it_wrong( __METHOD__, sprintf( esc_html__( 'Sanitizer (%s) class does not exist', 'amp' ), esc_html( $sanitizer_class ) ), '0.4.1' );
continue;
}
/**
* Sanitizer.
*
* @type AMP_Base_Sanitizer $sanitizer
*/
$sanitizer = new $sanitizer_class( $dom, array_merge( $args, $sanitizer_args ) );
if ( ! is_subclass_of( $sanitizer, 'AMP_Base_Sanitizer' ) ) {
_doing_it_wrong(
__METHOD__,
esc_html(
sprintf(
/* translators: 1: sanitizer class. 2: AMP_Base_Sanitizer */
__( 'Sanitizer (%1$s) must extend `%2$s`', 'amp' ),
esc_html( $sanitizer_class ),
'AMP_Base_Sanitizer'
)
),
'0.1'
);
continue;
}
$sanitizers[ $sanitizer_class ] = $sanitizer;
}
// Let the sanitizers know about each other prior to sanitizing.
foreach ( $sanitizers as $sanitizer ) {
$sanitizer->init( $sanitizers );
}
// Sanitize.
foreach ( $sanitizers as $sanitizer_class => $sanitizer ) {
$sanitize_class_start = microtime( true );
$sanitizer->sanitize();
$scripts = array_merge( $scripts, $sanitizer->get_scripts() );
if ( $return_styles ) {
$styles = array_merge( $styles, $sanitizer->get_styles() );
} else {
$stylesheets = array_merge( $stylesheets, $sanitizer->get_stylesheets() );
}
AMP_HTTP::send_server_timing( 'amp_sanitize', -$sanitize_class_start, $sanitizer_class );
}
return compact( 'scripts', 'styles', 'stylesheets' );
}
}

View File

@ -0,0 +1,228 @@
<?php
/**
* Class AMP_Content
*
* @package AMP
*/
/**
* Class AMP_Content
*/
class AMP_Content {
/**
* Content.
*
* @var string
*/
private $content;
/**
* AMP content.
*
* @var string
*/
private $amp_content = '';
/**
* AMP scripts.
*
* @var array
*/
private $amp_scripts = array();
/**
* AMP styles.
*
* @deprecated
* @var array
*/
private $amp_styles = array();
/**
* AMP stylesheets.
*
* @since 1.0
* @var array
*/
private $amp_stylesheets = array();
/**
* Args.
*
* @var array
*/
private $args = array();
/**
* Embed handlers.
*
* @var AMP_Base_Embed_Handler[] AMP_Base_Embed_Handler[]
*/
private $embed_handlers = array();
/**
* Sanitizer class names.
*
* @var string[]
*/
private $sanitizer_classes = array();
/**
* AMP_Content constructor.
*
* @param string $content Content.
* @param string[] $embed_handler_classes Embed handler class names.
* @param string[] $sanitizer_classes Sanitizer class names.
* @param array $args Args.
*/
public function __construct( $content, $embed_handler_classes, $sanitizer_classes, $args = array() ) {
$this->content = $content;
$this->args = $args;
$this->embed_handlers = $this->register_embed_handlers( $embed_handler_classes );
$this->sanitizer_classes = $sanitizer_classes;
$this->sanitizer_classes['AMP_Embed_Sanitizer']['embed_handlers'] = $this->embed_handlers;
$this->transform();
}
/**
* Get AMP content.
*
* @return string
*/
public function get_amp_content() {
return $this->amp_content;
}
/**
* Get AMP scripts.
*
* @return array
*/
public function get_amp_scripts() {
return $this->amp_scripts;
}
/**
* Get AMP styles.
*
* @deprecated Since 1.0 in favor of the get_amp_stylesheets method.
* @return array Empty list.
*/
public function get_amp_styles() {
_deprecated_function( __METHOD__, '1.0', __CLASS__ . '::get_amp_stylesheets' );
return array();
}
/**
* Get AMP styles.
*
* @since 1.0
* @return array
*/
public function get_amp_stylesheets() {
return $this->amp_stylesheets;
}
/**
* Transform.
*/
private function transform() {
$content = $this->content;
// First, embeds + the_content filter.
$content = apply_filters( 'the_content', $content );
$this->unregister_embed_handlers( $this->embed_handlers );
// Then, sanitize to strip and/or convert non-amp content.
$content = $this->sanitize( $content );
$this->amp_content = $content;
}
/**
* Add scripts.
*
* @param array $scripts Scripts.
*/
private function add_scripts( $scripts ) {
$this->amp_scripts = array_merge( $this->amp_scripts, $scripts );
}
/**
* Add stylesheets.
*
* @since 1.0
* @param array $stylesheets Styles.
*/
private function add_stylesheets( $stylesheets ) {
$this->amp_stylesheets = array_merge( $this->amp_stylesheets, $stylesheets );
}
/**
* Register embed handlers.
*
* @param string[] $embed_handler_classes Embed handler class names.
* @return array
*/
private function register_embed_handlers( $embed_handler_classes ) {
$embed_handlers = array();
foreach ( $embed_handler_classes as $embed_handler_class => $args ) {
$embed_handler = new $embed_handler_class( array_merge( $this->args, $args ) );
if ( ! is_subclass_of( $embed_handler, 'AMP_Base_Embed_Handler' ) ) {
_doing_it_wrong(
__METHOD__,
esc_html(
sprintf(
/* translators: 1: embed handler. 2: AMP_Embed_Handler */
__( 'Embed Handler (%1$s) must extend `%2$s`', 'amp' ),
esc_html( $embed_handler_class ),
'AMP_Embed_Handler'
)
),
'0.1'
);
continue;
}
$embed_handler->register_embed();
$embed_handlers[] = $embed_handler;
}
return $embed_handlers;
}
/**
* Unregister embed handlers.
*
* @param array $embed_handlers Embed handlers.
*/
private function unregister_embed_handlers( $embed_handlers ) {
foreach ( $embed_handlers as $embed_handler ) {
$this->add_scripts( $embed_handler->get_scripts() );
$embed_handler->unregister_embed();
}
}
/**
* Sanitize.
*
* @see AMP_Content_Sanitizer::sanitize()
* @param string $content Content.
* @return string Sanitized content.
*/
private function sanitize( $content ) {
$dom = AMP_DOM_Utils::get_dom_from_content( $content );
$results = AMP_Content_Sanitizer::sanitize_document( $dom, $this->sanitizer_classes, $this->args );
$this->add_scripts( $results['scripts'] );
$this->add_stylesheets( $results['stylesheets'] );
return AMP_DOM_Utils::get_content_from_dom( $dom );
}
}

View File

@ -0,0 +1,487 @@
<?php
/**
* AMP_Post_Template class.
*
* @package AMP
*/
/**
* Class AMP_Post_Template
*
* @since 0.2
*/
class AMP_Post_Template {
/**
* Site icon size.
*
* @since 0.2
* @var int
*/
const SITE_ICON_SIZE = 32;
/**
* Content max width.
*
* @since 0.4
* @var int
*/
const CONTENT_MAX_WIDTH = 600;
/**
* Default navbar background.
*
* Needed for 0.3 back-compat
*
* @since 0.4
* @var string
*/
const DEFAULT_NAVBAR_BACKGROUND = '#0a89c0';
/**
* Default navbar color.
*
* Needed for 0.3 back-compat
*
* @since 0.4
* @var string
*/
const DEFAULT_NAVBAR_COLOR = '#fff';
/**
* Template directory.
*
* @since 0.2
* @var string
*/
private $template_dir;
/**
* Post template data.
*
* @since 0.2
* @var array
*/
private $data;
/**
* Post ID.
*
* @since 0.2
* @var int
*/
public $ID;
/**
* Post.
*
* @since 0.2
* @var WP_Post
*/
public $post;
/**
* AMP_Post_Template constructor.
*
* @param WP_Post|int $post Post.
*/
public function __construct( $post ) {
$this->template_dir = apply_filters( 'amp_post_template_dir', AMP__DIR__ . '/templates' );
if ( $post instanceof WP_Post ) {
$this->post = $post;
} else {
$this->post = get_post( $post );
}
// Make sure we have a post, or bail if not.
if ( is_a( $this->post, 'WP_Post' ) ) {
$this->ID = $this->post->ID;
} else {
return;
}
$content_max_width = self::CONTENT_MAX_WIDTH;
if ( isset( $GLOBALS['content_width'] ) && $GLOBALS['content_width'] > 0 ) {
$content_max_width = $GLOBALS['content_width'];
}
$content_max_width = apply_filters( 'amp_content_max_width', $content_max_width );
$this->data = array(
'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( $this->ID ),
'home_url' => home_url( '/' ),
'blog_name' => get_bloginfo( 'name' ),
'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 ) : '' ),
'placeholder_image_url' => amp_get_asset_url( 'images/placeholder-icon.png' ),
'featured_image' => false,
'comments_link_url' => false,
'comments_link_text' => false,
'amp_runtime_script' => 'https://cdn.ampproject.org/v0.js',
'amp_component_scripts' => array(),
'customizer_settings' => array(),
'font_urls' => array(),
'post_amp_stylesheets' => array(),
'post_amp_styles' => array(), // Deprecated.
'amp_analytics' => amp_add_custom_analytics(),
);
$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 );
}
/**
* Getter.
*
* @param string $property Property name.
* @param mixed $default Default value.
*
* @return mixed Value.
*/
public function get( $property, $default = null ) {
if ( isset( $this->data[ $property ] ) ) {
return $this->data[ $property ];
} else {
/* translators: %s is key name */
_doing_it_wrong( __METHOD__, esc_html( sprintf( __( 'Called for non-existent key ("%s").', 'amp' ), $property ) ), '0.1' );
}
return $default;
}
/**
* Get customizer setting.
*
* @param string $name Name.
* @param mixed $default Default value.
* @return mixed value.
*/
public function get_customizer_setting( $name, $default = null ) {
$settings = $this->get( 'customizer_settings' );
if ( ! empty( $settings[ $name ] ) ) {
return $settings[ $name ];
}
return $default;
}
/**
* Load and print the template parts for the given post.
*/
public function load() {
global $wp_query;
$template = is_page() || $wp_query->is_posts_page ? 'page' : 'single';
$this->load_parts( array( $template ) );
}
/**
* Load template parts.
*
* @param string[] $templates Templates.
*/
public function load_parts( $templates ) {
foreach ( $templates as $template ) {
$file = $this->get_template_path( $template );
$this->verify_and_include( $file, $template );
}
}
/**
* Get template path.
*
* @param string $template Template name.
* @return string Template path.
*/
private function get_template_path( $template ) {
return sprintf( '%s/%s.php', $this->template_dir, $template );
}
/**
* Add data.
*
* @param array $data Data.
*/
private function add_data( $data ) {
$this->data = array_merge( $this->data, $data );
}
/**
* Add data by key.
*
* @param string $key Key.
* @param mixed $value Value.
*/
private function add_data_by_key( $key, $value ) {
$this->data[ $key ] = $value;
}
/**
* Merge data for key.
*
* @param string $key Key.
* @param mixed $value Value.
*/
private function merge_data_for_key( $key, $value ) {
if ( is_array( $this->data[ $key ] ) ) {
$this->data[ $key ] = array_merge( $this->data[ $key ], $value );
} else {
$this->add_data_by_key( $key, $value );
}
}
/**
* Build post data.
*
* @since 0.2
*/
private function build_post_data() {
$post_title = get_the_title( $this->ID );
$post_publish_timestamp = get_the_date( 'U', $this->ID );
$post_modified_timestamp = get_post_modified_time( 'U', false, $this->post );
$post_author = get_userdata( $this->post->post_author );
$data = array(
'post' => $this->post,
'post_id' => $this->ID,
'post_title' => $post_title,
'post_publish_timestamp' => $post_publish_timestamp,
'post_modified_timestamp' => $post_modified_timestamp,
'post_author' => $post_author,
'post_canonical_link_url' => '',
'post_canonical_link_text' => '',
);
$customizer_settings = AMP_Customizer_Settings::get_settings();
if ( ! empty( $customizer_settings['display_exit_link'] ) ) {
$data['post_canonical_link_url'] = get_permalink( $this->ID );
$data['post_canonical_link_text'] = __( 'Exit Reader Mode', 'amp' );
}
$this->add_data( $data );
$this->build_post_featured_image();
$this->build_post_comments_data();
}
/**
* Buuild post comments data.
*/
private function build_post_comments_data() {
if ( ! post_type_supports( $this->post->post_type, 'comments' ) ) {
return;
}
$comments_open = comments_open( $this->ID );
// Don't show link if close and no comments.
if ( ! $comments_open
&& ! $this->post->comment_count ) {
return;
}
$comments_link_url = get_comments_link( $this->ID );
$comments_link_text = $comments_open
? __( 'Leave a Comment', 'amp' )
: __( 'View Comments', 'amp' );
$this->add_data(
array(
'comments_link_url' => $comments_link_url,
'comments_link_text' => $comments_link_text,
)
);
}
/**
* Build post content.
*/
private function build_post_content() {
$amp_content = new AMP_Content(
$this->post->post_content,
amp_get_content_embed_handlers( $this->post ),
amp_get_content_sanitizers( $this->post ),
array(
'content_max_width' => $this->get( 'content_max_width' ),
)
);
$this->add_data_by_key( 'post_amp_content', $amp_content->get_amp_content() );
$this->merge_data_for_key( 'amp_component_scripts', $amp_content->get_amp_scripts() );
$this->add_data_by_key( 'post_amp_stylesheets', $amp_content->get_amp_stylesheets() );
}
/**
* Build post featured image.
*/
private function build_post_featured_image() {
$post_id = $this->ID;
$featured_html = get_the_post_thumbnail( $post_id, 'large' );
// Skip featured image if no featured image is available.
if ( ! $featured_html ) {
return;
}
$featured_id = get_post_thumbnail_id( $post_id );
// If an image with the same ID as the featured image exists in the content, skip the featured image markup.
// Prevents duplicate images, which is especially problematic for photo blogs.
// A bit crude but it's fast and should cover most cases.
$post_content = $this->post->post_content;
if ( false !== strpos( $post_content, 'wp-image-' . $featured_id )
|| false !== strpos( $post_content, 'attachment_' . $featured_id ) ) {
return;
}
$featured_image = get_post( $featured_id );
$dom = AMP_DOM_Utils::get_dom_from_content( $featured_html );
$assets = AMP_Content_Sanitizer::sanitize_document(
$dom,
amp_get_content_sanitizers( $this->post ),
array(
'content_max_width' => $this->get( 'content_max_width' ),
)
);
$sanitized_html = AMP_DOM_Utils::get_content_from_dom( $dom );
$this->add_data_by_key(
'featured_image',
array(
'amp_html' => $sanitized_html,
'caption' => $featured_image->post_excerpt,
)
);
if ( $assets['scripts'] ) {
$this->merge_data_for_key( 'amp_component_scripts', $assets['scripts'] );
}
if ( $assets['stylesheets'] ) {
$this->merge_data_for_key( 'post_amp_stylesheets', $assets['stylesheets'] );
}
}
/**
* Build customizer settings.
*/
private function build_customizer_settings() {
$settings = AMP_Customizer_Settings::get_settings();
/**
* Filter AMP Customizer settings.
*
* Inject your Customizer settings here to make them accessible via the getter in your custom style.php template.
*
* Example:
*
* echo esc_html( $this->get_customizer_setting( 'your_setting_key', 'your_default_value' ) );
*
* @since 0.4
*
* @param array $settings Array of AMP Customizer settings.
* @param WP_Post $post Current post object.
*/
$this->add_data_by_key( 'customizer_settings', apply_filters( 'amp_post_template_customizer_settings', $settings, $this->post ) );
}
/**
* Build HTML tag attributes.
*/
private function build_html_tag_attributes() {
$attributes = array();
if ( function_exists( 'is_rtl' ) && is_rtl() ) {
$attributes['dir'] = 'rtl';
}
$lang = get_bloginfo( 'language' );
if ( $lang ) {
$attributes['lang'] = $lang;
}
$this->add_data_by_key( 'html_tag_attributes', $attributes );
}
/**
* Verify and include.
*
* @param string $file File.
* @param string $template_type Template type.
*/
private function verify_and_include( $file, $template_type ) {
$located_file = $this->locate_template( $file );
if ( $located_file ) {
$file = $located_file;
}
$file = apply_filters( 'amp_post_template_file', $file, $template_type, $this->post );
if ( ! $this->is_valid_template( $file ) ) {
/* translators: 1: the template file, 2: WP_CONTENT_DIR. */
_doing_it_wrong( __METHOD__, sprintf( esc_html__( 'Path validation for template (%1$s) failed. Path cannot traverse and must be located in `%2$s`.', 'amp' ), esc_html( $file ), 'WP_CONTENT_DIR' ), '0.1' );
return;
}
do_action( 'amp_post_template_include_' . $template_type, $this );
include $file;
}
/**
* Locate template.
*
* @param string $file File.
* @return string The template filename if one is located.
*/
private function locate_template( $file ) {
$search_file = sprintf( 'amp/%s', basename( $file ) );
return locate_template( array( $search_file ), false );
}
/**
* Is valid template.
*
* @param string $template Template name.
* @return bool Whether valid.
*/
private function is_valid_template( $template ) {
if ( false !== strpos( $template, '..' ) ) {
return false;
}
if ( false !== strpos( $template, './' ) ) {
return false;
}
if ( ! file_exists( $template ) ) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,624 @@
<?php
/**
* Class AMP_DOM_Utils.
*
* @package AMP
*/
/**
* Class AMP_DOM_Utils
*
* Functionality to simplify working with DOMDocuments and DOMElements.
*/
class AMP_DOM_Utils {
/**
* HTML elements that are self-closing.
*
* Not all are valid AMP, but we include them for completeness.
*
* @since 0.7
* @link https://www.w3.org/TR/html5/syntax.html#serializing-html-fragments
* @var array
*/
private static $self_closing_tags = array(
'area',
'base',
'basefont',
'bgsound',
'br',
'col',
'embed',
'frame',
'hr',
'img',
'input',
'keygen',
'link',
'meta',
'param',
'source',
'track',
'wbr',
);
/**
* Stored noscript/comment replacements for libxml<2.8.
*
* @since 0.7
* @var array
*/
public static $noscript_placeholder_comments = array();
/**
* Return a valid DOMDocument representing HTML document passed as a parameter.
*
* @since 0.7
* @see AMP_DOM_Utils::get_content_from_dom_node()
*
* @param string $document Valid HTML document to be represented by a DOMDocument.
* @return DOMDocument|false Returns DOMDocument, or false if conversion failed.
*/
public static function get_dom( $document ) {
$libxml_previous_state = libxml_use_internal_errors( true );
$dom = new DOMDocument();
// @todo In the future consider an AMP_DOMDocument subclass that does this automatically. See <https://github.com/ampproject/amp-wp/pull/895/files#r163825513>.
$document = self::convert_amp_bind_attributes( $document );
// Force all self-closing tags to have closing tags since DOMDocument isn't fully aware.
$document = preg_replace(
'#<(' . implode( '|', self::$self_closing_tags ) . ')[^>]*>(?!</\1>)#',
'$0</$1>',
$document
);
// Deal with bugs in older versions of libxml.
$added_back_compat_meta_content_type = false;
if ( version_compare( LIBXML_DOTTED_VERSION, '2.8', '<' ) ) {
/*
* Replace noscript elements with placeholders since libxml<2.8 can parse them incorrectly.
* When appearing in the head element, a noscript can cause the head to close prematurely
* and the noscript gets moved to the body and anything after it which was in the head.
* See <https://stackoverflow.com/questions/39013102/why-does-noscript-move-into-body-tag-instead-of-head-tag>.
* This is limited to only running in the head element because this is where the problem lies,
* and it is important for the AMP_Script_Sanitizer to be able to access the noscript elements
* in the body otherwise.
*/
$document = preg_replace_callback(
'#^.+?(?=<body)#is',
function( $head_matches ) {
return preg_replace_callback(
'#<noscript[^>]*>.*?</noscript>#si',
function( $noscript_matches ) {
$placeholder = sprintf( '<!--noscript:%s-->', (string) wp_rand() );
AMP_DOM_Utils::$noscript_placeholder_comments[ $placeholder ] = $noscript_matches[0];
return $placeholder;
},
$head_matches[0]
);
},
$document
);
/*
* Add a pre-HTML5-style declaration of the encoding since libxml<2.8 doesn't recognize
* HTML5's meta charset. See <https://bugzilla.gnome.org/show_bug.cgi?id=655218>.
*/
$document = preg_replace(
'#(?=<meta\s+charset=["\']?([a-z0-9_-]+))#i',
'<meta http-equiv="Content-Type" content="text/html; charset=$1" id="meta-http-equiv-content-type">',
$document,
1,
$count
);
if ( 1 === $count ) {
$added_back_compat_meta_content_type = true;
}
}
/*
* Wrap in dummy tags, since XML needs one parent node.
* It also makes it easier to loop through nodes.
* We can later use this to extract our nodes.
* Add charset so loadHTML does not have problems parsing it.
*/
$result = $dom->loadHTML( $document );
libxml_clear_errors();
libxml_use_internal_errors( $libxml_previous_state );
if ( ! $result ) {
return false;
}
// Remove pre-HTML5-style encoding declaration if added above.
if ( $added_back_compat_meta_content_type ) {
$meta_http_equiv_element = $dom->getElementById( 'meta-http-equiv-content-type' );
if ( $meta_http_equiv_element ) {
$meta_http_equiv_element->parentNode->removeChild( $meta_http_equiv_element );
}
}
return $dom;
}
/**
* Get attribute prefix for converted amp-bind attributes.
*
* This contains a random string to prevent HTML content containing this data- attribute
* originally from being mutated to contain an amp-bind attribute when attributes are restored.
*
* @since 0.7
* @see \AMP_DOM_Utils::convert_amp_bind_attributes()
* @see \AMP_DOM_Utils::restore_amp_bind_attributes()
* @link https://www.ampproject.org/docs/reference/components/amp-bind
*
* @return string HTML5 data-* attribute name prefix for AMP binding attributes.
*/
public static function get_amp_bind_placeholder_prefix() {
static $attribute_prefix;
if ( ! isset( $attribute_prefix ) ) {
$attribute_prefix = sprintf( 'amp-binding-%s-', md5( wp_rand() ) );
}
return $attribute_prefix;
}
/**
* Get amp-mustache tag/placeholder mappings.
*
* @since 0.7
* @see \wpdb::placeholder_escape()
*
* @return array Mapping of mustache tag token to its placeholder.
*/
private static function get_mustache_tag_placeholders() {
static $placeholders;
if ( ! isset( $placeholders ) ) {
$salt = wp_rand();
// Note: The order of these tokens is important, as it determines the order of the order of the replacements.
$tokens = array(
'{{{',
'}}}',
'{{#',
'{{^',
'{{/',
'{{/',
'{{',
'}}',
);
$placeholders = array();
foreach ( $tokens as $token ) {
$placeholders[ $token ] = '_amp_mustache_' . md5( $salt . $token );
}
}
return $placeholders;
}
/**
* Replace AMP binding attributes with something that libxml can parse (as HTML5 data-* attributes).
*
* This is necessary because attributes in square brackets are not understood in PHP and
* get dropped with an error raised:
* > Warning: DOMDocument::loadHTML(): error parsing attribute name
* This is a reciprocal function of AMP_DOM_Utils::restore_amp_bind_attributes().
*
* @since 0.7
* @see \AMP_DOM_Utils::convert_amp_bind_attributes()
* @link https://www.ampproject.org/docs/reference/components/amp-bind
*
* @param string $html HTML containing amp-bind attributes.
* @return string HTML with AMP binding attributes replaced with HTML5 data-* attributes.
*/
public static function convert_amp_bind_attributes( $html ) {
$amp_bind_attr_prefix = self::get_amp_bind_placeholder_prefix();
// Pattern for HTML attribute accounting for binding attr name, boolean attribute, single/double-quoted attribute value, and unquoted attribute values.
$attr_regex = '#^\s+(?P<name>\[?[a-zA-Z0-9_\-]+\]?)(?P<value>=(?:"[^"]*+"|\'[^\']*+\'|[^\'"\s]+))?#';
/**
* Replace callback.
*
* @param array $tag_matches Tag matches.
* @return string Replacement.
*/
$replace_callback = function( $tag_matches ) use ( $amp_bind_attr_prefix, $attr_regex ) {
$old_attrs = rtrim( $tag_matches['attrs'] );
$new_attrs = '';
$offset = 0;
while ( preg_match( $attr_regex, substr( $old_attrs, $offset ), $attr_matches ) ) {
$offset += strlen( $attr_matches[0] );
if ( '[' === $attr_matches['name'][0] ) {
$new_attrs .= ' ' . $amp_bind_attr_prefix . trim( $attr_matches['name'], '[]' );
if ( isset( $attr_matches['value'] ) ) {
$new_attrs .= $attr_matches['value'];
}
} else {
$new_attrs .= $attr_matches[0];
}
}
// Bail on parse error which occurs when the regex isn't able to consume the entire $new_attrs string.
if ( strlen( $old_attrs ) !== $offset ) {
return $tag_matches[0];
}
return '<' . $tag_matches['name'] . $new_attrs . '>';
};
// Match all start tags that contain a binding attribute.
$pattern = join(
'',
array(
'#<',
'(?P<name>[a-zA-Z0-9_\-]+)', // Tag name.
'(?P<attrs>\s', // Attributes.
'(?:[^>"\'\[\]]+|"[^"]*+"|\'[^\']*+\')*+', // Non-binding attributes tokens.
'\[[a-zA-Z0-9_\-]+\]', // One binding attribute key.
'(?:[^>"\']+|"[^"]*+"|\'[^\']*+\')*+', // Any attribute tokens, including binding ones.
')>#s',
)
);
$converted = preg_replace_callback(
$pattern,
$replace_callback,
$html
);
/**
* If the regex engine incurred an error during processing, for example exceeding the backtrack
* limit, $converted will be null. In this case we return the originally passed document to allow
* DOMDocument to attempt to load it. If the AMP HTML doesn't make use of amp-bind or similar
* attributes, then everything should still work.
*
* See https://github.com/ampproject/amp-wp/issues/993 for additional context on this issue.
* See http://php.net/manual/en/pcre.constants.php for additional info on PCRE errors.
*/
return ( ! is_null( $converted ) ) ? $converted : $html;
}
/**
* Convert AMP bind-attributes back to their original syntax.
*
* This is a reciprocal function of AMP_DOM_Utils::convert_amp_bind_attributes().
*
* @since 0.7
* @see \AMP_DOM_Utils::convert_amp_bind_attributes()
* @link https://www.ampproject.org/docs/reference/components/amp-bind
*
* @param string $html HTML with amp-bind attributes converted.
* @return string HTML with amp-bind attributes restored.
*/
public static function restore_amp_bind_attributes( $html ) {
$html = preg_replace(
'#\s' . self::get_amp_bind_placeholder_prefix() . '([a-zA-Z0-9_\-]+)#',
' [$1]',
$html
);
return $html;
}
/**
* Return a valid DOMDocument representing arbitrary HTML content passed as a parameter.
*
* @see Reciprocal function get_content_from_dom()
*
* @since 0.2
*
* @param string $content Valid HTML content to be represented by a DOMDocument.
*
* @return DOMDocument|false Returns DOMDocument, or false if conversion failed.
*/
public static function get_dom_from_content( $content ) {
/*
* Wrap in dummy tags, since XML needs one parent node.
* It also makes it easier to loop through nodes.
* We can later use this to extract our nodes.
* Add utf-8 charset so loadHTML does not have problems parsing it.
* See: http://php.net/manual/en/domdocument.loadhtml.php#78243
*/
$document = sprintf(
'<html><head><meta http-equiv="content-type" content="text/html; charset=%s"></head><body>%s</body></html>',
get_bloginfo( 'charset' ),
$content
);
return self::get_dom( $document );
}
/**
* Return valid HTML *body* content extracted from the DOMDocument passed as a parameter.
*
* @since 0.2
* @see AMP_DOM_Utils::get_content_from_dom_node() Reciprocal function.
*
* @param DOMDocument $dom Represents an HTML document from which to extract HTML content.
* @return string Returns the HTML content of the body element represented in the DOMDocument.
*/
public static function get_content_from_dom( $dom ) {
$body = $dom->getElementsByTagName( 'body' )->item( 0 );
// The DOMDocument may contain no body. In which case return nothing.
if ( is_null( $body ) ) {
return '';
}
return preg_replace(
'#^.*?<body.*?>(.*)</body>.*?$#si',
'$1',
self::get_content_from_dom_node( $dom, $body )
);
}
/**
* Return valid HTML content extracted from the DOMNode passed as a parameter.
*
* @since 0.6
* @see AMP_DOM_Utils::get_dom() Where the operations in this method are mirrored.
* @see AMP_DOM_Utils::get_content_from_dom() Reciprocal function.
* @todo In the future consider an AMP_DOMDocument subclass that does this automatically at saveHTML(). See <https://github.com/ampproject/amp-wp/pull/895/files#r163825513>.
*
* @param DOMDocument $dom Represents an HTML document.
* @param DOMElement $node Represents an HTML element of the $dom from which to extract HTML content.
* @return string Returns the HTML content represented in the DOMNode
*/
public static function get_content_from_dom_node( $dom, $node ) {
/**
* Self closing tags regex.
*
* @var string Regular expression to match self-closing tags
* that saveXML() has generated a closing tag for.
*/
static $self_closing_tags_regex;
/*
* Cache this regex so we don't have to recreate it every call.
*/
if ( ! isset( $self_closing_tags_regex ) ) {
$self_closing_tags = implode( '|', self::$self_closing_tags );
$self_closing_tags_regex = "#</({$self_closing_tags})>#i";
}
/*
* Prevent amp-mustache syntax from getting URL-encoded in attributes when saveHTML is done.
* While this is applying to the entire document, it only really matters inside of <template>
* elements, since URL-encoding of curly braces in href attributes would not normally matter.
* But when this is done inside of a <template> then it breaks Mustache. Since Mustache
* is logic-less and curly braces are not unsafe for HTML, we can do a global replacement.
* The replacement is done on the entire HTML document instead of just inside of the <template>
* elements since it is faster and wouldn't change the outcome.
*/
$mustache_tag_placeholders = self::get_mustache_tag_placeholders();
$mustache_tags_replaced = false;
$xpath = new DOMXPath( $dom );
$templates = $dom->getElementsByTagName( 'template' );
foreach ( $templates as $template ) {
// These attributes are the only ones that saveHTML() will URL-encode.
foreach ( $xpath->query( './/*/@src|.//*/@href|.//*/@action', $template ) as $attribute ) {
$attribute->nodeValue = str_replace(
array_keys( $mustache_tag_placeholders ),
array_values( $mustache_tag_placeholders ),
$attribute->nodeValue,
$count
);
if ( $count ) {
$mustache_tags_replaced = true;
}
}
}
if ( version_compare( PHP_VERSION, '7.3', '>=' ) ) {
$html = $dom->saveHTML( $node );
} else {
/*
* Temporarily add fragment boundary comments in order to locate the desired node to extract from
* the given HTML document. This is required because libxml seems to only preserve whitespace when
* serializing when calling DOMDocument::saveHTML() on the entire document. If you pass the element
* to DOMDocument::saveHTML() then formatting whitespace gets added unexpectedly. This is seen to
* be fixed in PHP 7.3, but for older versions of PHP the following workaround is needed.
*/
/*
* First make sure meta[charset] gets http-equiv and content attributes to work around issue
* with $dom->saveHTML() erroneously encoding UTF-8 as HTML entities.
*/
$meta_charset = $xpath->query( '/html/head/meta[ @charset ]' )->item( 0 );
if ( $meta_charset ) {
$meta_charset->setAttribute( 'http-equiv', 'Content-Type' );
$meta_charset->setAttribute( 'content', sprintf( 'text/html; charset=%s', $meta_charset->getAttribute( 'charset' ) ) );
}
$boundary = 'fragment_boundary:' . (string) wp_rand();
$start_boundary = $boundary . ':start';
$end_boundary = $boundary . ':end';
$comment_start = $dom->createComment( $start_boundary );
$comment_end = $dom->createComment( $end_boundary );
$node->parentNode->insertBefore( $comment_start, $node );
$node->parentNode->insertBefore( $comment_end, $node->nextSibling );
$html = preg_replace(
'/^.*?' . preg_quote( "<!--$start_boundary-->", '/' ) . '(.*)' . preg_quote( "<!--$end_boundary-->", '/' ) . '.*?\s*$/s',
'$1',
$dom->saveHTML()
);
// Remove meta[http-equiv] and meta[content] attributes which were added to meta[charset] for HTML serialization.
if ( $meta_charset ) {
if ( $dom->documentElement === $node ) {
$html = preg_replace( '#(<meta\scharset=\S+)[^<]*?>#i', '$1>', $html );
}
$meta_charset->removeAttribute( 'http-equiv' );
$meta_charset->removeAttribute( 'content' );
}
$node->parentNode->removeChild( $comment_start );
$node->parentNode->removeChild( $comment_end );
}
// Whitespace just causes unit tests to fail... so whitespace begone.
if ( '' === trim( $html ) ) {
return '';
}
// Restore amp-mustache placeholders which were replaced to prevent URL-encoded corruption by saveHTML.
if ( $mustache_tags_replaced ) {
$html = str_replace(
array_values( $mustache_tag_placeholders ),
array_keys( $mustache_tag_placeholders ),
$html
);
}
// Restore noscript elements which were temporarily removed to prevent libxml<2.8 parsing problems.
if ( version_compare( LIBXML_DOTTED_VERSION, '2.8', '<' ) ) {
$html = str_replace(
array_keys( self::$noscript_placeholder_comments ),
array_values( self::$noscript_placeholder_comments ),
$html
);
}
$html = self::restore_amp_bind_attributes( $html );
/*
* Travis w/PHP 7.1 generates <br></br> and <hr></hr> vs. <br/> and <hr/>, respectively.
* Travis w/PHP 7.x generates <source ...></source> vs. <source ... />. Etc.
* Seems like LIBXML_NOEMPTYTAG was passed, but as you can see it was not.
* This does not happen in my (@mikeschinkel) local testing, btw.
*/
$html = preg_replace( $self_closing_tags_regex, '', $html );
return $html;
}
/**
* Create a new node w/attributes (a DOMElement) and add to the passed DOMDocument.
*
* @since 0.2
*
* @param DOMDocument $dom A representation of an HTML document to add the new node to.
* @param string $tag A valid HTML element tag for the element to be added.
* @param string[] $attributes One of more valid attributes for the new node.
*
* @return DOMElement|false The DOMElement for the given $tag, or false on failure
*/
public static function create_node( $dom, $tag, $attributes ) {
$node = $dom->createElement( $tag );
self::add_attributes_to_node( $node, $attributes );
return $node;
}
/**
* Extract a DOMElement node's HTML element attributes and return as an array.
*
* @since 0.2
*
* @param DOMElement $node Represents an HTML element for which to extract attributes.
*
* @return string[] The attributes for the passed node, or an
* empty array if it has no attributes.
*/
public static function get_node_attributes_as_assoc_array( $node ) {
$attributes = array();
if ( ! $node->hasAttributes() ) {
return $attributes;
}
foreach ( $node->attributes as $attribute ) {
$attributes[ $attribute->nodeName ] = $attribute->nodeValue;
}
return $attributes;
}
/**
* Add one or more HTML element attributes to a node's DOMElement.
*
* @since 0.2
*
* @param DOMElement $node Represents an HTML element.
* @param string[] $attributes One or more attributes for the node's HTML element.
*/
public static function add_attributes_to_node( $node, $attributes ) {
foreach ( $attributes as $name => $value ) {
$node->setAttribute( $name, $value );
}
}
/**
* Determines if a DOMElement's node is empty or not..
*
* @since 0.2
*
* @param DOMElement $node Represents an HTML element.
* @return bool Returns true if the DOMElement has no child nodes and
* the textContent property of the DOMElement is empty;
* Otherwise it returns false.
*/
public static function is_node_empty( $node ) {
return false === $node->hasChildNodes() && empty( $node->textContent );
}
/**
* Forces HTML element closing tags given a DOMDocument and optional DOMElement
*
* @since 0.2
* @deprecated
*
* @param DOMDocument $dom Represents HTML document on which to force closing tags.
* @param DOMElement $node Represents HTML element to start closing tags on.
* If not passed, defaults to first child of body.
*/
public static function recursive_force_closing_tags( $dom, $node = null ) {
_deprecated_function( __METHOD__, '0.7' );
if ( is_null( $node ) ) {
$node = $dom->getElementsByTagName( 'body' )->item( 0 );
}
if ( XML_ELEMENT_NODE !== $node->nodeType ) {
return;
}
if ( self::is_self_closing_tag( $node->nodeName ) ) {
/*
* Ensure there is no text content to accidentally force a child
*/
$node->textContent = null;
return;
}
if ( self::is_node_empty( $node ) ) {
$text_node = $dom->createTextNode( '' );
$node->appendChild( $text_node );
return;
}
$num_children = $node->childNodes->length;
for ( $i = $num_children - 1; $i >= 0; $i -- ) {
$child = $node->childNodes->item( $i );
self::recursive_force_closing_tags( $dom, $child );
}
}
/**
* Determines if an HTML element tag is validly a self-closing tag per W3C HTML5 specs.
*
* @since 0.2
*
* @param string $tag Tag.
* @return bool Returns true if a valid self-closing tag, false if not.
*/
private static function is_self_closing_tag( $tag ) {
return in_array( strtolower( $tag ), self::$self_closing_tags, true );
}
}

View File

@ -0,0 +1,62 @@
<?php
/**
* Class AMP_HTML_Utils
*
* @package AMP
*/
/**
* Class with static HTML utility methods.
*/
class AMP_HTML_Utils {
/**
* Generates HTML markup for a given tag, attributes and content.
*
* @param string $tag_name Tag name.
* @param array $attributes Associative array of $attribute => $value pairs.
* @param string $content Inner content for the generated node.
* @return string HTML markup.
*/
public static function build_tag( $tag_name, $attributes = array(), $content = '' ) {
$attr_string = self::build_attributes_string( $attributes );
return sprintf( '<%1$s %2$s>%3$s</%1$s>', sanitize_key( $tag_name ), $attr_string, $content );
}
/**
* Generates a HTML attributes string from given attributes.
*
* @param array $attributes Associative array of $attribute => $value pairs.
* @return string HTML attributes string.
*/
public static function build_attributes_string( $attributes ) {
$string = array();
foreach ( $attributes as $name => $value ) {
if ( '' === $value ) {
$string[] = sprintf( '%s', sanitize_key( $name ) );
} else {
$string[] = sprintf( '%s="%s"', sanitize_key( $name ), esc_attr( $value ) );
}
}
return implode( ' ', $string );
}
/**
* Checks whether the given string is valid JSON.
*
* @param string $data String hopefully containing JSON.
* @return bool True if the string is valid JSON, false otherwise.
*/
public static function is_valid_json( $data ) {
if ( ! empty( $data ) ) {
$decoded = json_decode( $data );
if ( function_exists( 'json_last_error' ) ) {
return ( json_last_error() === JSON_ERROR_NONE );
} else {
// For PHP 5.2 back-compatibility.
return null !== $decoded;
}
}
return false;
}
}

View File

@ -0,0 +1,285 @@
<?php
/**
* Class AMP_Image_Dimension_Extractor
*
* @package AMP
*/
/**
* Class with static methods to extract image dimensions.
*/
class AMP_Image_Dimension_Extractor {
const STATUS_FAILED_LAST_ATTEMPT = 'failed';
const STATUS_IMAGE_EXTRACTION_FAILED = 'failed';
/**
* Internal flag whether callbacks have been registered.
*
* @var bool
*/
private static $callbacks_registered = false;
/**
* Extracts dimensions from image URLs.
*
* @since 0.2
*
* @param array|string $urls Array of URLs to extract dimensions from, or a single URL string.
* @return array|string Extracted dimensions keyed by original URL, or else the single set of dimensions if one URL string is passed.
*/
public static function extract( $urls ) {
if ( ! self::$callbacks_registered ) {
self::register_callbacks();
}
$return_dimensions = array();
// Back-compat for users calling this method directly.
$is_single = is_string( $urls );
if ( $is_single ) {
$urls = array( $urls );
}
// Normalize URLs and also track a map of normalized-to-original as we'll need it to reformat things when returning the data.
$url_map = array();
$normalized_urls = array();
foreach ( $urls as $original_url ) {
$normalized_url = self::normalize_url( $original_url );
if ( false !== $normalized_url ) {
$url_map[ $original_url ] = $normalized_url;
$normalized_urls[] = $normalized_url;
} else {
// This is not a URL we can extract dimensions from, so default to false.
$return_dimensions[ $original_url ] = false;
}
}
$extracted_dimensions = array_fill_keys( $normalized_urls, false );
$extracted_dimensions = apply_filters( 'amp_extract_image_dimensions_batch', $extracted_dimensions );
// We need to return a map with the original (un-normalized URL) as we that to match nodes that need dimensions.
foreach ( $url_map as $original_url => $normalized_url ) {
$return_dimensions[ $original_url ] = $extracted_dimensions[ $normalized_url ];
}
// Back-compat: just return the dimensions, not the full mapped array.
if ( $is_single ) {
return current( $return_dimensions );
}
return $return_dimensions;
}
/**
* Normalizes the given URL.
*
* This method ensures the URL has a scheme and, if relative, is prepended the WordPress site URL.
*
* @param string $url URL to normalize.
* @return string Normalized URL.
*/
public static function normalize_url( $url ) {
if ( empty( $url ) ) {
return false;
}
if ( 0 === strpos( $url, 'data:' ) ) {
return false;
}
$normalized_url = $url;
if ( 0 === strpos( $url, '//' ) ) {
$normalized_url = set_url_scheme( $url, 'http' );
} else {
$parsed = wp_parse_url( $url );
if ( ! isset( $parsed['host'] ) ) {
$path = '';
if ( isset( $parsed['path'] ) ) {
$path .= $parsed['path'];
}
if ( isset( $parsed['query'] ) ) {
$path .= '?' . $parsed['query'];
}
$home = home_url();
$home_path = wp_parse_url( $home, PHP_URL_PATH );
if ( ! empty( $home_path ) ) {
$home = substr( $home, 0, - strlen( $home_path ) );
}
$normalized_url = $home . $path;
}
}
/**
* Apply filters on the normalized image URL for dimension extraction.
*
* @since 1.1
*
* @param string $normalized_url Normalized image URL.
* @param string $url Original image URL.
*/
$normalized_url = apply_filters( 'amp_normalized_dimension_extractor_image_url', $normalized_url, $url );
return $normalized_url;
}
/**
* Registers the necessary callbacks.
*/
private static function register_callbacks() {
self::$callbacks_registered = true;
add_filter( 'amp_extract_image_dimensions_batch', array( __CLASS__, 'extract_by_downloading_images' ), 999, 1 );
do_action( 'amp_extract_image_dimensions_batch_callbacks_registered' );
}
/**
* Extract dimensions from downloaded images (or transient/cached dimensions from downloaded images)
*
* @param array $dimensions Image urls mapped to dimensions.
* @param false $mode Deprecated.
* @return array Dimensions mapped to image urls, or false if they could not be retrieved
*/
public static function extract_by_downloading_images( $dimensions, $mode = false ) {
if ( $mode ) {
_deprecated_argument( __METHOD__, 'AMP 1.1' );
}
$transient_expiration = 30 * DAY_IN_SECONDS;
$urls_to_fetch = array();
$images = array();
self::determine_which_images_to_fetch( $dimensions, $urls_to_fetch );
try {
self::fetch_images( $urls_to_fetch, $images );
self::process_fetched_images( $urls_to_fetch, $images, $dimensions, $transient_expiration );
} catch ( \Exception $exception ) {
trigger_error( esc_html( $exception->getMessage() ), E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
}
return $dimensions;
}
/**
* Determine which images to fetch by checking for dimensions in transient/cache.
* Creates a short lived transient that acts as a semaphore so that another visitor
* doesn't trigger a remote fetch for the same image at the same time.
*
* @param array $dimensions Image urls mapped to dimensions.
* @param array $urls_to_fetch Urls of images to fetch because dimensions are not in transient/cache.
*/
private static function determine_which_images_to_fetch( &$dimensions, &$urls_to_fetch ) {
foreach ( $dimensions as $url => $value ) {
// Check whether some other callback attached to the filter already provided dimensions for this image.
if ( is_array( $value ) ) {
continue;
}
$url_hash = md5( $url );
$transient_name = sprintf( 'amp_img_%s', $url_hash );
$cached_dimensions = get_transient( $transient_name );
// If we're able to retrieve the dimensions from a transient, set them and move on.
if ( is_array( $cached_dimensions ) ) {
$dimensions[ $url ] = array(
'width' => $cached_dimensions[0],
'height' => $cached_dimensions[1],
);
continue;
}
// If the value in the transient reflects we couldn't get dimensions for this image the last time we tried, move on.
if ( self::STATUS_FAILED_LAST_ATTEMPT === $cached_dimensions ) {
$dimensions[ $url ] = false;
continue;
}
$transient_lock_name = sprintf( 'amp_lock_%s', $url_hash );
// If somebody is already trying to extract dimensions for this transient right now, move on.
if ( false !== get_transient( $transient_lock_name ) ) {
$dimensions[ $url ] = false;
continue;
}
// Include the image as a url to fetch.
$urls_to_fetch[ $url ] = array();
$urls_to_fetch[ $url ]['url'] = $url;
$urls_to_fetch[ $url ]['transient_name'] = $transient_name;
$urls_to_fetch[ $url ]['transient_lock_name'] = $transient_lock_name;
set_transient( $transient_lock_name, 1, MINUTE_IN_SECONDS );
}
}
/**
* Fetch dimensions of remote images
*
* @throws Exception When cURL handle cannot be added.
*
* @param array $urls_to_fetch Image src urls to fetch.
* @param array $images Array to populate with results of image/dimension inspection.
*/
private static function fetch_images( $urls_to_fetch, &$images ) {
$urls = array_keys( $urls_to_fetch );
$client = new \FasterImage\FasterImage();
/**
* Filters the user agent for onbtaining the image dimensions.
*
* @param string $user_agent User agent.
*/
$client->setUserAgent( apply_filters( 'amp_extract_image_dimensions_get_user_agent', self::get_default_user_agent() ) );
$client->setBufferSize( 1024 );
$client->setSslVerifyHost( true );
$client->setSslVerifyPeer( true );
$images = $client->batch( $urls );
}
/**
* Determine success or failure of remote fetch, integrate fetched dimensions into url to dimension mapping,
* cache fetched dimensions via transient and release/delete semaphore transient
*
* @param array $urls_to_fetch List of image urls that were fetched and transient names corresponding to each (for unlocking semaphore, setting "real" transient).
* @param array $images Results of remote fetch mapping fetched image url to dimensions.
* @param array $dimensions Map of image url to dimensions to be updated with results of remote fetch.
* @param int $transient_expiration Duration image dimensions should exist in transient/cache.
*/
private static function process_fetched_images( $urls_to_fetch, $images, &$dimensions, $transient_expiration ) {
foreach ( $urls_to_fetch as $url_data ) {
$image_data = $images[ $url_data['url'] ];
if ( self::STATUS_IMAGE_EXTRACTION_FAILED === $image_data['size'] ) {
$dimensions[ $url_data['url'] ] = false;
set_transient( $url_data['transient_name'], self::STATUS_FAILED_LAST_ATTEMPT, $transient_expiration );
} else {
$dimensions[ $url_data['url'] ] = array(
'width' => $image_data['size'][0],
'height' => $image_data['size'][1],
);
set_transient(
$url_data['transient_name'],
array(
$image_data['size'][0],
$image_data['size'][1],
),
$transient_expiration
);
}
delete_transient( $url_data['transient_lock_name'] );
}
}
/**
* Get default user agent
*
* @return string
*/
public static function get_default_user_agent() {
return 'amp-wp, v' . AMP__VERSION . ', ' . home_url();
}
}

View File

@ -0,0 +1,25 @@
<?php
/**
* Class AMP_String_Utils
*
* @package AMP
*/
/**
* Class with static string utility methods.
*/
class AMP_String_Utils {
/**
* Checks whether a given string ends in the given substring.
*
* @param string $haystack Input string.
* @param string $needle Substring to look for at the end of $haystack.
* @return bool True if $haystack ends in $needle, false otherwise.
*/
public static function endswith( $haystack, $needle ) {
return '' !== $haystack
&& '' !== $needle
&& substr( $haystack, -strlen( $needle ) ) === $needle;
}
}

View File

@ -0,0 +1,102 @@
<?php
/**
* Class AMP_WP_Utils
*
* @package AMP
*/
/**
* Class with static WordPress utility methods.
*
* @since 0.5
*
* @deprecated 0.7 As WordPress 4.7 is our minimum supported version.
*/
class AMP_WP_Utils {
/**
* The core function wp_parse_url in < WordPress 4.7 does not respect the component arg. This helper lets us use it.
*
* Don't use.
*
* @deprecated 0.7 wp_parse_url() is now used instead.
*
* @param string $url The raw URL. Can be false if the URL failed to parse.
* @param int $component The specific component to retrieve. Use one of the PHP
* predefined constants to specify which one.
* Defaults to -1 (= return all parts as an array).
* @return mixed False on parse failure; Array of URL components on success;
* When a specific component has been requested: null if the component
* doesn't exist in the given URL; a string or - in the case of
* PHP_URL_PORT - integer when it does. See parse_url()'s return values.
*/
public static function parse_url( $url, $component = -1 ) {
_deprecated_function( __METHOD__, '0.7', 'wp_parse_url' );
$parsed = wp_parse_url( $url, $component );
// Because < 4.7 always returned a full array regardless of component.
if ( -1 !== $component && is_array( $parsed ) ) {
return self::_get_component_from_parsed_url_array( $parsed, $component );
}
return $parsed;
}
/**
* Included for 4.6 back-compat
*
* Copied from https://developer.wordpress.org/reference/functions/_get_component_from_parsed_url_array/
*
* @deprecated 0.7
*
* @param array|false $url_parts The parsed URL. Can be false if the URL failed to parse.
* @param int $component The specific component to retrieve. Use one of the PHP
* predefined constants to specify which one.
* Defaults to -1 (= return all parts as an array).
* @return mixed False on parse failure; Array of URL components on success;
* When a specific component has been requested: null if the component
* doesn't exist in the given URL; a string or - in the case of
* PHP_URL_PORT - integer when it does. See parse_url()'s return values.
*/
protected static function _get_component_from_parsed_url_array( $url_parts, $component = -1 ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
if ( -1 === $component ) {
return $url_parts;
}
$key = self::_wp_translate_php_url_constant_to_key( $component );
if ( false !== $key && is_array( $url_parts ) && isset( $url_parts[ $key ] ) ) {
return $url_parts[ $key ];
}
return null;
}
/**
* Included for 4.6 back-compat
*
* Copied from https://developer.wordpress.org/reference/functions/_wp_translate_php_url_constant_to_key/
*
* @param int $constant The specific component to retrieve. Use one of the PHP
* predefined constants to specify which one.
* @return mixed False if component not found. string or integer if found.
*
* @deprecated 0.7
*/
protected static function _wp_translate_php_url_constant_to_key( $constant ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
$translation = array(
PHP_URL_SCHEME => 'scheme',
PHP_URL_HOST => 'host',
PHP_URL_PORT => 'port',
PHP_URL_USER => 'user',
PHP_URL_PASS => 'pass',
PHP_URL_PATH => 'path',
PHP_URL_QUERY => 'query',
PHP_URL_FRAGMENT => 'fragment',
);
if ( isset( $translation[ $constant ] ) ) {
return $translation[ $constant ];
}
return false;
}
}

View File

@ -0,0 +1,111 @@
<?php
/**
* Class AMP_Widget_Archives
*
* @since 0.7.0
* @package AMP
*/
/**
* Class AMP_Widget_Archives
*
* @since 0.7.0
* @package AMP
*/
class AMP_Widget_Archives extends WP_Widget_Archives {
/**
* Echoes the markup of the widget.
*
* Mainly copied from WP_Widget_Archives::widget()
* Changes include:
* An id for the <form>.
* More escaping.
* The dropdown is now filtered with 'wp_dropdown_cats.'
* This enables adding an 'on' attribute, with the id of the form.
* So changing the dropdown value will redirect to the category page, with valid AMP.
*
* @since 0.7.0
*
* @param array $args Widget display data.
* @param array $instance Data for widget.
* @return void.
*/
public function widget( $args, $instance ) {
if ( ! is_amp_endpoint() ) {
parent::widget( $args, $instance );
return;
}
$c = ! empty( $instance['count'] ) ? '1' : '0';
$d = ! empty( $instance['dropdown'] ) ? '1' : '0';
/** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */
$title = apply_filters( 'widget_title', empty( $instance['title'] ) ? __( 'Archives', 'default' ) : $instance['title'], $instance, $this->id_base );
echo wp_kses_post( $args['before_widget'] );
if ( $title ) :
echo wp_kses_post( $args['before_title'] . $title . $args['after_title'] );
endif;
if ( $d ) :
$dropdown_id = "{$this->id_base}-dropdown-{$this->number}";
?>
<form action="<?php echo esc_url( home_url() ); ?>" method="get" target="_top">
<label class="screen-reader-text" for="<?php echo esc_attr( $dropdown_id ); ?>"><?php echo esc_html( $title ); ?></label>
<select id="<?php echo esc_attr( $dropdown_id ); ?>" name="archive-dropdown" on="change:AMP.navigateTo(url=event.value)">
<?php
/** This filter is documented in wp-includes/widgets/class-wp-widget-archives.php */
$dropdown_args = apply_filters(
'widget_archives_dropdown_args',
array(
'type' => 'monthly',
'format' => 'option',
'show_post_count' => $c,
)
);
switch ( $dropdown_args['type'] ) {
case 'yearly':
$label = __( 'Select Year', 'default' );
break;
case 'monthly':
$label = __( 'Select Month', 'default' );
break;
case 'daily':
$label = __( 'Select Day', 'default' );
break;
case 'weekly':
$label = __( 'Select Week', 'default' );
break;
default:
$label = __( 'Select Post', 'default' );
break;
}
?>
<option value=""><?php echo esc_attr( $label ); ?></option>
<?php wp_get_archives( $dropdown_args ); ?>
</select>
</form>
<?php else : ?>
<ul>
<?php
/** This filter is documented in wp-includes/widgets/class-wp-widget-archives.php */
wp_get_archives(
apply_filters(
'widget_archives_args',
array(
'type' => 'monthly',
'show_post_count' => $c,
)
)
);
?>
</ul>
<?php
endif;
echo wp_kses_post( $args['after_widget'] );
}
}

View File

@ -0,0 +1,93 @@
<?php
/**
* Class AMP_Widget_Categories
*
* @since 0.7.0
* @package AMP
*/
/**
* Class AMP_Widget_Categories
*
* @since 0.7.0
* @package AMP
*/
class AMP_Widget_Categories extends WP_Widget_Categories {
/**
* Echoes the markup of the widget.
*
* Mainly copied from WP_Widget_Categories::widget()
* There's now an id for the <form>.
* And the dropdown is now filtered with 'wp_dropdown_cats.'
* This enables adding an 'on' attribute, with the id of the form.
* So changing the dropdown value will redirect to the category page, with valid AMP.
*
* @since 0.7.0
*
* @param array $args Widget display data.
* @param array $instance Data for widget.
* @return void
*/
public function widget( $args, $instance ) {
if ( ! is_amp_endpoint() ) {
parent::widget( $args, $instance );
return;
}
static $first_dropdown = true;
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Categories', 'default' );
/** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */
$title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
$c = ! empty( $instance['count'] ) ? '1' : '0';
$h = ! empty( $instance['hierarchical'] ) ? '1' : '0';
$d = ! empty( $instance['dropdown'] ) ? '1' : '0';
echo wp_kses_post( $args['before_widget'] );
if ( $title ) {
echo wp_kses_post( $args['before_title'] . $title . $args['after_title'] );
}
$cat_args = array(
'orderby' => 'name',
'show_count' => $c,
'hierarchical' => $h,
);
if ( $d ) :
$form_id = sprintf( 'widget-categories-dropdown-%d', $this->number );
printf( '<form action="%s" method="get" target="_top" id="%s">', esc_url( home_url() ), esc_attr( $form_id ) );
$dropdown_id = ( $first_dropdown ) ? 'cat' : "{$this->id_base}-dropdown-{$this->number}";
$first_dropdown = false;
echo '<label class="screen-reader-text" for="' . esc_attr( $dropdown_id ) . '">' . esc_html( $title ) . '</label>';
$cat_args['show_option_none'] = __( 'Select Category', 'default' );
$cat_args['id'] = $dropdown_id;
$dropdown = wp_dropdown_categories(
array_merge(
/** This filter is documented in wp-includes/widgets/class-wp-widget-categories.php */
apply_filters( 'widget_categories_dropdown_args', $cat_args, $instance ),
array( 'echo' => false )
)
);
$dropdown = preg_replace(
'/(?<=<select\b)/',
sprintf( '<select on="change:%s.submit"', esc_attr( $form_id ) ),
$dropdown,
1
);
echo $dropdown; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</form>';
else :
?>
<ul>
<?php
$cat_args['title_li'] = '';
/** This filter is documented in wp-includes/widgets/class-wp-widget-categories.php */
wp_list_categories( apply_filters( 'widget_categories_args', $cat_args, $instance ) );
?>
</ul>
<?php
endif;
echo wp_kses_post( $args['after_widget'] );
}
}

View File

@ -0,0 +1,33 @@
<?php
/**
* Class AMP_Widget_Text
*
* @since 0.7.0
* @package AMP
*/
if ( class_exists( 'WP_Widget_Text' ) ) {
/**
* Class AMP_Widget_Text
*
* @since 0.7.0
* @package AMP
*/
class AMP_Widget_Text extends WP_Widget_Text {
/**
* Overrides the parent callback that strips width and height attributes.
*
* @param array $matches The matches returned from preg_replace_callback().
* @return string $html The markup, unaltered.
*/
public function inject_video_max_width_style( $matches ) {
if ( is_amp_endpoint() ) {
return $matches[0];
}
return parent::inject_video_max_width_style( $matches );
}
}
}