PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.5
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 13.8.3 All 506 releases
jetpack / modules / seo-tools / class-jetpack-seo-titles.php

class-jetpack-seo-titles.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.5, at modules/seo-tools/class-jetpack-seo-titles.php

367 lines 10.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 }
367