PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 All 507 releases
← All changes | modules/seo-tools/class-jetpack-seo-titles.php +366 -0 16.2-beta → 16.3-beta View file →
@@ -1,0 +1,366 @@
1 +<?php
2 +/**
3 + * Class containing utility static methods for managing SEO custom title formats.
4 + *
5 + * @package automattic/jetpack
6 + */
7 +
8 +/*
9 + * Each title format is an array of arrays containing two values:
10 + * - type
11 + * - value
12 + *
13 + * Possible values for type are: 'token' and 'string'.
14 + * Possible values for 'value' are: any string in case that 'type' is set
15 + * to 'string', or allowed token values for page type in case that 'type'
16 + * is set to 'token'.
17 + *
18 + * Examples of valid formats:
19 + *
20 + * [
21 + * 'front_page' => [
22 + * [ 'type' => 'string', 'value' => 'Front page title and site name:'],
23 + * [ 'type' => 'token', 'value' => 'site_name']
24 + * ],
25 + * 'posts' => [
26 + * [ 'type' => 'token', 'value' => 'site_name' ],
27 + * [ 'type' => 'string', 'value' => ' | ' ],
28 + * [ 'type' => 'token', 'value' => 'post_title' ]
29 + * ],
30 + * 'pages' => [],
31 + * 'groups' => [],
32 + * 'archives' => []
33 + * ]
34 + * Custom title for given page type is created by concatenating all of the array 'value' parts.
35 + * Tokens are replaced with their corresponding values for current site.
36 + * Empty array signals that we are not overriding the default title for particular page type.
37 + */
38 +
39 +/**
40 + * Class containing utility static methods for managing SEO custom title formats.
41 + */
42 +class Jetpack_SEO_Titles {
43 + /**
44 + * Site option name used to store custom title formats.
45 + */
46 + const TITLE_FORMATS_OPTION = 'advanced_seo_title_formats';
47 +
48 + /**
49 + * Retrieves custom title formats from site option.
50 + *
51 + * @return array Array of custom title formats, or empty array.
52 + */
53 + public static function get_custom_title_formats() {
54 + if ( Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) {
55 + return get_option( self::TITLE_FORMATS_OPTION, array() );
56 + }
57 +
58 + return array();
59 + }
60 +
61 + /**
62 + * Returns tokens that are currently supported for each page type.
63 + *
64 + * @return array Array of allowed token strings.
65 + */
66 + public static function get_allowed_tokens() {
67 + return array(
68 + 'front_page' => array( 'site_name', 'tagline' ),
69 + 'posts' => array( 'site_name', 'tagline', 'post_title' ),
70 + 'pages' => array( 'site_name', 'tagline', 'page_title' ),
71 + 'groups' => array( 'site_name', 'tagline', 'group_title' ),
72 + 'archives' => array( 'site_name', 'tagline', 'date', 'archive_title' ),
73 + );
74 + }
75 +
76 + /**
77 + * Used to modify the default title with custom SEO title.
78 + *
79 + * @param string $default_title Default title for current page.
80 + *
81 + * @return string A custom per-post title, custom title structure with replaced tokens, or default title.
82 + */
83 + public static function get_custom_title( $default_title = '' ) {
84 + // Don't filter title for unsupported themes.
85 + if ( self::is_conflicted_theme() ) {
86 + return $default_title;
87 + }
88 +
89 + $page_type = self::get_page_type();
90 +
91 + // Keep default title if invalid page type is supplied.
92 + if ( empty( $page_type ) ) {
93 + return $default_title;
94 + }
95 +
96 + if ( ! Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) {
97 + return $default_title;
98 + }
99 +
100 + // If it's a singular -- page or post -- check for a meta title override. Also
101 + // check a static front page, where get_post() returns that page. On a latest-posts
102 + // homepage it would instead return the first post in the loop, letting the newest
103 + // post's SEO title hijack the homepage title.
104 + $check_post_meta = 'pages' === $page_type
105 + || 'posts' === $page_type
106 + || ( 'front_page' === $page_type && 'page' === get_option( 'show_on_front' ) );
107 +
108 + if ( $check_post_meta ) {
109 + $post = get_post();
110 + if ( $post instanceof WP_Post ) {
111 + $custom_title = get_post_meta( $post->ID, Jetpack_SEO_Posts::HTML_TITLE_META_KEY, true );
112 + if ( ! empty( trim( $custom_title ) ) ) {
113 + return esc_html( $custom_title );
114 + }
115 + }
116 + }
117 +
118 + $title_formats = self::get_custom_title_formats();
119 +
120 + // Keep default title if user has not defined custom title for this page type.
121 + if ( empty( $title_formats[ $page_type ] ) ) {
122 + return $default_title;
123 + }
124 +
125 + $custom_title = '';
126 + $format_array = $title_formats[ $page_type ];
127 +
128 + foreach ( $format_array as $item ) {
129 + if ( 'token' === $item['type'] ) {
130 + $custom_title .= self::get_token_value( $item['value'] );
131 + } else {
132 + $custom_title .= $item['value'];
133 + }
134 + }
135 +
136 + return esc_html( $custom_title );
137 + }
138 +
139 + /**
140 + * Returns string value for given token.
141 + *
142 + * @param string $token_name The token name value that should be replaced.
143 + *
144 + * @return string Token replacement for current site, or empty string for unknown token name.
145 + */
146 + public static function get_token_value( $token_name ) {
147 +
148 + switch ( $token_name ) {
149 + case 'site_name':
150 + return get_bloginfo( 'name' );
151 +
152 + case 'tagline':
153 + return get_bloginfo( 'description' );
154 +
155 + case 'post_title':
156 + case 'page_title':
157 + return the_title_attribute( array( 'echo' => false ) );
158 +
159 + case 'group_title':
160 + return single_tag_title( '', false );
161 +
162 + case 'date':
163 + case 'archive_title':
164 + return self::get_archive_title();
165 +
166 + default:
167 + return '';
168 + }
169 + }
170 +
171 + /**
172 + * Returns page type for current page. We need this helper in order to determine what
173 + * user defined title format should be used for custom title.
174 + *
175 + * @return string|bool Type of current page or false if unsupported.
176 + */
177 + public static function get_page_type() {
178 +
179 + if ( is_front_page() ) {
180 + return 'front_page';
181 + }
182 +
183 + if ( is_category() || is_tag() || is_tax() ) {
184 + return 'groups';
185 + }
186 +
187 + if ( is_archive() && ! is_author() ) {
188 + return 'archives';
189 + }
190 +
191 + if ( is_page() ) {
192 + return 'pages';
193 + }
194 +
195 + if ( is_singular() ) {
196 + return 'posts';
197 + }
198 +
199 + return false;
200 + }
201 +
202 + /**
203 + * Returns the value that should be used as a replacement for the `date` or `archive_title` tokens.
204 + * For date-based archives, a date is returned. Otherwise the `post_type_archive_title` is returned.
205 + *
206 + * The `archive_title` token was added after the `date` token to provide a more generic option
207 + * that would work for non date-based archives.
208 + *
209 + * @return string Token replaced string.
210 + */
211 + public static function get_archive_title() {
212 + // If archive year, month, and day are specified.
213 + if ( is_day() ) {
214 + return get_the_date();
215 + }
216 +
217 + // If archive year, and month are specified.
218 + if ( is_month() ) {
219 + return trim( single_month_title( ' ', false ) );
220 + }
221 +
222 + // Only archive year is specified.
223 + if ( is_year() ) {
224 + return get_query_var( 'year' );
225 + }
226 +
227 + // Not a date based archive.
228 + // An example would be "Projects" for Jetpack's Portoflio CPT.
229 + return post_type_archive_title( '', false );
230 + }
231 +
232 + /**
233 + * Checks if current theme is defining custom title that won't work nicely
234 + * with our custom SEO title override.
235 + *
236 + * @return bool True if current theme sets custom title, false otherwise.
237 + */
238 + public static function is_conflicted_theme() {
239 + /**
240 + * Can be used to specify a list of themes that use their own custom title format.
241 + *
242 + * If current site is using one of the themes listed as conflicting,
243 + * Jetpack SEO custom title formats will be disabled.
244 + *
245 + * @module seo-tools
246 + *
247 + * @since 4.4.0
248 + *
249 + * @param array List of conflicted theme names. Defaults to empty array.
250 + */
251 + $conflicted_themes = apply_filters( 'jetpack_seo_custom_title_conflicted_themes', array() );
252 +
253 + return isset( $conflicted_themes[ get_option( 'template' ) ] );
254 + }
255 +
256 + /**
257 + * Checks if a given format conforms to predefined SEO title templates.
258 + *
259 + * Every format type and token must be specifically allowed.
260 + *
261 + * @see get_allowed_tokens()
262 + *
263 + * @param array $title_formats Template of SEO title to check.
264 + *
265 + * @return bool True if the formats are valid, false otherwise.
266 + */
267 + public static function are_valid_title_formats( $title_formats ) {
268 + $allowed_tokens = self::get_allowed_tokens();
269 +
270 + if ( ! is_array( $title_formats ) ) {
271 + return false;
272 + }
273 +
274 + foreach ( $title_formats as $format_type => $format_array ) {
275 + if ( ! array_key_exists( $format_type, $allowed_tokens ) ) {
276 + return false;
277 + }
278 +
279 + if ( '' === $format_array ) {
280 + continue;
281 + }
282 +
283 + if ( ! is_array( $format_array ) ) {
284 + return false;
285 + }
286 +
287 + foreach ( $format_array as $item ) {
288 + if ( empty( $item['type'] ) || empty( $item['value'] ) ) {
289 + return false;
290 + }
291 +
292 + if ( 'token' === $item['type'] ) {
293 + if ( ! in_array( $item['value'], $allowed_tokens[ $format_type ], true ) ) {
294 + return false;
295 + }
296 + }
297 + }
298 + }
299 +
300 + return true;
301 + }
302 +
303 + /**
304 + * Sanitizes the arbitrary user input strings for custom SEO titles.
305 + *
306 + * @param array $title_formats Array of custom title formats.
307 + *
308 + * @return array The sanitized array.
309 + */
310 + public static function sanitize_title_formats( $title_formats ) {
311 + foreach ( $title_formats as &$format_array ) {
312 + // The API accepts an empty string as "clear this page type"; store it as
313 + // the empty list every reader expects, and don't iterate a string.
314 + if ( ! is_array( $format_array ) ) {
315 + $format_array = array();
316 + continue;
317 + }
318 + foreach ( $format_array as &$item ) {
319 + if ( 'string' === $item['type'] ) {
320 + // From `wp_strip_all_tags`, but omitting the `trim` portion since we want spacing preserved.
321 + $item['value'] = preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $item['value'] );
322 + $item['value'] = strip_tags( $item['value'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags
323 + $item['value'] = preg_replace( '/[\r\n\t ]+/', ' ', $item['value'] );
324 + }
325 + }
326 + }
327 + unset( $format_array );
328 + unset( $item );
329 +
330 + return $title_formats;
331 + }
332 +
333 + /**
334 + * Combines the previous values of title formats, stored as array in site options,
335 + * with the new values that are provided.
336 + *
337 + * @param array $new_formats Array containing new title formats.
338 + *
339 + * @return array $result Array of updated title formats, or empty array if no update was performed.
340 + */
341 + public static function update_title_formats( $new_formats ) {
342 + $new_formats = self::sanitize_title_formats( $new_formats );
343 +
344 + // Empty array signals that custom title shouldn't be used.
345 + $empty_formats = array(
346 + 'front_page' => array(),
347 + 'posts' => array(),
348 + 'pages' => array(),
349 + 'groups' => array(),
350 + 'archives' => array(),
351 + );
352 +
353 + // Sanitize the stored formats too: a page type saved as '' before this guard
354 + // existed would otherwise survive every partial save from Calypso.
355 + $previous_formats = self::get_custom_title_formats();
356 + $previous_formats = is_array( $previous_formats ) ? self::sanitize_title_formats( $previous_formats ) : array();
357 +
358 + $result = array_merge( $empty_formats, $previous_formats, $new_formats );
359 +
360 + if ( update_option( self::TITLE_FORMATS_OPTION, $result ) ) {
361 + return $result;
362 + }
363 +
364 + return array();
365 + }
366 +}