Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
45.27% covered (danger)
45.27%
67 / 148
37.50% covered (danger)
37.50%
3 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_SEO
45.27% covered (danger)
45.27%
67 / 148
37.50% covered (danger)
37.50%
3 / 8
570.10
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 init
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 add_custom_field_post_type_meta
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 get_authors
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 get_viewed_posts_page
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 get_posts_page_description
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 set_custom_og_tags
77.27% covered (warning)
77.27%
17 / 22
0.00% covered (danger)
0.00%
0 / 1
13.69
 meta_tags
47.13% covered (danger)
47.13%
41 / 87
0.00% covered (danger)
0.00%
0 / 1
134.76
1<?php
2/**
3 * Main class file for the SEO Tools module.
4 *
5 * @package automattic/jetpack
6 */
7
8/**
9 * An SEO expert walks into a bar, bars, pub, public house, Irish pub, drinks, beer, wine, liquor, Grey Goose, Cristal...
10 *
11 * @phan-constructor-used-for-side-effects
12 */
13class Jetpack_SEO {
14    /**
15     * Constructor.
16     */
17    public function __construct() {
18        add_action( 'init', array( $this, 'init' ) );
19    }
20
21    /**
22     * Initialization method for Jetpack_SEO.
23     */
24    public function init() {
25        /**
26         * Can be used to prevent SEO tools from inserting custom meta tags.
27         *
28         * @module seo-tools
29         *
30         * @since 4.4.0
31         *
32         * @param bool true Should Jetpack's SEO Meta Tags be enabled. Defaults to true.
33         */
34        if ( apply_filters( 'jetpack_seo_meta_tags_enabled', true ) ) {
35            add_action( 'wp_head', array( $this, 'meta_tags' ) );
36
37            // Add support for editing page excerpts in pages, regardless of theme support.
38            add_post_type_support( 'page', 'excerpt' );
39        }
40
41        /**
42         * Can be used to prevent SEO tools from modifying site titles.
43         *
44         * @module seo-tools
45         *
46         * @since 4.4.0
47         *
48         * @param bool true Should Jetpack SEO modify site titles. Defaults to true.
49         */
50        if ( apply_filters( 'jetpack_seo_custom_titles', true ) ) {
51            // Overwrite page title with custom SEO meta title for themes that support title-tag.
52            add_filter( 'pre_get_document_title', array( 'Jetpack_SEO_Titles', 'get_custom_title' ) );
53
54            // Add overwrite support for themes that don't support title-tag.
55            add_filter( 'wp_title', array( 'Jetpack_SEO_Titles', 'get_custom_title' ) );
56        }
57
58        add_filter( 'jetpack_open_graph_tags', array( $this, 'set_custom_og_tags' ) );
59        Jetpack_SEO_Posts::register_post_meta();
60        // Exclude posts with 'jetpack_seo_noindex' set true from the Jetpack sitemap.
61        add_filter( 'jetpack_sitemap_skip_post', array( 'Jetpack_SEO_Posts', 'exclude_noindex_posts_from_jetpack_sitemap' ), 10, 2 );
62        add_action( 'rest_api_init', array( $this, 'add_custom_field_post_type_meta' ) );
63    }
64
65    /**
66     * Add custom field meta to all public post types that don't already have it.
67     */
68    public function add_custom_field_post_type_meta() {
69        /**
70         * Filter the list of post types for which custom fields support is added.
71         *
72         * This filter allows modification of the post types that will be processed
73         * to add support for custom fields if they do not already support it.
74         *
75         * @since 14.2
76         *
77         * @param array $post_types An array of post type names.
78         */
79        $post_types = apply_filters(
80            'jetpack_seo_custom_field_post_types',
81            get_post_types(
82                array(
83                    'public'   => true,
84                    'show_ui'  => true,
85                    '_builtin' => false,
86                )
87            )
88        );
89
90        foreach ( $post_types as $post_type ) {
91            if ( ! post_type_supports( $post_type, 'custom-fields' ) ) {
92                add_post_type_support( $post_type, 'custom-fields' );
93            }
94        }
95    }
96
97    /**
98     * Helper method to fetch authors.
99     */
100    private function get_authors() {
101        global $wp_query;
102
103        $authors = array();
104
105        foreach ( $wp_query->posts as $post ) {
106            if ( ! $post instanceof WP_Post ) {
107                continue;
108            }
109            $authors[] = get_the_author_meta( 'display_name', (int) $post->post_author );
110        }
111
112        $authors = array_unique( $authors );
113
114        return $authors;
115    }
116
117    /**
118     * Gets the page set as the posts page in Settings > Reading, when that page is being viewed.
119     *
120     * @return WP_Post|null The published posts page, or null on any other view.
121     */
122    private function get_viewed_posts_page() {
123        global $wp_query;
124
125        if ( empty( $wp_query->is_posts_page ) ) {
126            return null;
127        }
128
129        $page = get_queried_object();
130
131        // WordPress still lists posts at an unpublished page's URL, and that page's text must stay private.
132        return $page instanceof WP_Post && 'publish' === $page->post_status ? $page : null;
133    }
134
135    /**
136     * Gets the posts page's SEO description, falling back to its excerpt.
137     *
138     * Jetpack_SEO_Posts::get_post_description() can't be used: it returns nothing off singular views.
139     *
140     * @param WP_Post $page The posts page.
141     * @return string Description, or empty string if there is none to show.
142     */
143    private function get_posts_page_description( $page ) {
144        if ( post_password_required( $page ) ) {
145            return '';
146        }
147
148        $description = Jetpack_SEO_Posts::get_post_custom_description( $page );
149
150        return $description ? $description : $page->post_excerpt;
151    }
152
153    /**
154     * Constructs open graph tag data.
155     *
156     * @param array $tags Array of tag data.
157     * @return array of tag data.
158     */
159    public function set_custom_og_tags( $tags ) {
160        $custom_title = Jetpack_SEO_Titles::get_custom_title();
161
162        if ( ! empty( $custom_title ) ) {
163            $tags['og:title'] = $custom_title;
164        }
165
166        // On archives and a latest-posts homepage, get_post() returns the first post in
167        // the loop, whose description does not represent the page being viewed. Only use
168        // the per-post custom description when we are actually on that singular post/page.
169        $post_custom_description = is_singular()
170            ? Jetpack_SEO_Posts::get_post_custom_description( get_post() )
171            : '';
172        $front_page_meta         = Jetpack_SEO_Utils::get_front_page_meta_description();
173
174        $posts_page = Jetpack_SEO_Utils::is_enabled_jetpack_seo() ? $this->get_viewed_posts_page() : null;
175
176        if ( $posts_page ) {
177            $posts_page_title = wp_kses( $posts_page->post_title, array() );
178
179            if ( $posts_page_title ) {
180                $tags['og:title'] = $posts_page_title;
181            }
182
183            $post_custom_description = wp_strip_all_tags( strip_shortcodes( $this->get_posts_page_description( $posts_page ) ) );
184        }
185
186        if ( class_exists( 'woocommerce' ) && is_shop() ) {
187            $shop_page_id = get_option( 'woocommerce_shop_page_id' );
188            if ( $shop_page_id ) {
189                $post_custom_description = Jetpack_SEO_Posts::get_post_custom_description( get_post( $shop_page_id ) );
190            }
191        }
192
193        if ( is_front_page() && ! empty( $front_page_meta ) ) {
194            $tags['og:description'] = $front_page_meta;
195        } elseif ( ! empty( $post_custom_description ) ) {
196            $tags['og:description'] = $post_custom_description;
197        }
198
199        return $tags;
200    }
201
202    /**
203     * Outputs Jetpack's SEO <meta> tags.
204     */
205    public function meta_tags() {
206        global $wp_query;
207
208        $post_count = is_countable( $wp_query->posts ) ? count( $wp_query->posts ) : 0;
209        $period     = '';
210        $template   = '';
211        $meta       = array();
212
213        /**
214         * Can be used to specify a list of themes that set their own meta tags.
215         *
216         * If current site is using one of the themes listed as conflicting, inserting Jetpack SEO
217         * meta tags will be prevented.
218         *
219         * @module seo-tools
220         *
221         * @since 4.4.0
222         *
223         * @param array List of conflicted theme names. Defaults to empty array.
224         */
225        $conflicted_themes = apply_filters( 'jetpack_seo_meta_tags_conflicted_themes', array() );
226
227        if ( isset( $conflicted_themes[ get_option( 'template' ) ] ) ) {
228            return;
229        }
230
231        $front_page_meta     = Jetpack_SEO_Utils::get_front_page_meta_description();
232        $description         = $front_page_meta ? $front_page_meta : get_bloginfo( 'description' );
233        $meta['description'] = trim( $description );
234        $posts_page          = $this->get_viewed_posts_page();
235
236        // Try to target things if we're on a "specific" page of any kind.
237        if ( is_singular() ) {
238            if ( ! ( is_front_page() && Jetpack_SEO_Utils::get_front_page_meta_description() ) ) {
239                $description = Jetpack_SEO_Posts::get_post_description( get_post() );
240
241                if ( $description ) {
242                    $description         = wp_trim_words(
243                        strip_shortcodes(
244                            wp_strip_all_tags( $description, true )
245                        )
246                    );
247                    $meta['description'] = $description;
248                }
249            }
250        } elseif ( $posts_page ) {
251            $description = $this->get_posts_page_description( $posts_page );
252
253            if ( $description ) {
254                $meta['description'] = wp_trim_words( strip_shortcodes( wp_strip_all_tags( $description, true ) ) );
255            }
256        } elseif ( is_author() ) {
257            $obj = get_queried_object();
258
259            $meta['description'] = sprintf(
260                /* translators: first property is an user's display name, the second is the site's title. */
261                _x( 'Read all of the posts by %1$s on %2$s', 'Read all of the posts by Author Name on Blog Title', 'jetpack' ),
262                $obj->display_name ?? __( 'the author', 'jetpack' ),
263                get_bloginfo( 'title' )
264            );
265        } elseif ( is_tag() || is_category() || is_tax() ) {
266            $obj         = get_queried_object();
267            $description = '';
268
269            if ( isset( $obj->term_id ) && isset( $obj->taxonomy ) ) {
270                $description = get_term_field( 'description', $obj->term_id, $obj->taxonomy, 'raw' );
271            }
272
273            if ( ! is_wp_error( $description ) && $description ) {
274                $meta['description'] = wp_trim_words( $description );
275            } else {
276                $authors = $this->get_authors();
277
278                $meta['description'] = wp_sprintf(
279                    /* translators: %1$s: A post category. %2$l: Post authors. */
280                    _x( 'Posts about %1$s written by %2$l', 'Posts about Category written by John and Bob', 'jetpack' ),
281                    single_term_title( '', false ),
282                    $authors
283                );
284            }
285        } elseif ( is_date() ) {
286            if ( is_year() ) {
287                $period = get_query_var( 'year' );
288
289                /* translators: %1$s: Number of posts published. %2$l: Post author. %3$s: A year date. */
290                $template = _nx(
291                    '%1$s post published by %2$l in the year %3$s', // Singular.
292                    '%1$s posts published by %2$l in the year %3$s', // Plural.
293                    $post_count, // Number.
294                    '10 posts published by John in the year 2012', // Context.
295                    'jetpack'
296                );
297            } elseif ( is_month() ) {
298                $period = gmdate( 'F Y', mktime( 0, 0, 0, get_query_var( 'monthnum' ), 1, get_query_var( 'year' ) ) );
299
300                /* translators: %1$s: Number of posts published. %2$l: Post author. %3$s: A month/year date. */
301                $template = _nx(
302                    '%1$s post published by %2$l during %3$s', // Singular.
303                    '%1$s posts published by %2$l during %3$s', // Plural.
304                    $post_count, // Number.
305                    '10 posts publishes by John during May 2012', // Context.
306                    'jetpack'
307                );
308            } elseif ( is_day() ) {
309                $period = gmdate(
310                    'F j, Y',
311                    mktime( 0, 0, 0, get_query_var( 'monthnum' ), get_query_var( 'day' ), get_query_var( 'year' ) )
312                );
313
314                /* translators: %1$s: Number of posts published. %2$l: Post author. %3$s: A month/day/year date. */
315                $template = _nx(
316                    '%1$s post published by %2$l on %3$s', // Singular.
317                    '%1$s posts published by %2$l on %3$s', // Plural.
318                    $post_count, // Number.
319                    '10 posts published by John on May 30, 2012', // Context.
320                    'jetpack'
321                );
322            }
323
324            $authors             = $this->get_authors();
325            $meta['description'] = wp_sprintf( $template, $post_count, $authors, $period );
326        }
327
328        $mark_as_noindex = is_singular() && Jetpack_SEO_Posts::get_post_noindex_setting( get_post() );
329        if ( $mark_as_noindex ) {
330            $meta['robots'] = 'noindex';
331        }
332
333        /**
334         * Can be used to edit the default SEO tools meta tags.
335         *
336         * @module seo-tools
337         *
338         * @since 4.4.0
339         *
340         * @param array Array that consists of meta name and meta content pairs.
341         */
342        $meta = apply_filters( 'jetpack_seo_meta_tags', $meta );
343
344        if ( ! is_array( $meta ) ) {
345            return;
346        }
347
348        // Output them.
349        foreach ( $meta as $name => $content ) {
350            if ( ! empty( $content ) ) {
351                echo '<meta name="' . esc_attr( $name ) . '" content="' . esc_attr( $content ) . '" />' . "\n";
352            }
353        }
354    }
355}