Completed
Push — update/admin-menu-sync-wpcom ( b0ac91...43248e )
by
unknown
124:17 queued 113:08
created

Jetpack_SEO_Titles::sanitize_title_formats()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 4
nop 1
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * Each title format is an array of arrays containing two values:
5
 *  - type
6
 *  - value
7
 *
8
 * Possible values for type are: 'token' and 'string'.
9
 * Possible values for 'value' are: any string in case that 'type' is set
10
 * to 'string', or allowed token values for page type in case that 'type'
11
 * is set to 'token'.
12
 *
13
 * Examples of valid formats:
14
 *
15
 * [
16
 *  'front_page' => [
17
 *      [ 'type' => 'string', 'value' => 'Front page title and site name:'],
18
 *      [ 'type' => 'token', 'value' => 'site_name']
19
 *  ],
20
 *  'posts' => [
21
 *      [ 'type' => 'token', 'value' => 'site_name' ],
22
 *      [ 'type' => 'string', 'value' => ' | ' ],
23
 *      [ 'type' => 'token', 'value' => 'post_title' ]
24
 *  ],
25
 *  'pages' => [],
26
 *  'groups' => [],
27
 *  'archives' => []
28
 * ]
29
 *  Custom title for given page type is created by concatenating all of the array 'value' parts.
30
 *  Tokens are replaced with their corresponding values for current site.
31
 *  Empty array signals that we are not overriding the default title for particular page type.
32
 */
33
34
/**
35
 * Class containing utility static methods for managing SEO custom title formats.
36
 */
