AMP for WordPress - Version 0.3.3

Version Description

(Aug 18, 2016) =

  • Handle many more validation errors (props bcampeau and alleyinteractive).
  • New filter: amp_post_template_dir (props mustafauysal).
  • New template: Nav bar is now it's own template part (props jdevalk).
  • Better ratio for YouTube embeds.
  • Fix: better timezone handling (props rinatkhaziev).
  • Fix: better handling of non-int dimensions (like 100%).
  • Fix: better handling of empty dimensions.
  • Fix: autoplay is a bool-like value.
  • Fix: breakage when using the query_string hook (h/t mkuplens).
  • Fix: don't break really large Twitter IDs.
  • Fix: don't break Instagram shortcodes when using URLs with querystrings.
  • Readme improvements (props nickjohnford, sotayamashita)
Download this release

Release Info

Developer batmoo
Plugin Icon 128x128 AMP for WordPress
Version 0.3.3
Comparing to
See all releases

Code changes from version 0.3.2 to 0.3.3

amp.php CHANGED
@@ -5,7 +5,7 @@
5
  * Plugin URI: https://github.com/automattic/amp-wp
6
  * Author: Automattic
7
  * Author URI: https://automattic.com
8
- * Version: 0.3.2
9
  * Text Domain: amp
10
  * Domain Path: /languages/
11
  * License: GPLv2 or later
@@ -17,13 +17,13 @@ define( 'AMP__DIR__', dirname( __FILE__ ) );
17
  require_once( AMP__DIR__ . '/includes/amp-helper-functions.php' );
18
 
19
  register_activation_hook( __FILE__, 'amp_activate' );
20
- function amp_activate(){
21
  amp_init();
22
  flush_rewrite_rules();
23
  }
24
 
25
  register_deactivation_hook( __FILE__, 'amp_deactivate' );
26
- function amp_deactivate(){
27
  flush_rewrite_rules();
28
  }
29
 
