Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
54.55% covered (warning)
54.55%
66 / 121
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Meta_Tags
54.55% covered (warning)
54.55%
66 / 121
50.00% covered (danger)
50.00%
4 / 8
181.84
0.00% covered (danger)
0.00%
0 / 1
 get_active_plugins
42.86% covered (danger)
42.86%
3 / 7
0.00% covered (danger)
0.00%
0 / 1
2.75
 should_render_meta_tags
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 should_render_twitter_cards_tags
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_featured_image
89.47% covered (warning)
89.47%
17 / 19
0.00% covered (danger)
0.00%
0 / 1
9.09
 get_description
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
2
 get_og_title_for_social_notes
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
6
 get_note_title
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 render_tags
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
210
1<?php
2/**
3 * Adds meta tags to pages that need it.
4 *
5 * @package automattic/jetpack-social-plugin
6 */
7
8namespace Automattic\Jetpack\Social;
9
10use WP_Post;
11
12/**
13 * Adds the meta tags.
14 */
15class Meta_Tags {
16    /**
17     * This list is copied verbatim from class.jetpack.php
18     *
19     * Note: All in One SEO Pack, All in one SEO Pack Pro, WordPress SEO by Yoast, and WordPress SEO Premium by Yoast automatically deactivate
20     * Jetpack's Open Graph tags via filter when their Social Meta modules are active.
21     *
22     * @var array Array of plugin slugs.
23     */
24    private $open_graph_conflicting_plugins = array(
25        'jetpack/jetpack.php',                                   // The Jetpack plugin adds its own meta tags.
26        'jetpack-dev/jetpack.php',                               // Jetpack's location with the beta plugin.
27        '2-click-socialmedia-buttons/2-click-socialmedia-buttons.php', // 2 Click Social Media Buttons.
28        'add-link-to-facebook/add-link-to-facebook.php',         // Add Link to Facebook.
29        'add-meta-tags/add-meta-tags.php',                       // Add Meta Tags.
30        'complete-open-graph/complete-open-graph.php',           // Complete Open Graph.
31        'easy-facebook-share-thumbnails/esft.php',               // Easy Facebook Share Thumbnail.
32        'heateor-open-graph-meta-tags/heateor-open-graph-meta-tags.php', // Open Graph Meta Tags by Heateor.
33        'facebook/facebook.php',                                 // Facebook (official plugin).
34        'facebook-awd/AWD_facebook.php',                         // Facebook AWD All in one.
35        'facebook-featured-image-and-open-graph-meta-tags/fb-featured-image.php', // Facebook Featured Image & OG Meta Tags.
36        'facebook-meta-tags/facebook-metatags.php',              // Facebook Meta Tags.
37        'wonderm00ns-simple-facebook-open-graph-tags/wonderm00n-open-graph.php', // Facebook Open Graph Meta Tags for WordPress.
38        'facebook-revised-open-graph-meta-tag/index.php',        // Facebook Revised Open Graph Meta Tag.
39        'facebook-thumb-fixer/_facebook-thumb-fixer.php',        // Facebook Thumb Fixer.
40        'facebook-and-digg-thumbnail-generator/facebook-and-digg-thumbnail-generator.php', // Fedmich's Facebook Open Graph Meta.
41        'network-publisher/networkpub.php',                      // Network Publisher.
42        'nextgen-facebook/nextgen-facebook.php',                 // NextGEN Facebook OG.
43        'social-networks-auto-poster-facebook-twitter-g/NextScripts_SNAP.php', // NextScripts SNAP.
44        'og-tags/og-tags.php',                                   // OG Tags.
45        'opengraph/opengraph.php',                               // Open Graph.
46        'open-graph-protocol-framework/open-graph-protocol-framework.php', // Open Graph Protocol Framework.
47        'seo-facebook-comments/seofacebook.php',                 // SEO Facebook Comments.
48        'seo-ultimate/seo-ultimate.php',                         // SEO Ultimate.
49        'sexybookmarks/sexy-bookmarks.php',                      // Shareaholic.
50        'shareaholic/sexy-bookmarks.php',                        // Shareaholic.
51        'sharepress/sharepress.php',                             // SharePress.
52        'simple-facebook-connect/sfc.php',                       // Simple Facebook Connect.
53        'social-discussions/social-discussions.php',             // Social Discussions.
54        'social-sharing-toolkit/social_sharing_toolkit.php',     // Social Sharing Toolkit.
55        'socialize/socialize.php',                               // Socialize.
56        'squirrly-seo/squirrly.php',                             // SEO by SQUIRRLYâ„¢.
57        'only-tweet-like-share-and-google-1/tweet-like-plusone.php', // Tweet, Like, Google +1 and Share.
58        'wordbooker/wordbooker.php',                             // Wordbooker.
59        'wpsso/wpsso.php',                                       // WordPress Social Sharing Optimization.
60        'wp-caregiver/wp-caregiver.php',                         // WP Caregiver.
61        'wp-facebook-like-send-open-graph-meta/wp-facebook-like-send-open-graph-meta.php', // WP Facebook Like Send & Open Graph Meta.
62        'wp-facebook-open-graph-protocol/wp-facebook-ogp.php',   // WP Facebook Open Graph protocol.
63        'wp-ogp/wp-ogp.php',                                     // WP-OGP.
64        'wp-seopress/seopress.php',                              // SEOPress.
65        'wp-seopress-pro/seopress-pro.php',                      // SEOPress Pro.
66        'zoltonorg-social-plugin/zosp.php',                      // Zolton.org Social Plugin.
67        'wp-fb-share-like-button/wp_fb_share-like_widget.php',   // WP Facebook Like Button.
68        'open-graph-metabox/open-graph-metabox.php',             // Open Graph Metabox.
69        'seo-by-rank-math/rank-math.php',                        // Rank Math.
70        'slim-seo/slim-seo.php',                                 // Slim SEO.
71    );
72
73    /**
74     * This list is copied verbatim from class.jetpack.php
75     *
76     * @var array Plugins that conflict with Twitter cards.
77     */
78    private $twitter_cards_conflicting_plugins = array(
79        'eewee-twitter-card/index.php',              // Eewee Twitter Card.
80        'ig-twitter-cards/ig-twitter-cards.php',     // IG:Twitter Cards.
81        'jm-twitter-cards/jm-twitter-cards.php',     // JM Twitter Cards.
82        'kevinjohn-gallagher-pure-web-brilliants-social-graph-twitter-cards-extention/kevinjohn_gallagher___social_graph_twitter_output.php',  // Pure Web Brilliant's Social Graph Twitter Cards Extension.
83        'twitter-cards/twitter-cards.php',           // Twitter Cards.
84        'twitter-cards-meta/twitter-cards-meta.php', // Twitter Cards Meta.
85        'wp-to-twitter/wp-to-twitter.php',           // WP to Twitter.
86        'wp-twitter-cards/twitter_cards.php',        // WP Twitter Cards.
87        'seo-by-rank-math/rank-math.php',            // Rank Math.
88        'slim-seo/slim-seo.php',                     // Slim SEO.
89    );
90
91    /**
92     * Get a list of all active plugins.
93     *
94     * @return array Array of active plugins.
95     */
96    public function get_active_plugins() {
97        $active_plugins = (array) get_option( 'active_plugins', array() );
98
99        if ( is_multisite() ) {
100            // Due to legacy code, active_sitewide_plugins stores them in the keys,
101            // whereas active_plugins stores them in the values.
102            $active_plugins = array_merge(
103                $active_plugins,
104                array_keys( get_site_option( 'active_sitewide_plugins', array() ) )
105            );
106        }
107
108        return array_unique( $active_plugins );
109    }
110
111    /**
112     * Check if meta tags should be rendered.
113     *
114     * @return bool True if meta tags should be rendered.
115     */
116    public function should_render_meta_tags() {
117        if ( ! empty( array_intersect( $this->get_active_plugins(), $this->open_graph_conflicting_plugins ) ) ) {
118            return false;
119        }
120
121        /** This filter is documented in projects/plugins/jetpack/functions.opengraph.php */
122        return apply_filters( 'jetpack_enable_open_graph', is_singular() );
123    }
124
125    /**
126     * Check if Twitter Cards tags should be rendered.
127     *
128     * @return bool True if Twitter Cards tags should be rendered.
129     */
130    public function should_render_twitter_cards_tags() {
131        return empty( array_intersect( $this->get_active_plugins(), $this->twitter_cards_conflicting_plugins ) );
132    }
133
134    /**
135     * Get the featured image for a post.
136     *
137     * @param int $post_id The post ID. Optional. Defaults to global $post.
138     * @param int $width   The minimum width of the image. Optional. Defaults to 200.
139     * @param int $height  The minimum height of the image. Optional. Defaults to 200.
140     * @return array The featured image and dimensions. Empty array if no image is found.
141     */
142    public function get_featured_image( $post_id = null, $width = 200, $height = 200 ) {
143        $post = get_post( $post_id );
144
145        if (
146            empty( $post ) ||
147            ! has_post_thumbnail( $post ) ||
148            post_password_required( $post_id )
149        ) {
150            return array();
151        }
152
153        $thumb = get_post_thumbnail_id( $post );
154        $meta  = wp_get_attachment_metadata( $thumb );
155
156        // Must be larger than requested minimums.
157        if ( ! isset( $meta['width'] ) || $meta['width'] < $width ) {
158            return array();
159        }
160
161        if ( ! isset( $meta['height'] ) || $meta['height'] < $height ) {
162            return array();
163        }
164
165        $img_src = wp_get_attachment_image_src( $thumb, array( 1200, 1200 ) );
166
167        if ( empty( $img_src ) ) {
168            return array();
169        }
170
171        return array(
172            'src'        => $img_src[0],
173            'src_width'  => $img_src[1],
174            'src_height' => $img_src[2],
175        );
176    }
177
178    /**
179     * Clean up text meant to be used as Description Open Graph tag.
180     *
181     * There should be:
182     * - no links
183     * - no shortcodes
184     * - no html tags or their contents
185     * - not too many words.
186     *
187     * @param string       $description Text coming from WordPress (autogenerated or manually generated by author).
188     * @param WP_Post|null $data        Information about our post.
189     *
190     * @return string $description Cleaned up description string.
191     */
192    public function get_description( $description = '', $data = null ) {
193        // Remove tags such as <style or <script.
194        $description = wp_strip_all_tags( $description );
195
196        /*
197         * Clean up any plain text entities left into formatted entities.
198         * Intentionally not using a filter to prevent pollution.
199         * @see https://github.com/Automattic/jetpack/pull/2899#issuecomment-151957382
200         */
201        $description = wp_kses(
202            trim(
203                convert_chars(
204                    wptexturize( $description )
205                )
206            ),
207            array()
208        );
209
210        // Remove shortcodes.
211        $description = strip_shortcodes( $description );
212
213        // Remove links.
214        $description = preg_replace(
215            '@https?://[\S]+@',
216            '',
217            $description
218        );
219
220        /*
221         * Limit things to a small text blurb.
222         * There isn't a hard limit set by Facebook, so let's rely on WP's own limit.
223         * (55 words or the localized equivalent).
224         * This limit can be customized with the wp_trim_words filter.
225         */
226        $description = wp_trim_words( $description );
227
228        // Let's set a default if we have no text by now.
229        if ( empty( $description ) ) {
230            /** This filter is documented in projects/plugins/jetpack/functions.opengraph.php */
231            $description = apply_filters(
232                'jetpack_open_graph_fallback_description',
233                __( 'Visit the post for more.', 'jetpack-social' ),
234                $data
235            );
236        }
237
238        // Trim the description if it's still too long, and add an ellipsis.
239        $description_length = 197;
240        $description        = mb_strimwidth( $description, 0, $description_length, '…' );
241
242        return $description;
243    }
244
245    /**
246     * To set a custom OG:title for social notes.
247     */
248    public function get_og_title_for_social_notes() {
249        $text     = wp_strip_all_tags( get_the_excerpt() );
250        $length   = 55;
251        $ellipsis = "\u{2026}";
252
253        if ( strlen( $text ) <= $length ) {
254            return $text;
255        }
256
257        $words   = str_word_count( $text, 2 );
258        $indices = array_keys( $words );
259
260        // There is only one word, or the first word plus initial non-word characters, is longer than 55 characters.
261        if ( count( $indices ) === 1 || $indices[0] + strlen( $words[ $indices[0] ] ) > $length ) {
262            return substr( $text, 0, $length ) . $ellipsis;
263        }
264
265        $substring_index = 0;
266        foreach ( $indices as $current_index ) {
267            $current_length = $current_index + strlen( $words[ $current_index ] );
268            if ( $current_length > $length ) {
269                $substring_index = $current_index - 1;
270                break;
271            }
272            $substring_index = $current_length;
273        }
274        return substr( $text, 0, $substring_index ) . $ellipsis;
275    }
276
277    /**
278     * Filters the OG tags when we are displaying a Social Note,
279     * and adjusts the title. This allows us to adjust the title
280     * when Jetpack is active as well as social.
281     *
282     * @param array $tags The array of OG tags so far.
283     */
284    public function get_note_title( $tags ) {
285        if ( ! isset( $tags['og:title'] ) || empty( trim( $tags['og:title'] ) ) ) {
286            $tags['og:title'] = $this->get_og_title_for_social_notes();
287        }
288        return $tags;
289    }
290
291    /**
292     * Render meta tags in head.
293     *
294     * @param WP_Post|null $post The post to render the tags for.
295     */
296    public function render_tags( $post = null ) {
297        $data = get_post( $post );
298        if ( empty( $data ) ) {
299            return;
300        }
301
302        if ( $data->post_type === Note::JETPACK_SOCIAL_NOTE_CPT ) {
303            add_filter( 'jetpack_open_graph_tags', array( $this, 'get_note_title' ) );
304        }
305
306        if ( ! $this->should_render_meta_tags() ) {
307            return;
308        }
309
310        $tags = array();
311
312        /** This filter is documented in core/src/wp-includes/post-template.php */
313        $tags['og:title'] = wp_kses( apply_filters( 'the_title', $data->post_title, $data->ID ), array() );
314        $tags['og:url']   = get_permalink( $data->ID );
315        if ( ! post_password_required( $data ) ) {
316            $excerpt = '';
317
318            /*
319             * If the post author set an excerpt, use that.
320             * Otherwise, pick the post content that comes before the More tag if there is one.
321             * Do not use the post content if it contains premium content.
322             */
323            if ( ! empty( $data->post_excerpt ) ) {
324                $excerpt = $data->post_excerpt;
325            } elseif ( ! has_block( 'premium-content/container', $data->post_content ) ) {
326                $excerpt = explode( '<!--more-->', $data->post_content )[0];
327            }
328
329            $tags['og:description'] = $this->get_description( $excerpt );
330        }
331
332        $image = $this->get_featured_image();
333
334        if ( ! empty( $image ) ) {
335            $tags = array_merge(
336                $tags,
337                array(
338                    'og:image'        => $image['src'],
339                    'og:image:width'  => $image['src_width'],
340                    'og:image:height' => $image['src_height'],
341                )
342            );
343        }
344
345        /** This filter is documented in projects/plugins/jetpack/functions.opengraph.php */
346        $image_width = absint( apply_filters( 'jetpack_open_graph_image_width', 200 ) );
347
348        /** This filter is documented in projects/plugins/jetpack/functions.opengraph.php */
349        $image_height = absint( apply_filters( 'jetpack_open_graph_image_height', 200 ) );
350
351        /** This filter is documented in projects/plugins/jetpack/functions.opengraph.php */
352        $tags = apply_filters( 'jetpack_open_graph_tags', $tags, compact( 'image_width', 'image_height' ) );
353        if ( empty( trim( $tags['og:title'] ) ) ) {
354                $tags['og:title'] = __( '(no title)', 'jetpack-social' );
355        }
356
357        if ( ! empty( $tags['og:image'] ) && $this->should_render_twitter_cards_tags() ) {
358            $tags = array_merge(
359                $tags,
360                array(
361                    'twitter:image' => $tags['og:image'],
362                    'twitter:card'  => 'summary_large_image',
363                )
364            );
365        }
366
367        echo '<!-- Generated by Jetpack Social -->' . PHP_EOL;
368
369        foreach ( $tags as $property => $content ) {
370            $label = ! str_contains( $property, 'twitter' ) ? 'property' : 'name';
371
372            if ( $content ) {
373                printf( '<meta %1$s="%2$s" content="%3$s">' . PHP_EOL, esc_attr( $label ), esc_attr( $property ), esc_attr( $content ) );
374            }
375        }
376
377        echo '<!-- / Jetpack Social -->' . PHP_EOL;
378    }
379}