Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
8.00% covered (danger)
8.00%
14 / 175
4.17% covered (danger)
4.17%
1 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_AMP_Support
8.00% covered (danger)
8.00%
14 / 175
4.17% covered (danger)
4.17%
1 / 24
5845.18
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 comment_likes_enabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 admin_init
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_amp_canonical
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 is_amp_available
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 is_amp_request
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 is_amp_legacy
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 amp_disable_the_content_filters
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 disable_comment_likes_before_the_content
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 add_stats_pixel
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 amp_post_template_metadata
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
 add_site_icon_to_metadata
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 add_image_to_metadata
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
42
 add_fallback_image_to_metadata
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 staticize_subdomain
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 extract_image_dimensions_from_getimagesize
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
56
 amp_post_jetpack_og_tags
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 videopress_enable_freedom_mode
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 render_sharing_html
80.00% covered (warning)
80.00%
12 / 15
0.00% covered (danger)
0.00%
0 / 1
6.29
 amp_disable_sharedaddy_css
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 amp_enqueue_sharing_css
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 amp_reader_sharing_css
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 filter_photon_post_image_args_for_stories
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
210
 filter_jetpack_options_safelist
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
1<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
3use Automattic\Jetpack\Assets;
4use Automattic\Jetpack\Stats\Tracking_Pixel as Stats_Tracking_Pixel;
5use Automattic\Jetpack\Sync\Functions;
6
7/**
8 * Manages compatibility with the amp-wp plugin
9 *
10 * @see https://github.com/Automattic/amp-wp
11 */
12class Jetpack_AMP_Support {
13
14    /**
15     * Apply custom AMP changes on the front-end.
16     */
17    public static function init() {
18
19        // Add Stats tracking pixel on Jetpack sites when the Stats module is active.
20        if (
21            Jetpack::is_module_active( 'stats' )
22            && ! ( defined( 'IS_WPCOM' ) && IS_WPCOM )
23        ) {
24            add_action( 'amp_post_template_footer', array( 'Jetpack_AMP_Support', 'add_stats_pixel' ) );
25        }
26
27        // Sharing.
28        add_filter( 'jetpack_sharing_display_markup', array( 'Jetpack_AMP_Support', 'render_sharing_html' ), 10, 2 );
29        add_filter( 'sharing_enqueue_scripts', array( 'Jetpack_AMP_Support', 'amp_disable_sharedaddy_css' ) );
30        add_action( 'wp_enqueue_scripts', array( 'Jetpack_AMP_Support', 'amp_enqueue_sharing_css' ) );
31
32        // Sharing for Reader mode.
33        if ( function_exists( 'jetpack_social_menu_include_svg_icons' ) ) {
34            add_action( 'amp_post_template_footer', 'jetpack_social_menu_include_svg_icons' );
35        }
36        add_action( 'amp_post_template_css', array( 'Jetpack_AMP_Support', 'amp_reader_sharing_css' ), 10, 0 );
37
38        // enforce freedom mode for videopress.
39        add_filter( 'videopress_shortcode_options', array( 'Jetpack_AMP_Support', 'videopress_enable_freedom_mode' ) );
40
41        // include Jetpack og tags when rendering native AMP head.
42        add_action( 'amp_post_template_head', array( 'Jetpack_AMP_Support', 'amp_post_jetpack_og_tags' ) );
43
44        // Post rendering changes for legacy AMP.
45        add_action( 'pre_amp_render_post', array( 'Jetpack_AMP_Support', 'amp_disable_the_content_filters' ) );
46
47        // Disable Comment Likes.
48        add_filter( 'jetpack_comment_likes_enabled', array( 'Jetpack_AMP_Support', 'comment_likes_enabled' ) );
49
50        // Transitional mode AMP should not have comment likes.
51        add_filter( 'the_content', array( 'Jetpack_AMP_Support', 'disable_comment_likes_before_the_content' ) );
52
53        // Add post template metadata for legacy AMP.
54        add_filter( 'amp_post_template_metadata', array( 'Jetpack_AMP_Support', 'amp_post_template_metadata' ), 10, 2 );
55
56        // Filter photon image args for AMP Stories.
57        add_filter( 'jetpack_photon_post_image_args', array( 'Jetpack_AMP_Support', 'filter_photon_post_image_args_for_stories' ), 10, 2 );
58
59        // Sync the amp-options.
60        add_filter( 'jetpack_options_whitelist', array( 'Jetpack_AMP_Support', 'filter_jetpack_options_safelist' ) );
61    }
62
63    /**
64     * Disable the Comment Likes feature on AMP views.
65     *
66     * @param bool $enabled Should comment likes be enabled.
67     */
68    public static function comment_likes_enabled( $enabled ) {
69        return $enabled && ! self::is_amp_request();
70    }
71
72    /**
73     * Apply custom AMP changes in wp-admin.
74     */
75    public static function admin_init() {
76        // disable Likes metabox for post editor if AMP canonical disabled.
77        add_filter( 'post_flair_disable', array( 'Jetpack_AMP_Support', 'is_amp_canonical' ), 99 );
78    }
79
80    /**
81     * Is the page in AMP 'canonical mode'.
82     * Used when themes register support for AMP with `add_theme_support( 'amp' )`.
83     *
84     * @return bool is_amp_canonical
85     */
86    public static function is_amp_canonical() {
87        return function_exists( 'amp_is_canonical' ) && amp_is_canonical();
88    }
89
90    /**
91     * Is AMP available for this request
92     * This returns false for admin, CLI requests etc.
93     *
94     * @return bool is_amp_available
95     */
96    public static function is_amp_available() {
97        return ( function_exists( 'amp_is_available' ) && amp_is_available() );
98    }
99
100    /**
101     * Does the page return AMP content.
102     *
103     * @return bool $is_amp_request Are we on am AMP view.
104     */
105    public static function is_amp_request() {
106        $is_amp_request = ( function_exists( 'is_amp_endpoint' ) && is_amp_endpoint() );
107
108        /**
109         * Returns true if the current request should return valid AMP content.
110         *
111         * @since 6.2.0
112         *
113         * @param boolean $is_amp_request Is this request supposed to return valid AMP content?
114         */
115        return apply_filters( 'jetpack_is_amp_request', $is_amp_request );
116    }
117
118    /**
119     * Determines whether the legacy AMP post templates are being used.
120     *
121     * @since 10.6.0
122     *
123     * @return bool
124     */
125    public static function is_amp_legacy() {
126        return ( function_exists( 'amp_is_legacy' ) && amp_is_legacy() );
127    }
128
129    /**
130     * Remove content filters added by Jetpack.
131     */
132    public static function amp_disable_the_content_filters() {
133        if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
134            add_filter( 'protected_embeds_use_form_post', '__return_false' );
135            remove_filter( 'the_title', 'widont' );
136        }
137
138        remove_filter( 'pre_kses', array( 'Filter_Embedded_HTML_Objects', 'filter' ), 11 );
139        remove_filter( 'pre_kses', array( 'Filter_Embedded_HTML_Objects', 'maybe_create_links' ), 100 );
140    }
141
142    /**
143     * Do not add comment likes on AMP requests.
144     *
145     * @param string $content Post content.
146     */
147    public static function disable_comment_likes_before_the_content( $content ) {
148        if ( self::is_amp_request() ) {
149            remove_filter( 'comment_text', 'comment_like_button', 12 );
150        }
151        return $content;
152    }
153
154    /**
155     * Add Jetpack stats pixel.
156     *
157     * @since 6.2.1
158     */
159    public static function add_stats_pixel() {
160        if ( ! has_action( 'wp_footer', array( Stats_Tracking_Pixel::class, 'add_amp_pixel' ) ) ) {
161            return;
162        }
163
164        $stats_data = Stats_Tracking_Pixel::build_view_data();
165        Stats_Tracking_Pixel::render_amp_footer( $stats_data );
166    }
167
168    /**
169     * Add publisher and image metadata to legacy AMP post.
170     *
171     * @since 6.2.0
172     *
173     * @param array   $metadata Metadata array.
174     * @param WP_Post $post     Post.
175     * @return array Modified metadata array.
176     */
177    public static function amp_post_template_metadata( $metadata, $post ) {
178        if ( isset( $metadata['publisher'] ) && ! isset( $metadata['publisher']['logo'] ) ) {
179            $metadata = self::add_site_icon_to_metadata( $metadata );
180        }
181
182        if ( ! isset( $metadata['image'] ) && ! empty( $post ) ) {
183            $metadata = self::add_image_to_metadata( $metadata, $post );
184        }
185
186        return $metadata;
187    }
188
189    /**
190     * Add blavatar to legacy AMP post metadata.
191     *
192     * @since 6.2.0
193     *
194     * @param array $metadata Metadata.
195     *
196     * @return array Metadata.
197     */
198    private static function add_site_icon_to_metadata( $metadata ) {
199        $size          = 60;
200        $site_icon_url = class_exists( 'Automattic\\Jetpack\\Sync\\Functions' ) ? Functions::site_icon_url( $size ) : '';
201
202        if ( function_exists( 'blavatar_domain' ) ) {
203            $metadata['publisher']['logo'] = array(
204                '@type'  => 'ImageObject',
205                'url'    => blavatar_url( blavatar_domain( site_url() ), 'img', $size, self::staticize_subdomain( 'https://wordpress.com/i/favicons/apple-touch-icon-60x60.png' ) ),
206                'width'  => $size,
207                'height' => $size,
208            );
209        } elseif ( $site_icon_url ) {
210            $metadata['publisher']['logo'] = array(
211                '@type'  => 'ImageObject',
212                'url'    => $site_icon_url,
213                'width'  => $size,
214                'height' => $size,
215            );
216        }
217
218        return $metadata;
219    }
220
221    /**
222     * Add image to legacy AMP post metadata.
223     *
224     * @since 6.2.0
225     *
226     * @param array   $metadata Metadata.
227     * @param WP_Post $post     Post.
228     * @return array Metadata.
229     */
230    private static function add_image_to_metadata( $metadata, $post ) {
231        $image = Jetpack_PostImages::get_image(
232            $post->ID,
233            array(
234                'fallback_to_avatars' => true,
235                'avatar_size'         => 200,
236                // AMP already attempts these.
237                'from_thumbnail'      => false,
238                'from_attachment'     => false,
239            )
240        );
241
242        if ( empty( $image ) ) {
243            return self::add_fallback_image_to_metadata( $metadata );
244        }
245
246        if ( ! isset( $image['src_width'] ) ) {
247            $dimensions = self::extract_image_dimensions_from_getimagesize(
248                array(
249                    $image['src'] => false,
250                )
251            );
252
253            if ( false !== $dimensions[ $image['src'] ] ) {
254                $image['src_width']  = $dimensions['width'];
255                $image['src_height'] = $dimensions['height'];
256            }
257        }
258
259        $metadata['image'] = array(
260            '@type' => 'ImageObject',
261            'url'   => $image['src'],
262        );
263        if ( isset( $image['src_width'] ) ) {
264            $metadata['image']['width'] = $image['src_width'];
265        }
266        if ( isset( $image['src_width'] ) ) {
267            $metadata['image']['height'] = $image['src_height'];
268        }
269
270        return $metadata;
271    }
272
273    /**
274     * Add fallback image to legacy AMP post metadata.
275     *
276     * @since 6.2.0
277     *
278     * @param array $metadata Metadata.
279     * @return array Metadata.
280     */
281    private static function add_fallback_image_to_metadata( $metadata ) {
282        /** This filter is documented in functions.opengraph.php */
283        $default_image = apply_filters( 'jetpack_open_graph_image_default', 'https://wordpress.com/i/blank.jpg' );
284
285        $metadata['image'] = array(
286            '@type'  => 'ImageObject',
287            'url'    => self::staticize_subdomain( $default_image ),
288            'width'  => 200,
289            'height' => 200,
290        );
291
292        return $metadata;
293    }
294
295    /**
296     * Return static WordPress.com domain to use to load resources from WordPress.com.
297     *
298     * @param string $domain Asset URL.
299     */
300    private static function staticize_subdomain( $domain ) {
301        // deal with WPCOM vs Jetpack.
302        if ( function_exists( 'staticize_subdomain' ) ) {
303            return staticize_subdomain( $domain );
304        } else {
305            return Assets::staticize_subdomain( $domain );
306        }
307    }
308
309    /**
310     * Extract image dimensions via wpcom/imagesize, only on WPCOM
311     *
312     * @since 6.2.0
313     *
314     * @param array $dimensions Dimensions.
315     * @return array Dimensions.
316     */
317    private static function extract_image_dimensions_from_getimagesize( $dimensions ) {
318        if ( ! ( defined( 'IS_WPCOM' ) && IS_WPCOM && function_exists( 'require_lib' ) ) ) {
319            return $dimensions;
320        }
321        require_lib( 'wpcom/imagesize' );
322
323        foreach ( $dimensions as $url => $value ) {
324            if ( is_array( $value ) ) {
325                continue;
326            }
327            $result = wpcom_getimagesize( $url );
328            if ( is_array( $result ) ) {
329                $dimensions[ $url ] = array(
330                    'width'  => $result[0],
331                    'height' => $result[1],
332                );
333            }
334        }
335
336        return $dimensions;
337    }
338
339    /**
340     * Display Open Graph Meta tags in AMP views.
341     */
342    public static function amp_post_jetpack_og_tags() {
343        if ( ! ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
344            Jetpack::init()->check_open_graph();
345        }
346
347        if ( function_exists( 'jetpack_og_tags' ) ) {
348            jetpack_og_tags();
349        }
350    }
351
352    /**
353     * Force Freedom mode in VideoPress.
354     *
355     * @param array $options Array of VideoPress shortcode options.
356     */
357    public static function videopress_enable_freedom_mode( $options ) {
358        if ( self::is_amp_request() ) {
359            $options['freedom'] = true;
360        }
361        return $options;
362    }
363
364    /**
365     * Display custom markup for the sharing buttons when in an AMP view.
366     *
367     * @param string $markup          Content markup of the Jetpack sharing links.
368     * @param array  $sharing_enabled Array of Sharing Services currently enabled.
369     */
370    public static function render_sharing_html( $markup, $sharing_enabled ) {
371        global $post;
372
373        if ( empty( $post ) ) {
374            return '';
375        }
376
377        if ( ! self::is_amp_request() ) {
378            return $markup;
379        }
380
381        remove_action( 'wp_footer', 'sharing_add_footer' );
382        if ( empty( $sharing_enabled ) ) {
383            return $markup;
384        }
385
386        $sharing_links = array();
387        foreach ( $sharing_enabled['visible'] as $service ) {
388            $sharing_link = $service->get_amp_display( $post );
389            if ( ! empty( $sharing_link ) ) {
390                $sharing_links[] = $sharing_link;
391            }
392        }
393
394        // Replace the existing unordered list with AMP sharing buttons.
395        $markup = preg_replace( '#<ul>(.+)</ul>#', implode( '', $sharing_links ), $markup );
396
397        // Remove any lingering share-end list items.
398        $markup = str_replace( '<li class="share-end"></li>', '', $markup );
399
400        return $markup;
401    }
402
403    /**
404     * Tells Jetpack not to enqueue CSS for share buttons.
405     *
406     * @param  bool $enqueue Whether or not to enqueue.
407     * @return bool          Whether or not to enqueue.
408     */
409    public static function amp_disable_sharedaddy_css( $enqueue ) {
410        if ( self::is_amp_request() ) {
411            $enqueue = false;
412        }
413
414        return $enqueue;
415    }
416
417    /**
418     * Enqueues the AMP specific sharing styles for the sharing icons.
419     */
420    public static function amp_enqueue_sharing_css() {
421        if (
422            Jetpack::is_module_active( 'sharedaddy' )
423            && self::is_amp_request()
424            && ! self::is_amp_legacy()
425        ) {
426            wp_enqueue_style( 'sharedaddy-amp', plugin_dir_url( __DIR__ ) . 'modules/sharedaddy/amp-sharing.css', array( 'social-logos' ), JETPACK__VERSION );
427        }
428    }
429
430    /**
431     * For the AMP Reader mode template, include styles that we need.
432     */
433    public static function amp_reader_sharing_css() {
434        // If sharing is not enabled, we should not proceed to render the CSS.
435        if ( ! defined( 'JETPACK_SOCIAL_LOGOS_DIR' ) || ! defined( 'JETPACK_SOCIAL_LOGOS_URL' ) || ! defined( 'WP_SHARING_PLUGIN_DIR' ) ) {
436            return;
437        }
438
439        /*
440         * We'll need to output the full contents of the 2 files
441         * in the head on AMP views. We can't rely on regular enqueues here.
442         * @todo As of AMP plugin v1.5, you can actually rely on regular enqueues thanks to https://github.com/ampproject/amp-wp/pull/4299. Once WPCOM upgrades AMP, then this method can be eliminated.
443         *
444         * phpcs:disable WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
445         * phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
446         */
447        $css = file_get_contents( JETPACK_SOCIAL_LOGOS_DIR . 'social-logos.css' );
448        $css = preg_replace( '#(?<=url\(")(?=social-logos\.)#', JETPACK_SOCIAL_LOGOS_URL, $css ); // Make sure font files get their absolute paths.
449        echo $css;
450        echo file_get_contents( WP_SHARING_PLUGIN_DIR . 'amp-sharing.css' );
451
452        /*
453         * phpcs:enable WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
454         * phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
455         */
456    }
457
458    /**
459     * Ensure proper Photon image dimensions for AMP Stories.
460     *
461     * @param array $args Array of Photon Arguments.
462     * @param array $details {
463     *     Array of image details.
464     *
465     *     @type string    $tag            Image tag (Image HTML output).
466     *     @type string    $src            Image URL.
467     *     @type string    $src_orig       Original Image URL.
468     *     @type int|false $width          Image width.
469     *     @type int|false $height         Image height.
470     *     @type int|false $width_orig     Original image width before constrained by content_width.
471     *     @type int|false $height_orig    Original Image height before constrained by content_width.
472     *     @type string    $transform_orig Original transform before constrained by content_width.
473     * }
474     * @return array Args.
475     */
476    public static function filter_photon_post_image_args_for_stories( $args, $details ) {
477        if ( ! is_singular( 'amp_story' ) ) {
478            return $args;
479        }
480
481        // Percentage-based dimensions are not allowed in AMP, so this shouldn't happen, but short-circuit just in case.
482        if ( str_contains( $details['width_orig'], '%' ) || str_contains( $details['height_orig'], '%' ) ) {
483            return $args;
484        }
485
486        $max_height = 1280; // See image size with the slug \AMP_Story_Post_Type::MAX_IMAGE_SIZE_SLUG.
487        $transform  = $details['transform_orig'];
488        $width      = $details['width_orig'];
489        $height     = $details['height_orig'];
490
491        // If height is available, constrain to $max_height.
492        if ( false !== $height ) {
493            if ( $height > $max_height && false !== $height ) {
494                $width  = ( $max_height * $width ) / $height;
495                $height = $max_height;
496            } elseif ( $height > $max_height ) {
497                $height = $max_height;
498            }
499        }
500
501        /*
502         * Set a height if none is found.
503         * If height is set in this manner and height is available, use `fit` instead of `resize` to prevent skewing.
504         */
505        if ( false === $height ) {
506            $height = $max_height;
507            if ( false !== $width ) {
508                $transform = 'fit';
509            }
510        }
511
512        // Build array of Photon args and expose to filter before passing to Photon URL function.
513        $args = array();
514
515        if ( false !== $width && false !== $height ) {
516            $args[ $transform ] = $width . ',' . $height;
517        } elseif ( false !== $width ) {
518            $args['w'] = $width;
519        } elseif ( false !== $height ) {
520            $args['h'] = $height;
521        }
522
523        return $args;
524    }
525
526    /**
527     *  Adds amp-options to the list of options to sync, if AMP is available
528     *
529     * @param array $options_safelist Safelist of options to sync.
530     *
531     * @return array Updated options safelist
532     */
533    public static function filter_jetpack_options_safelist( $options_safelist ) {
534        if ( function_exists( 'is_amp_endpoint' ) ) {
535            $options_safelist[] = 'amp-options';
536        }
537        return $options_safelist;
538    }
539}