@@ -42,6 +42,7 @@ function amp_init() {
42
  add_rewrite_endpoint( AMP_QUERY_VAR, EP_PERMALINK );
43
  add_post_type_support( 'post', AMP_QUERY_VAR );
44
 
 
45
  add_action( 'wp', 'amp_maybe_add_actions' );
46
 
47
  if ( class_exists( 'Jetpack' ) && ! ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
@@ -49,6 +50,15 @@ function amp_init() {
49
  }
50
  }
51
 
 
 
 
 
 
 
 
 
 
52
  function amp_maybe_add_actions() {
53
  if ( ! is_singular() || is_feed() ) {
54
  return;
5
  * Plugin URI: https://github.com/automattic/amp-wp
6
  * Author: Automattic
7
  * Author URI: https://automattic.com
8
+ * Version: 0.3.3
9
  * Text Domain: amp
10
  * Domain Path: /languages/
11
  * License: GPLv2 or later
17
  require_once( AMP__DIR__ . '/includes/amp-helper-functions.php' );
18
 
19
  register_activation_hook( __FILE__, 'amp_activate' );
20
+ function amp_activate() {
21
  amp_init();
22
  flush_rewrite_rules();
23
  }
24
 
25
  register_deactivation_hook( __FILE__, 'amp_deactivate' );
26
+ function amp_deactivate() {
27
  flush_rewrite_rules();
28
  }
29
 
42
  add_rewrite_endpoint( AMP_QUERY_VAR, EP_PERMALINK );
43
  add_post_type_support( 'post', AMP_QUERY_VAR );
44
 
45
+ add_filter( 'request', 'amp_force_query_var_value' );
46
  add_action( 'wp', 'amp_maybe_add_actions' );
47
 
48
  if ( class_exists( 'Jetpack' ) && ! ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
50
  }
51
  }
52
 
53
+ // Make sure the `amp` query var has an explicit value.
54
+ // Avoids issues when filtering the deprecated `query_string` hook.
55
+ function amp_force_query_var_value( $query_vars ) {
56
+ if ( isset( $query_vars[ AMP_QUERY_VAR ] ) && '' === $query_vars[ AMP_QUERY_VAR ] ) {
57
+ $query_vars[ AMP_QUERY_VAR ] = 1;
58
+ }
59
+ return $query_vars;
60
+ }
61
+
62
  function amp_maybe_add_actions() {
63
  if ( ! is_singular() || is_feed() ) {
64
  return;
includes/class-amp-post-template.php CHANGED
@@ -2,6 +2,7 @@
2
 
3
  require_once( AMP__DIR__ . '/includes/utils/class-amp-dom-utils.php' );
4
  require_once( AMP__DIR__ . '/includes/utils/class-amp-html-utils.php' );
 
5
 
6
  require_once( AMP__DIR__ . '/includes/class-amp-content.php' );
7
 
@@ -26,7 +27,7 @@ class AMP_Post_Template {
26
  private $data;
27
 
28
  public function __construct( $post_id ) {
29
- $this->template_dir = AMP__DIR__ . '/templates';
30
 
31
  $this->ID = $post_id;
32
  $this->post = get_post( $post_id );
2
 
3
  require_once( AMP__DIR__ . '/includes/utils/class-amp-dom-utils.php' );
4
  require_once( AMP__DIR__ . '/includes/utils/class-amp-html-utils.php' );
5
+ require_once( AMP__DIR__ . '/includes/utils/class-amp-string-utils.php' );
6
 
7
  require_once( AMP__DIR__ . '/includes/class-amp-content.php' );
8
 
27
  private $data;
28
 
29
  public function __construct( $post_id ) {
30
+ $this->template_dir = apply_filters( 'amp_post_template_dir', AMP__DIR__ . '/templates' );
31
 
32
  $this->ID = $post_id;
33
  $this->post = get_post( $post_id );
includes/embeds/class-amp-instagram-embed.php CHANGED
@@ -79,15 +79,12 @@ class AMP_Instagram_Embed_Handler extends AMP_Base_Embed_Handler {
79
  }
80
 
81
  private function get_instagram_id_from_url( $url ) {
82
- $url_path = parse_url( $url, PHP_URL_PATH );
83
 
84
- // /p/{id} on both, short url and normal urls
85
- $instagram_id = mb_substr( $url_path, 3 );
86
-
87
- if( ! empty( $instagram_id ) ) {
88
- return $instagram_id;
89
  }
90
 
91
- return false;
92
  }
93
  }
79
  }
80
 
81
  private function get_instagram_id_from_url( $url ) {
82
+ $found = preg_match( self::URL_PATTERN, $url, $matches );
83
 
84
+ if ( ! $found ) {
85
+ return false;
 
 
 
86
  }
87
 
88
+ return end( $matches );
89
  }
90
  }
includes/embeds/class-amp-twitter-embed.php CHANGED
@@ -32,13 +32,17 @@ class AMP_Twitter_Embed_Handler extends AMP_Base_Embed_Handler {
32
  'tweet' => false,
33
  ) );
34
 
 
 
 
 
35
  $id = false;
36
- if ( intval( $attr['tweet'] ) ) {
37
- $id = intval( $attr['tweet'] );
38
  } else {
39
  preg_match( self::URL_PATTERN, $attr['tweet'], $matches );
40
- if ( isset( $matches[5] ) && intval( $matches[5] ) ) {
41
- $id = intval( $matches[5] );
42
  }
43
 
44
  if ( empty( $id ) ) {
@@ -62,8 +66,8 @@ class AMP_Twitter_Embed_Handler extends AMP_Base_Embed_Handler {
62
  function oembed( $matches, $attr, $url, $rawattr ) {
63
  $id = false;
64
 
65
- if ( isset( $matches[5] ) && intval( $matches[5] ) ) {
66
- $id = intval( $matches[5] );
67
  }
68
 
69
  if ( ! $id ) {
32
  'tweet' => false,
33
  ) );
34
 
35
+ if ( empty( $attr['tweet'] ) && ! empty( $attr[0] ) ) {
36
+ $attr['tweet'] = $attr[0];
37
+ }
38
+
39
  $id = false;
40
+ if ( is_numeric( $attr['tweet'] ) ) {
41
+ $id = $attr['tweet'];
42
  } else {
43
  preg_match( self::URL_PATTERN, $attr['tweet'], $matches );
44
+ if ( isset( $matches[5] ) && is_numeric( $matches[5] ) ) {
45
+ $id = $matches[5];
46
  }
47
 
48
  if ( empty( $id ) ) {
66
  function oembed( $matches, $attr, $url, $rawattr ) {
67
  $id = false;
68
 
69
+ if ( isset( $matches[5] ) && is_numeric( $matches[5] ) ) {
70
+ $id = $matches[5];
71
  }
72
 
73
  if ( ! $id ) {
includes/embeds/class-amp-youtube-embed.php CHANGED
@@ -6,10 +6,24 @@ require_once( AMP__DIR__ . '/includes/embeds/class-amp-base-embed-handler.php' )
6
  class AMP_YouTube_Embed_Handler extends AMP_Base_Embed_Handler {
7
  const SHORT_URL_HOST = 'youtu.be';
8
  const URL_PATTERN = '#https?://(?:www\.)?(?:youtube.com/(?:v/|e/|embed/|playlist|watch[/\#?])|youtu\.be/).*#i';
 
 
 
 
9
 
10
  private static $script_slug = 'amp-youtube';
11
  private static $script_src = 'https://cdn.ampproject.org/v0/amp-youtube-0.1.js';
12
 
 
 
 
 
 
 
 
 
 
 
13
  function register_embed() {
14
  wp_embed_register_handler( 'amp-youtube', self::URL_PATTERN, array( $this, 'oembed' ), -1 );
15
  add_shortcode( 'youtube', array( $this, 'shortcode' ) );
6
  class AMP_YouTube_Embed_Handler extends AMP_Base_Embed_Handler {
7
  const SHORT_URL_HOST = 'youtu.be';
8
  const URL_PATTERN = '#https?://(?:www\.)?(?:youtube.com/(?:v/|e/|embed/|playlist|watch[/\#?])|youtu\.be/).*#i';
9
+ const RATIO = 0.5625;
10
+
11
+ protected $DEFAULT_WIDTH = 600;
12
+ protected $DEFAULT_HEIGHT = 338;
13
 
14
  private static $script_slug = 'amp-youtube';
15
  private static $script_src = 'https://cdn.ampproject.org/v0/amp-youtube-0.1.js';
16
 
17
+ function __construct( $args = array() ) {
18
+ parent::__construct( $args );
19
+
20
+ if ( isset( $this->args['content_max_width'] ) ) {
21
+ $max_width = $this->args['content_max_width'];
22
+ $this->args['width'] = $max_width;
23
+ $this->args['height'] = round( $max_width * self::RATIO );
24
+ }
25
+ }
26
+
27
  function register_embed() {
28
  wp_embed_register_handler( 'amp-youtube', self::URL_PATTERN, array( $this, 'oembed' ), -1 );
29
  add_shortcode( 'youtube', array( $this, 'shortcode' ) );
includes/sanitizers/class-amp-audio-sanitizer.php CHANGED
@@ -31,14 +31,28 @@ class AMP_Audio_Sanitizer extends AMP_Base_Sanitizer {
31
 
32
  $new_node = AMP_DOM_Utils::create_node( $this->dom, 'amp-audio', $new_attributes );
33
 
34
- // TODO: limit child nodes too (only allowed: `source`; move rest to div+fallback)
35
  // TODO: `source` does not have closing tag, and DOMDocument doesn't handle it well.
36
  foreach ( $node->childNodes as $child_node ) {
37
  $new_child_node = $child_node->cloneNode( true );
38
- $new_node->appendChild( $new_child_node );
 
 
 
 
 
 
39
  }
40
 
41
- $node->parentNode->replaceChild( $new_node, $node );
 
 
 
 
 
 
 
 
 
42
 
43
  $this->did_convert_elements = true;
44
  }
@@ -50,20 +64,25 @@ class AMP_Audio_Sanitizer extends AMP_Base_Sanitizer {
50
  foreach ( $attributes as $name => $value ) {
51
  switch ( $name ) {
52
  case 'src':
 
 
 
53
  case 'width':
54
  case 'height':
 
 
 
55
  case 'class':
56
  $out[ $name ] = $value;
57
  break;
58
  case 'loop':
59
  case 'muted':
 
60
  if ( 'false' !== $value ) {
61
  $out[ $name ] = '';
62
  }
63
  break;
64
- case 'autoplay':
65
- $out[ $name ] = 'desktop tablet mobile';
66
- break;
67
  default;
68
  break;
69
  }
31
 
32
  $new_node = AMP_DOM_Utils::create_node( $this->dom, 'amp-audio', $new_attributes );
33
 
 
34
  // TODO: `source` does not have closing tag, and DOMDocument doesn't handle it well.
35
  foreach ( $node->childNodes as $child_node ) {
36
  $new_child_node = $child_node->cloneNode( true );
37
+ $old_child_attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $new_child_node );
38
+ $new_child_attributes = $this->filter_attributes( $old_child_attributes );
39
+
40
+ // Only append source tags with a valid src attribute
41
+ if ( ! empty( $new_child_attributes['src'] ) && 'source' === $new_child_node->tagName ) {
42
+ $new_node->appendChild( $new_child_node );
43
+ }
44
  }
45
 
46
+ // If the node has at least one valid source, replace the old node with it.
47
+ // Otherwise, just remove the node.
48
+ //
49
+ // TODO: Add a fallback handler.
50
+ // See: https://github.com/ampproject/amphtml/issues/2261
51
+ if ( 0 === $new_node->childNodes->length && empty( $new_attributes['src'] ) ) {
52
+ $node->parentNode->removeChild( $node );
53
+ } else {
54
+ $node->parentNode->replaceChild( $new_node, $node );
55
+ }
56
 
57
  $this->did_convert_elements = true;
58
  }
64
  foreach ( $attributes as $name => $value ) {
65
  switch ( $name ) {
66
  case 'src':
67
+ $out[ $name ] = $this->maybe_enforce_https_src( $value );
68
+ break;
69
+
70
  case 'width':
71
  case 'height':
72
+ $out[ $name ] = $this->sanitize_dimension( $value, $name );
73
+ break;
74
+
75
  case 'class':
76
  $out[ $name ] = $value;
77
  break;
78
  case 'loop':
79
  case 'muted':
80
+ case 'autoplay':
81
  if ( 'false' !== $value ) {
82
  $out[ $name ] = '';
83
  }
84
  break;
85
+
 
 
86
  default;
87
  break;
88
  }
includes/sanitizers/class-amp-base-sanitizer.php CHANGED
@@ -1,6 +1,8 @@
1
  <?php
2
 
3
  abstract class AMP_Base_Sanitizer {
 
 
4
  protected $DEFAULT_ARGS = array();
5
 
6
  protected $dom;
@@ -22,6 +24,42 @@ abstract class AMP_Base_Sanitizer {
22
  return $this->dom->getElementsByTagName( 'body' )->item( 0 );
23
  }
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  /**
26
  * This is our workaround to enforce max sizing with layout=responsive.
27
  *
@@ -54,4 +92,29 @@ abstract class AMP_Base_Sanitizer {
54
  $attributes[ $key ] = $value;
55
  }
56
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  }
1
  <?php
2
 
3
  abstract class AMP_Base_Sanitizer {
4
+ const FALLBACK_HEIGHT = 400;
5
+
6
  protected $DEFAULT_ARGS = array();
7
 
8
  protected $dom;
24
  return $this->dom->getElementsByTagName( 'body' )->item( 0 );
25
  }
26
 
27
+ public function sanitize_dimension( $value, $dimension ) {
28
+ if ( empty( $value ) ) {
29
+ return $value;
30
+ }
31
+
32
+ if ( false !== filter_var( $value, FILTER_VALIDATE_INT ) ) {
33
+ return absint( $value );
34
+ }
35
+
36
+ if ( AMP_String_Utils::endswith( $value, 'px' ) ) {
37
+ return absint( $value );
38
+ }
39
+
40
+ if ( AMP_String_Utils::endswith( $value, '%' ) ) {
41
+ if ( 'width' === $dimension && isset( $this->args[ 'content_max_width'] ) ) {
42
+ $percentage = absint( $value ) / 100;
43
+ return round( $percentage * $this->args[ 'content_max_width'] );
44
+ }
45
+ }
46
+
47
+ return '';
48
+ }
49
+
50
+ public function enforce_fixed_height( $attributes ) {
51
+ if ( empty( $attributes['height'] ) ) {
52
+ unset( $attributes['width'] );
53
+ $attributes['height'] = self::FALLBACK_HEIGHT;
54
+ }
55
+
56
+ if ( empty( $attributes['width'] ) ) {
57
+ $attributes['layout'] = 'fixed-height';
58
+ }
59
+
60
+ return $attributes;
61
+ }
62
+
63
  /**
64
  * This is our workaround to enforce max sizing with layout=responsive.
65
  *
92
  $attributes[ $key ] = $value;
93
  }
94
  }
95
+
96
+ /**
97
+ * Decide if we should remove a src attribute if https is required.
98
+ * If not required, the implementing class may want to try and force https instead.
99
+ *
100
+ * @param string $src
101
+ * @param boolean $force_https
102
+ * @return string
103
+ */
104
+ public function maybe_enforce_https_src( $src, $force_https = false ) {
105
+ $protocol = strtok( $src, ':' );
106
+ if ( 'https' !== $protocol ) {
107
+ // Check if https is required
108
+ if ( isset( $this->args['require_https_src'] ) && true === $this->args['require_https_src'] ) {
109
+ // Remove the src. Let the implementing class decide what do from here.
110
+ $src = '';
111
+ } elseif ( ( ! isset( $this->args['require_https_src'] ) || false === $this->args['require_https_src'] )
112
+ && true === $force_https ) {
113
+ // Don't remove the src, but force https instead
114
+ $src = set_url_scheme( $src, 'https' );
115
+ }
116
+ }
117
+
118
+ return $src;
119
+ }
120
  }
includes/sanitizers/class-amp-blacklist-sanitizer.php CHANGED
@@ -11,6 +11,12 @@ require_once( AMP__DIR__ . '/includes/sanitizers/class-amp-base-sanitizer.php' )
11
  class AMP_Blacklist_Sanitizer extends AMP_Base_Sanitizer {
12
  const PATTERN_REL_WP_ATTACHMENT = '#wp-att-([\d]+)#';
13
 
 
 
 
 
 
 
14
  public function sanitize() {
15
  $blacklisted_tags = $this->get_blacklisted_tags();
16
  $blacklisted_attributes = $this->get_blacklisted_attributes();
@@ -26,8 +32,17 @@ class AMP_Blacklist_Sanitizer extends AMP_Base_Sanitizer {
26
  return;
27
  }
28
 
 
 
 
 
 
 
 
 
 
 
29
  if ( $node->hasAttributes() ) {
30
- $node_name = $node->nodeName;
31
  $length = $node->attributes->length;
32
  for ( $i = $length - 1; $i >= 0; $i-- ) {
33
  $attribute = $node->attributes->item( $i );
@@ -41,19 +56,16 @@ class AMP_Blacklist_Sanitizer extends AMP_Base_Sanitizer {
41
  if ( 0 === stripos( $attribute_name, 'on' ) && $attribute_name != 'on' ) {
42
  $node->removeAttribute( $attribute_name );
43
  continue;
44
- } elseif ( 'href' === $attribute_name ) {
45
- $protocol = strtok( $attribute->value, ':' );
46
- if ( in_array( $protocol, $bad_protocols ) ) {
47
- $node->removeAttribute( $attribute_name );
48
- continue;
49
- }
50
  } elseif ( 'a' === $node_name ) {
51
  $this->sanitize_a_attribute( $node, $attribute );
52
  }
53
  }
54
  }
55
 
56
- foreach ( $node->childNodes as $child_node ) {
 
 
 
57
  $this->strip_attributes_recursive( $child_node, $bad_attributes, $bad_protocols );
58
  }
59
  }
@@ -93,24 +105,88 @@ class AMP_Blacklist_Sanitizer extends AMP_Base_Sanitizer {
93
  // rev removed from HTML5 spec, which was used by Jetpack Markdown.
94
  $node->removeAttribute( $attribute_name );
95
  } elseif ( 'target' === $attribute_name ) {
96
- if ( '_blank' === $attribute->value || '_new' === $attribute->value ) {
97
- // _new is not allowed; swap with _blank
98
- $node->setAttribute( $attribute_name, '_blank' );
99
- } else {
100
- // only _blank is allowed
101
- $node->removeAttribute( $attribute_name );
102
- }
 
 
 
103
  }
104
  }
105
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  private function get_blacklisted_protocols() {
107
- return array(
108
  'javascript',
109
- );
110
  }
111
 
112
  private function get_blacklisted_tags() {
113
- return array(
114
  'script',
115
  'noscript',
116
  'style',
@@ -126,18 +202,24 @@ class AMP_Blacklist_Sanitizer extends AMP_Base_Sanitizer {
126
  'select',
127
  'option',
128
  'link',
 
 
 
 
 
129
 
130
  // These are converted into amp-* versions
131
  //'img',
132
  //'video',
133
  //'audio',
134
  //'iframe',
135
- );
136
  }
137
 
138
  private function get_blacklisted_attributes() {
139
- return array(
140
  'style',
141
- );
 
142
  }
143
  }
11
  class AMP_Blacklist_Sanitizer extends AMP_Base_Sanitizer {
12
  const PATTERN_REL_WP_ATTACHMENT = '#wp-att-([\d]+)#';
13
 
14
+ protected $DEFAULT_ARGS = array(
15
+ 'add_blacklisted_protocols' => array(),
16
+ 'add_blacklisted_tags' => array(),
17
+ 'add_blacklisted_attributes' => array(),
18
+ );
19
+
20
  public function sanitize() {
21
  $blacklisted_tags = $this->get_blacklisted_tags();
22
  $blacklisted_attributes = $this->get_blacklisted_attributes();
32
  return;
33
  }
34
 
35
+ $node_name = $node->nodeName;
36
+
37
+ // Some nodes may contain valid content but are themselves invalid.
38
+ // Remove the node but preserve the children.
39
+ if ( 'font' === $node_name ) {
40
+ $this->replace_node_with_children( $node );
41
+ } elseif ( 'a' === $node_name && false === $this->validate_a_node( $node ) ) {
42
+ $this->replace_node_with_children( $node );
43
+ }
44
+
45
  if ( $node->hasAttributes() ) {
 
46
  $length = $node->attributes->length;
47
  for ( $i = $length - 1; $i >= 0; $i-- ) {
48
  $attribute = $node->attributes->item( $i );
56
  if ( 0 === stripos( $attribute_name, 'on' ) && $attribute_name != 'on' ) {
57
  $node->removeAttribute( $attribute_name );
58
  continue;
 
 
 
 
 
 
59
  } elseif ( 'a' === $node_name ) {
60
  $this->sanitize_a_attribute( $node, $attribute );
61
  }
62
  }
63
  }
64
 
65
+ $length = $node->childNodes->length;
66
+ for ( $i = $length - 1; $i >= 0; $i-- ) {
67
+ $child_node = $node->childNodes->item( $i );
68
+
69
  $this->strip_attributes_recursive( $child_node, $bad_attributes, $bad_protocols );
70
  }
71
  }
105
  // rev removed from HTML5 spec, which was used by Jetpack Markdown.
106
  $node->removeAttribute( $attribute_name );
107
  } elseif ( 'target' === $attribute_name ) {
108
+ // _blank is the only allowed value and it must be lowercase.
109
+ // replace _new with _blank and others should simply be removed.
110
+ $old_value = strtolower( $attribute->value );
111
+ if ( '_blank' === $old_value || '_new' === $old_value ) {
112
+ // _new is not allowed; swap with _blank
113
+ $node->setAttribute( $attribute_name, '_blank' );
114
+ } else {
115
+ // only _blank is allowed
116
+ $node->removeAttribute( $attribute_name );
117
+ }
118
  }
119
  }
120
 
121
+ private function validate_a_node( $node ) {
122
+ // Get the href attribute
123
+ $href = $node->getAttribute( 'href' );
124
+
125
+ // If no href is set and this isn't an anchor, it's invalid
126
+ if ( empty( $href ) ) {
127
+ $name_attr = $node->getAttribute( 'name' );
128
+ if ( ! empty( $name_attr ) ) {
129
+ // No further validation is required
130
+ return true;
131
+ } else {
132
+ return false;
133
+ }
134
+ }
135
+
136
+ // If this is an anchor link, just return true
137
+ if ( 0 === strpos( $href, '#' ) ) {
138
+ return true;
139
+ }
140
+
141
+ // If the href starts with a '/', append the home_url to it for validation purposes.
142
+ if ( 0 === stripos( $href, '/' ) ) {
143
+ $href = untrailingslashit( get_home_url() ) . $href;
144
+ }
145
+
146
+ $valid_protocols = array( 'http', 'https', 'mailto', 'sms', 'tel', 'viber', 'whatsapp' );
147
+ $protocol = strtok( $href, ':' );
148
+ if ( false === filter_var( $href, FILTER_VALIDATE_URL )
149
+ || ! in_array( $protocol, $valid_protocols ) ) {
150
+ return false;
151
+ }
152
+
153
+ return true;
154
+ }
155
+
156
+ private function replace_node_with_children( $node ) {
157
+ // If the node has children and also has a parent node,
158
+ // clone and re-add all the children just before current node.
159
+ if ( $node->hasChildNodes() && $node->parentNode ) {
160
+ foreach ( $node->childNodes as $child_node ) {
161
+ $new_child = $child_node->cloneNode( true );
162
+ $node->parentNode->insertBefore( $new_child, $node );
163
+ }
164
+ }
165
+
166
+ // Remove the node from the parent, if defined.
167
+ if ( $node->parentNode ) {
168
+ $node->parentNode->removeChild( $node );
169
+ }
170
+ }
171
+
172
+ private function merge_defaults_with_args( $key, $values ) {
173
+ // Merge default values with user specified args
174
+ if ( ! empty( $this->args[ $key ] )
175
+ && is_array( $this->args[ $key ] ) ) {
176
+ $values = array_merge( $values, $this->args[ $key ] );
177
+ }
178
+
179
+ return $values;
180
+ }
181
+
182
  private function get_blacklisted_protocols() {
183
+ return $this->merge_defaults_with_args( 'add_blacklisted_protocols', array(
184
  'javascript',
185
+ ) );
186
  }
187
 
188
  private function get_blacklisted_tags() {
189
+ return $this->merge_defaults_with_args( 'add_blacklisted_tags', array(
190
  'script',
191
  'noscript',
192
  'style',
202
  'select',
203
  'option',
204
  'link',
205
+ 'picture',
206
+
207
+ // Sanitizers run after embed handlers, so if anything wasn't matched, it needs to be removed.
208
+ 'embed',
209
+ 'embedvideo',
210
 
211
  // These are converted into amp-* versions
212
  //'img',
213
  //'video',
214
  //'audio',
215
  //'iframe',
216
+ ) );
217
  }
218
 
219
  private function get_blacklisted_attributes() {
220
+ return $this->merge_defaults_with_args( 'add_blacklisted_attributes', array(
221
  'style',
222
+ 'size',
223
+ ) );
224
  }
225
  }
includes/sanitizers/class-amp-iframe-sanitizer.php CHANGED
@@ -37,23 +37,22 @@ class AMP_Iframe_Sanitizer extends AMP_Base_Sanitizer {
37
  $node = $nodes->item( $i );
38
  $old_attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node );
39
 
40
- if ( ! array_key_exists( 'src', $old_attributes ) ) {
 
 
 
 
 
 
 
 
41
  $node->parentNode->removeChild( $node );
42
  continue;
43
  }
44
 
45
  $this->did_convert_elements = true;
46
 
47
- $new_attributes = $this->filter_attributes( $old_attributes );
48
-
49
- if ( ! isset( $new_attributes['height'] ) ) {
50
- unset( $new_attributes['width'] );
51
- $new_attributes['height'] = self::FALLBACK_HEIGHT;
52
- }
53
-
54
- if ( ! isset( $new_attributes['width'] ) ) {
55
- $new_attributes['layout'] = 'fixed-height';
56
- }
57
  $new_attributes = $this->enforce_sizes_attribute( $new_attributes );
58
 
59
  $new_node = AMP_DOM_Utils::create_node( $this->dom, 'amp-iframe', $new_attributes );
@@ -84,23 +83,21 @@ class AMP_Iframe_Sanitizer extends AMP_Base_Sanitizer {
84
  foreach ( $attributes as $name => $value ) {
85
  switch ( $name ) {
86
  case 'sandbox':
87
- case 'height':
88
  case 'class':
89
  case 'sizes':
90
  $out[ $name ] = $value;
91
  break;
92
 
93
  case 'src':
94
- $out[ $name ] = set_url_scheme( $value, 'https' );
95
  break;
96
 
97
  case 'width':
98
- if ( $value === '100%' ) {
99
- continue;
100
- }
101
- $out[ $name ] = $value;
102
  break;
103
 
 
104
  case 'frameborder':
105
  if ( '0' !== $value && '1' !== $value ) {
106
  $value = '0';
37
  $node = $nodes->item( $i );
38
  $old_attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node );
39
 
40
+ $new_attributes = $this->filter_attributes( $old_attributes );
41
+
42
+ // If the src doesn't exist, remove the node.
43
+ // This means that it never existed or was invalidated
44
+ // while filtering attributes above.
45
+ //
46
+ // TODO: add a filter to allow for a fallback element in this instance.
47
+ // See: https://github.com/ampproject/amphtml/issues/2261
48
+ if ( empty( $new_attributes['src'] ) ) {
49
  $node->parentNode->removeChild( $node );
50
  continue;
51
  }
52
 
53
  $this->did_convert_elements = true;
54
 
55
+ $new_attributes = $this->enforce_fixed_height( $new_attributes );
 
 
 
 
 
 
 
 
 
56
  $new_attributes = $this->enforce_sizes_attribute( $new_attributes );
57
 
58
  $new_node = AMP_DOM_Utils::create_node( $this->dom, 'amp-iframe', $new_attributes );
83
  foreach ( $attributes as $name => $value ) {
84
  switch ( $name ) {
85
  case 'sandbox':
 
86
  case 'class':
87
  case 'sizes':
88
  $out[ $name ] = $value;
89
  break;
90
 
91
  case 'src':
92
+ $out[ $name ] = $this->maybe_enforce_https_src( $value, true );
93
  break;
94
 
95
  case 'width':
96
+ case 'height':
97
+ $out[ $name ] = $this->sanitize_dimension( $value, $name );
 
 
98
  break;
99
 
100
+
101
  case 'frameborder':
102
  if ( '0' !== $value && '1' !== $value ) {
103
  $value = '0';
includes/sanitizers/class-amp-img-sanitizer.php CHANGED
@@ -81,14 +81,18 @@ class AMP_Img_Sanitizer extends AMP_Base_Sanitizer {
81
  switch ( $name ) {
82
  case 'src':
83
  case 'alt':
84
- case 'width':
85
- case 'height':
86
  case 'class':
87
  case 'srcset':
88
  case 'sizes':
89
  case 'on':
90
  $out[ $name ] = $value;
91
  break;
 
 
 
 
 
 
92
  default;
93
  break;
94
  }
81
  switch ( $name ) {
82
  case 'src':
83
  case 'alt':
 
 
84
  case 'class':
85
  case 'srcset':
86
  case 'sizes':
87
  case 'on':
88
  $out[ $name ] = $value;
89
  break;
90
+
91
+ case 'width':
92
+ case 'height':
93
+ $out[ $name ] = $this->sanitize_dimension( $value, $name );
94
+ break;
95
+
96
  default;
97
  break;
98
  }
includes/sanitizers/class-amp-video-sanitizer.php CHANGED
@@ -22,22 +22,34 @@ class AMP_Video_Sanitizer extends AMP_Base_Sanitizer {
22
  $old_attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node );
23
 
24
  $new_attributes = $this->filter_attributes( $old_attributes );
25
- if ( ! isset( $new_attributes['width'], $new_attributes['height'] ) ) {
26
- $new_attributes['height'] = self::FALLBACK_HEIGHT;
27
- $new_attributes['layout'] = 'fixed-height';
28
- }
29
  $new_attributes = $this->enforce_sizes_attribute( $new_attributes );
30
 
31
  $new_node = AMP_DOM_Utils::create_node( $this->dom, 'amp-video', $new_attributes );
32
 
33
- // TODO: limit child nodes too (only allowed: `source`; move rest to div+fallback)
34
  // TODO: `source` does not have closing tag, and DOMDocument doesn't handle it well.
35
  foreach ( $node->childNodes as $child_node ) {
36
  $new_child_node = $child_node->cloneNode( true );
37
- $new_node->appendChild( $new_child_node );
 
 
 
 
 
 
38
  }
39
 
40
- $node->parentNode->replaceChild( $new_node, $node );
 
 
 
 
 
 
 
 
 
41
  }
42
  }
43
 
@@ -47,23 +59,29 @@ class AMP_Video_Sanitizer extends AMP_Base_Sanitizer {
47
  foreach ( $attributes as $name => $value ) {
48
  switch ( $name ) {
49
  case 'src':
50
- case 'poster':
 
 
51
  case 'width':
52
  case 'height':
 
 
 
 
53
  case 'class':
54
  case 'sizes':
55
  $out[ $name ] = $value;
56
  break;
 
57
  case 'controls':
58
  case 'loop':
59
  case 'muted':
 
60
  if ( 'false' !== $value ) {
61
  $out[ $name ] = '';
62
  }
63
  break;
64
- case 'autoplay':
65
- $out[ $name ] = 'desktop tablet mobile';
66
- break;
67
  default;
68
  break;
69
  }
22
  $old_attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node );
23
 
24
  $new_attributes = $this->filter_attributes( $old_attributes );
25
+
26
+ $new_attributes = $this->enforce_fixed_height( $new_attributes );
 
 
27
  $new_attributes = $this->enforce_sizes_attribute( $new_attributes );
28
 
29
  $new_node = AMP_DOM_Utils::create_node( $this->dom, 'amp-video', $new_attributes );
30
 
 
31
  // TODO: `source` does not have closing tag, and DOMDocument doesn't handle it well.
32
  foreach ( $node->childNodes as $child_node ) {
33
  $new_child_node = $child_node->cloneNode( true );
34
+ $old_child_attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $new_child_node );
35
+ $new_child_attributes = $this->filter_attributes( $old_child_attributes );
36
+
37
+ // Only append source tags with a valid src attribute
38
+ if ( ! empty( $new_child_attributes['src'] ) && 'source' === $new_child_node->tagName ) {
39
+ $new_node->appendChild( $new_child_node );
40
+ }
41
  }
42
 
43
+ // If the node has at least one valid source, replace the old node with it.
44
+ // Otherwise, just remove the node.
45
+ //
46
+ // TODO: Add a fallback handler.
47
+ // See: https://github.com/ampproject/amphtml/issues/2261
48
+ if ( 0 === $new_node->childNodes->length && empty( $new_attributes['src'] ) ) {
49
+ $node->parentNode->removeChild( $node );
50
+ } else {
51
+ $node->parentNode->replaceChild( $new_node, $node );
52
+ }
53
  }
54
  }
55
 
59
  foreach ( $attributes as $name => $value ) {
60
  switch ( $name ) {
61
  case 'src':
62
+ $out[ $name ] = $this->maybe_enforce_https_src( $value );
63
+ break;
64
+
65
  case 'width':
66
  case 'height':
67
+ $out[ $name ] = $this->sanitize_dimension( $value, $name );
68
+ break;
69
+
70
+ case 'poster':
71
  case 'class':
72
  case 'sizes':
73
  $out[ $name ] = $value;
74
  break;
75
+
76
  case 'controls':
77
  case 'loop':
78
  case 'muted':
79
+ case 'autoplay':
80
  if ( 'false' !== $value ) {
81
  $out[ $name ] = '';
82
  }
83
  break;
84
+
 
 
85
  default;
86
  break;
87
  }
includes/utils/class-amp-string-utils.php ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class AMP_String_Utils {
4
+ public static function endswith( $haystack, $needle ) {
5
+ return '' !== $haystack
6
+ && '' !== $needle
7
+ && $needle === substr( $haystack, -strlen( $needle ) );
8
+ }
9
+ }
readme.md CHANGED
@@ -2,7 +2,7 @@
2
 
3
  ## Overview
4
 
5
- This plugin adds support for the [Accelerated Mobile Pages](https://www.ampproject.org) (AMP) Project, which is an an open source initiative that aims to provide mobile optimized content that can load instantly everywhere.
6
 
7
  With the plugin active, all posts on your site will have dynamically generated AMP-compatible versions, accessible by appending `/amp/` to the end your post URLs. For example, if your post URL is `http://example.com/2016/01/01/amp-on/`, you can access the AMP version at `http://example.com/2016/01/01/amp-on/amp/`. If you do not have [pretty permalinks](https://codex.wordpress.org/Using_Permalinks#mod_rewrite:_.22Pretty_Permalinks.22) enabled, you can do the same thing by appending `?amp=1`, i.e. `http://example.com/2016/01/01/amp-on/?amp=1`
8
 
@@ -275,7 +275,7 @@ If you want to add stuff to the head or footer of the default AMP template, use
275
  ```php
276
  add_action( 'amp_post_template_footer', 'xyz_amp_add_pixel' );
277
 
278
- function xyz_amp_add_analytics( $amp_template ) {
279
  $post_id = $amp_template->get( 'post_id' );
280
  ?>
281
  <amp-pixel src="https://example.com/hi.gif?x=RANDOM"></amp-pixel>
@@ -559,7 +559,7 @@ If you want a custom template for your post type:
559
  ```
560
  add_filter( 'amp_post_template_file', 'xyz_amp_set_review_template', 10, 3 );
561
 
562
- function xyz_amp_set_custom_template( $file, $type, $post ) {
563
  if ( 'single' === $type && 'xyz-review' === $post->post_type ) {
564
  $file = dirname( __FILE__ ) . '/templates/my-amp-review-template.php';
565
  }
2
 
3
  ## Overview
4
 
5
+ This plugin adds support for the [Accelerated Mobile Pages](https://www.ampproject.org) (AMP) Project, which is an open source initiative that aims to provide mobile optimized content that can load instantly everywhere.
6
 
7
  With the plugin active, all posts on your site will have dynamically generated AMP-compatible versions, accessible by appending `/amp/` to the end your post URLs. For example, if your post URL is `http://example.com/2016/01/01/amp-on/`, you can access the AMP version at `http://example.com/2016/01/01/amp-on/amp/`. If you do not have [pretty permalinks](https://codex.wordpress.org/Using_Permalinks#mod_rewrite:_.22Pretty_Permalinks.22) enabled, you can do the same thing by appending `?amp=1`, i.e. `http://example.com/2016/01/01/amp-on/?amp=1`
8
 
275
  ```php
276
  add_action( 'amp_post_template_footer', 'xyz_amp_add_pixel' );
277
 
278
+ function xyz_amp_add_pixel( $amp_template ) {
279
  $post_id = $amp_template->get( 'post_id' );
280
  ?>
281
  <amp-pixel src="https://example.com/hi.gif?x=RANDOM"></amp-pixel>
559
  ```
560
  add_filter( 'amp_post_template_file', 'xyz_amp_set_review_template', 10, 3 );
561
 
562
+ function xyz_amp_set_review_template( $file, $type, $post ) {
563
  if ( 'single' === $type && 'xyz-review' === $post->post_type ) {
564
  $file = dirname( __FILE__ ) . '/templates/my-amp-review-template.php';
565
  }
readme.txt CHANGED
@@ -1,9 +1,9 @@
1
  === AMP ===
2
- Contributors: batmoo, joen, automattic
3
  Tags: amp, mobile
4
  Requires at least: 4.4
5
- Tested up to: 4.5
6
- Stable tag: 0.3.2
7
  License: GPLv2 or later
8
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
9
 
@@ -15,7 +15,7 @@ This plugin adds support for the [Accelerated Mobile Pages](https://www.ampproje
15
 
16
  With the plugin active, all posts on your site will have dynamically generated AMP-compatible versions, accessible by appending `/amp/` to the end your post URLs. For example, if your post URL is `http://example.com/2016/01/01/amp-on/`, you can access the AMP version at `http://example.com/2016/01/01/amp-on/amp/`. If you do not have [pretty permalinks](https://codex.wordpress.org/Using_Permalinks#mod_rewrite:_.22Pretty_Permalinks.22) enabled, you can do the same thing by appending `?amp=1`, i.e. `http://example.com/2016/01/01/amp-on/?amp=1`
17
 
18
- Note #1: that Pages and archives are not currently supported.
19
 
20
  Note #2: this plugin only creates AMP content but does not automatically display it to your users when they visit from a mobile device. That is handled by AMP consumers such as Google Search. For more details, see the [AMP Project FAQ](https://www.ampproject.org/docs/support/faqs.html).
21
 
@@ -34,6 +34,27 @@ You can find details about customization options at https://github.com/Automatti
34
 
35
  == Changelog ==
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  = 0.3.2 (Mar 4, 2016) =
38
 
39
  * Jetpack Stats support.
1
  === AMP ===
2
+ Contributors: batmoo, joen, automattic, potatomaster
3
  Tags: amp, mobile
4
  Requires at least: 4.4
5
+ Tested up to: 4.6
6
+ Stable tag: 0.3.3
7
  License: GPLv2 or later
8
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
9
 
15
 
16
  With the plugin active, all posts on your site will have dynamically generated AMP-compatible versions, accessible by appending `/amp/` to the end your post URLs. For example, if your post URL is `http://example.com/2016/01/01/amp-on/`, you can access the AMP version at `http://example.com/2016/01/01/amp-on/amp/`. If you do not have [pretty permalinks](https://codex.wordpress.org/Using_Permalinks#mod_rewrite:_.22Pretty_Permalinks.22) enabled, you can do the same thing by appending `?amp=1`, i.e. `http://example.com/2016/01/01/amp-on/?amp=1`
17
 
18
+ Note #1: that Pages and archives are not currently supported. Pages support is being worked on.
19
 
20
  Note #2: this plugin only creates AMP content but does not automatically display it to your users when they visit from a mobile device. That is handled by AMP consumers such as Google Search. For more details, see the [AMP Project FAQ](https://www.ampproject.org/docs/support/faqs.html).
21
 
34
 
35
  == Changelog ==
36
 
37
+ = 0.4 (in-progress) =
38
+
39
+ - Pages
40
+ - Customizer
41
+ - Updated Template
42
+
43
+ = 0.3.3 (Aug 18, 2016) =
44
+
45
+ - Handle many more validation errors (props bcampeau and alleyinteractive).
46
+ - New filter: `amp_post_template_dir` (props mustafauysal).
47
+ - New template: Nav bar is now it's own template part (props jdevalk).
48
+ - Better ratio for YouTube embeds.
49
+ - Fix: better timezone handling (props rinatkhaziev).
50
+ - Fix: better handling of non-int dimensions (like `100%`).
51
+ - Fix: better handling of empty dimensions.
52
+ - Fix: `autoplay` is a bool-like value.
53
+ - Fix: breakage when using the `query_string` hook (h/t mkuplens).
54
+ - Fix: don't break really large Twitter IDs.
55
+ - Fix: don't break Instagram shortcodes when using URLs with querystrings.
56
+ - Readme improvements (props nickjohnford, sotayamashita)
57
+
58
  = 0.3.2 (Mar 4, 2016) =
59
 
60
  * Jetpack Stats support.
templates/header-bar.php ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <nav class="amp-wp-title-bar">
2
+ <div>
3
+ <a href="<?php echo esc_url( $this->get( 'home_url' ) ); ?>">
4
+ <?php $site_icon_url = $this->get( 'site_icon_url' ); ?>
5
+ <?php if ( $site_icon_url ) : ?>
6
+ <amp-img src="<?php echo esc_url( $site_icon_url ); ?>" width="32" height="32" class="amp-wp-site-icon"></amp-img>
7
+ <?php endif; ?>
8
+ <?php echo esc_html( $this->get( 'blog_name' ) ); ?>
9
+ </a>
10
+ </div>
11
+ </nav>
templates/meta-time.php CHANGED
@@ -4,7 +4,7 @@
4
  echo esc_html(
5
  sprintf(
6
  _x( '%s ago', '%s = human-readable time difference', 'amp' ),
7
- human_time_diff( $this->get( 'post_publish_timestamp' ) )
8
  )
9
  );
10
  ?>
4
  echo esc_html(
5
  sprintf(
6
  _x( '%s ago', '%s = human-readable time difference', 'amp' ),
7
+ human_time_diff( $this->get( 'post_publish_timestamp' ), current_time( 'timestamp' ) )
8
  )
9
  );
10
  ?>
templates/single.php CHANGED
@@ -11,17 +11,7 @@
11
  </style>
12
  </head>
13
  <body>
14
- <nav class="amp-wp-title-bar">
15
- <div>
16
- <a href="<?php echo esc_url( $this->get( 'home_url' ) ); ?>">
17
- <?php $site_icon_url = $this->get( 'site_icon_url' ); ?>
18
- <?php if ( $site_icon_url ) : ?>
19
- <amp-img src="<?php echo esc_url( $site_icon_url ); ?>" width="32" height="32" class="amp-wp-site-icon"></amp-img>
20
- <?php endif; ?>
21
- <?php echo esc_html( $this->get( 'blog_name' ) ); ?>
22
- </a>
23
- </div>
24
- </nav>
25
  <div class="amp-wp-content">
26
  <h1 class="amp-wp-title"><?php echo wp_kses_data( $this->get( 'post_title' ) ); ?></h1>
27
  <ul class="amp-wp-meta">
11
  </style>
12
  </head>
13
  <body>
14
+ <?php $this->load_parts( array( 'header-bar' ) ); ?>
 
 
 
 
 
 
 
 
 
 
15
  <div class="amp-wp-content">
16
  <h1 class="amp-wp-title"><?php echo wp_kses_data( $this->get( 'post_title' ) ); ?></h1>
17
  <ul class="amp-wp-meta">