37
class Jetpack_SEO_Titles {
38
	/**
39
	 * Site option name used to store custom title formats.
40
	 */
41
	const TITLE_FORMATS_OPTION = 'advanced_seo_title_formats';
42
43
	/**
44
	 * Retrieves custom title formats from site option.
45
	 *
46
	 * @return array Array of custom title formats, or empty array.
47
	 */
48
	public static function get_custom_title_formats() {
49
		if( Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) {
50
			return get_option( self::TITLE_FORMATS_OPTION, array() );
51
		}
52
53
		return array();
54
	}
55
56
	/**
57
	 * Returns tokens that are currently supported for each page type.
58
	 *
59
	 * @return array Array of allowed token strings.
60
	 */
61
	public static function get_allowed_tokens() {
62
		return array(
63
			'front_page' => array( 'site_name', 'tagline' ),
64
			'posts'      => array( 'site_name', 'tagline', 'post_title' ),
65
			'pages'      => array( 'site_name', 'tagline', 'page_title' ),
66
			'groups'     => array( 'site_name', 'tagline', 'group_title' ),
67
			'archives'   => array( 'site_name', 'tagline', 'date' ),
68
		);
69
	}
70
71
	/**
72
	 * Used to modify the default title with custom SEO title.
73
	 *
74
	 * @param string $default_title Default title for current page.
75
	 *
76
	 * @return string Custom title with replaced tokens or default title.
77
	 */
78
	public static function get_custom_title( $default_title = '' ) {
79
		// Don't filter title for unsupported themes.
80
		if ( self::is_conflicted_theme() ) {
81
			return $default_title;
82
		}
83
84
		$page_type = self::get_page_type();
85
86
		// Keep default title if invalid page type is supplied.
87
		if ( empty( $page_type ) ) {
88
			return $default_title;
89
		}
90
91
		$title_formats = self::get_custom_title_formats();
92
93
		// Keep default title if user has not defined custom title for this page type.
94
		if ( empty( $title_formats[ $page_type ] ) ) {
95
			return $default_title;
96
		}
97
98
		if ( ! Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) {
99
			return $default_title;
100
		}
101
102
		$custom_title = '';
103
		$format_array = $title_formats[ $page_type ];
104
105
		foreach ( $format_array as $item ) {
106
			if ( 'token' == $item['type'] ) {
107
				$custom_title .= self::get_token_value( $item['value'] );
108
			} else {
109
				$custom_title .= $item['value'];
110
			}
111
		}
112
113
		return esc_html( $custom_title );
114
	}
115
116
	/**
117
	 * Returns string value for given token.
118
	 *
119
	 * @param string $token_name The token name value that should be replaced.
120
	 *
121
	 * @return string Token replacement for current site, or empty string for unknown token name.
122
	 */
123
	public static function get_token_value( $token_name ) {
124
125
		switch ( $token_name ) {
126
			case 'site_name':
127
				return get_bloginfo( 'name' );
128
129
			case 'tagline':
130
				return get_bloginfo( 'description' );
131
132
			case 'post_title':
133
			case 'page_title':
134
				return the_title_attribute( array( 'echo' => false ) );
135
136
			case 'group_title':
137
				return single_tag_title( '', false );
138
139
			case 'date':
140
				return self::get_date_for_title();
141
142
			default:
143
				return '';
144
		}
145
	}
146
147
	/**
148
	 * Returns page type for current page. We need this helper in order to determine what
149
	 * user defined title format should be used for custom title.
150
	 *
151
	 * @return string|bool Type of current page or false if unsupported.
152
	 */
153
	public static function get_page_type() {
154
155
		if ( is_front_page() ) {
156
			return 'front_page';
157
		}
158
159
		if ( is_category() || is_tag() || is_tax() ) {
160
			return 'groups';
161
		}
162
163
		if ( is_archive() && ! is_author() ) {
164
			return 'archives';
165
		}
166
167
		if ( is_page() ) {
168
			return 'pages';
169
		}
170
171
		if ( is_singular() ) {
172
			return 'posts';
173
		}
174
175
		return false;
176
	}
177
178
	/**
179
	 * Returns the value that should be used as a replacement for the date token,
180
	 * depending on the archive path specified.
181
	 *
182
	 * @return string Token replacement for a given date, or empty string if no date is specified.
183
	 */
184
	public static function get_date_for_title() {
185
		// If archive year, month, and day are specified.
186
		if ( is_day() ) {
187
			return get_the_date();
188
		}
189
190
		// If archive year, and month are specified.
191
		if ( is_month() ) {
192
			return trim( single_month_title( ' ', false ) );
193
		}
194
195
		// Only archive year is specified.
196
		if ( is_year() ) {
197
			return get_query_var( 'year' );
198
		}
199
200
		return '';
201
	}
202
203
	/**
204
	 * Checks if current theme is defining custom title that won't work nicely
205
	 * with our custom SEO title override.
206
	 *
207
	 * @return bool True if current theme sets custom title, false otherwise.
208
	 */
209
	public static function is_conflicted_theme() {
210
		/**
211
		 * Can be used to specify a list of themes that use their own custom title format.
212
		 *
213
		 * If current site is using one of the themes listed as conflicting,
214
		 * Jetpack SEO custom title formats will be disabled.
215
		 *
216
		 * @module seo-tools
217
		 *
218
		 * @since 4.4.0
219
		 *
220
		 * @param array List of conflicted theme names. Defaults to empty array.
221
		 */
222
		$conflicted_themes = apply_filters( 'jetpack_seo_custom_title_conflicted_themes', array() );
223
224
		return isset( $conflicted_themes[ get_option( 'template' ) ] );
225
	}
226
227
	/**
228
	 * Checks if a given format conforms to predefined SEO title templates.
229
	 *
230
	 * Every format type and token must be specifically allowed..
231
	 * @see get_allowed_tokens()
232
	 *
233
	 * @param array $title_formats Template of SEO title to check.
234
	 *
235
	 * @return bool True if the formats are valid, false otherwise.
236
	 */
237
	public static function are_valid_title_formats( $title_formats ) {
238
		$allowed_tokens = self::get_allowed_tokens();
239
240
		if ( ! is_array( $title_formats ) ) {
241
			return false;
242
		}
243
244
		foreach ( $title_formats as $format_type => $format_array ) {
245
			if ( ! in_array( $format_type, array_keys( $allowed_tokens ) ) ) {
246
				return false;
247
			}
248
249
			if ( '' === $format_array ) {
250
				continue;
251
			}
252
253
			if ( ! is_array( $format_array ) ) {
254
				return false;
255
			}
256
257
			foreach ( $format_array as $item ) {
258
				if ( empty( $item['type'] ) || empty( $item['value'] ) ) {
259
					return false;
260
				}
261
262
				if ( 'token' == $item['type'] ) {
263
					if ( ! in_array( $item['value'], $allowed_tokens[ $format_type ] ) ) {
264
						return false;
265
					}
266
				}
267
			}
268
		}
269
270
		return true;
271
	}
272
273
	/**
274
	 * Sanitizes the arbitrary user input strings for custom SEO titles.
275
	 *
276
	 * @param array $title_formats Array of custom title formats.
277
	 *
278
	 * @return array The sanitized array.
279
	 */
280
	public static function sanitize_title_formats( $title_formats ) {
281
		foreach ( $title_formats as &$format_array ) {
282
			foreach ( $format_array as &$item ) {
283
				if ( 'string' === $item['type'] ) {
284
					// Using esc_html() vs sanitize_text_field() since we want to preserve extra spacing around items.
285
					$item['value'] = esc_html( $item['value'] );
286
				}
287
			}
288
		}
289
		unset( $format_array );
290
		unset( $item );
291
292
		return $title_formats;
293
	}
294
295
	/**
296
	 * Combines the previous values of title formats, stored as array in site options,
297
	 * with the new values that are provided.
298
	 *
299
	 * @param array $new_formats Array containing new title formats.
300
	 *
301
	 * @return array $result Array of updated title formats, or empty array if no update was performed.
302
	 */
303
	public static function update_title_formats( $new_formats ) {
304
		$new_formats = self::sanitize_title_formats( $new_formats );
305
306
		// Empty array signals that custom title shouldn't be used.
307
		$empty_formats = array(
308
			'front_page' => array(),
309
			'posts'      => array(),
310
			'pages'      => array(),
311
			'groups'     => array(),
312
			'archives'   => array(),
313
		);
314
315
		$previous_formats = self::get_custom_title_formats();
316
317
		$result = array_merge( $empty_formats, $previous_formats, $new_formats );
318
319
		if ( update_option( self::TITLE_FORMATS_OPTION, $result ) ) {
320
			return $result;
321
		}
322
323
		return array();
324
	}
325
}
326