summaryrefslogtreecommitdiff
blob: a7c2758d6379a283cc439153c30d9407d4e4de01 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
<?php

/*
 * Each title format is an array of arrays containing two values:
 *  - type
 *  - value
 *
 * Possible values for type are: 'token' and 'string'.
 * Possible values for 'value' are: any string in case that 'type' is set
 * to 'string', or allowed token values for page type in case that 'type'
 * is set to 'token'.
 *
 * Examples of valid formats:
 *
 * [
 *  'front_page' => [
 *      [ 'type' => 'string', 'value' => 'Front page title and site name:'],
 *      [ 'type' => 'token', 'value' => 'site_name']
 *  ],
 *  'posts' => [
 *      [ 'type' => 'token', 'value' => 'site_name' ],
 *      [ 'type' => 'string', 'value' => ' | ' ],
 *      [ 'type' => 'token', 'value' => 'post_title' ]
 *  ],
 *  'pages' => [],
 *  'groups' => [],
 *  'archives' => []
 * ]
 *  Custom title for given page type is created by concatenating all of the array 'value' parts.
 *  Tokens are replaced with their corresponding values for current site.
 *  Empty array signals that we are not overriding the default title for particular page type.
 */

/**
 * Class containing utility static methods for managing SEO custom title formats.
 */
class Jetpack_SEO_Titles {
	/**
	 * Site option name used to store custom title formats.
	 */
	const TITLE_FORMATS_OPTION = 'advanced_seo_title_formats';

	/**
	 * Retrieves custom title formats from site option.
	 *
	 * @return array Array of custom title formats, or empty array.
	 */
	public static function get_custom_title_formats() {
		if( Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) {
			return get_option( self::TITLE_FORMATS_OPTION, array() );
		}

		return array();
	}

	/**
	 * Returns tokens that are currently supported for each page type.
	 *
	 * @return array Array of allowed token strings.
	 */
	public static function get_allowed_tokens() {
		return array(
			'front_page' => array( 'site_name', 'tagline' ),
			'posts'      => array( 'site_name', 'tagline', 'post_title' ),
			'pages'      => array( 'site_name', 'tagline', 'page_title' ),
			'groups'     => array( 'site_name', 'tagline', 'group_title' ),
			'archives'   => array( 'site_name', 'tagline', 'date' ),
		);
	}

	/**
	 * Used to modify the default title with custom SEO title.
	 *
	 * @param string $default_title Default title for current page.
	 *
	 * @return string Custom title with replaced tokens or default title.
	 */
	public static function get_custom_title( $default_title = '' ) {
		// Don't filter title for unsupported themes.
		if ( self::is_conflicted_theme() ) {
			return $default_title;
		}

		$page_type = self::get_page_type();

		// Keep default title if invalid page type is supplied.
		if ( empty( $page_type ) ) {
			return $default_title;
		}

		$title_formats = self::get_custom_title_formats();

		// Keep default title if user has not defined custom title for this page type.
		if ( empty( $title_formats[ $page_type ] ) ) {
			return $default_title;
		}

		if ( ! Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) {
			return $default_title;
		}

		$custom_title = '';
		$format_array = $title_formats[ $page_type ];

		foreach ( $format_array as $item ) {
			if ( 'token' == $item['type'] ) {
				$custom_title .= self::get_token_value( $item['value'] );
			} else {
				$custom_title .= $item['value'];
			}
		}

		return esc_html( $custom_title );
	}

	/**
	 * Returns string value for given token.
	 *
	 * @param string $token_name The token name value that should be replaced.
	 *
	 * @return string Token replacement for current site, or empty string for unknown token name.
	 */
	public static function get_token_value( $token_name ) {

		switch ( $token_name ) {
			case 'site_name':
				return get_bloginfo( 'name' );

			case 'tagline':
				return get_bloginfo( 'description' );

			case 'post_title':
			case 'page_title':
				return the_title_attribute( array( 'echo' => false ) );

			case 'group_title':
				return single_tag_title( '', false );

			case 'date':
				return self::get_date_for_title();

			default:
				return '';
		}
	}

	/**
	 * Returns page type for current page. We need this helper in order to determine what
	 * user defined title format should be used for custom title.
	 *
	 * @return string|bool Type of current page or false if unsupported.
	 */
	public static function get_page_type() {

		if ( is_front_page() ) {
			return 'front_page';
		}

		if ( is_category() || is_tag() || is_tax() ) {
			return 'groups';
		}

		if ( is_archive() && ! is_author() ) {
			return 'archives';
		}

		if ( is_page() ) {
			return 'pages';
		}

		if ( is_singular() ) {
			return 'posts';
		}

		return false;
	}

	/**
	 * Returns the value that should be used as a replacement for the date token,
	 * depending on the archive path specified.
	 *
	 * @return string Token replacement for a given date, or empty string if no date is specified.
	 */
	public static function get_date_for_title() {
		// If archive year, month, and day are specified.
		if ( is_day() ) {
			return get_the_date();
		}

		// If archive year, and month are specified.
		if ( is_month() ) {
			return trim( single_month_title( ' ', false ) );
		}

		// Only archive year is specified.
		if ( is_year() ) {
			return get_query_var( 'year' );
		}

		return '';
	}

	/**
	 * Checks if current theme is defining custom title that won't work nicely
	 * with our custom SEO title override.
	 *
	 * @return bool True if current theme sets custom title, false otherwise.
	 */
	public static function is_conflicted_theme() {
		/**
		 * Can be used to specify a list of themes that use their own custom title format.
		 *
		 * If current site is using one of the themes listed as conflicting,
		 * Jetpack SEO custom title formats will be disabled.
		 *
		 * @module seo-tools
		 *
		 * @since 4.4.0
		 *
		 * @param array List of conflicted theme names. Defaults to empty array.
		 */
		$conflicted_themes = apply_filters( 'jetpack_seo_custom_title_conflicted_themes', array() );

		return isset( $conflicted_themes[ get_option( 'template' ) ] );
	}

	/**
	 * Checks if a given format conforms to predefined SEO title templates.
	 *
	 * Every format type and token must be whitelisted.
	 * @see get_allowed_tokens()
	 *
	 * @param array $title_formats Template of SEO title to check.
	 *
	 * @return bool True if the formats are valid, false otherwise.
	 */
	public static function are_valid_title_formats( $title_formats ) {
		$allowed_tokens = self::get_allowed_tokens();

		if ( ! is_array( $title_formats ) ) {
			return false;
		}

		foreach ( $title_formats as $format_type => $format_array ) {
			if ( ! in_array( $format_type, array_keys( $allowed_tokens ) ) ) {
				return false;
			}

			if ( '' === $format_array ) {
				continue;
			}

			if ( ! is_array( $format_array ) ) {
				return false;
			}

			foreach ( $format_array as $item ) {
				if ( empty( $item['type'] ) || empty( $item['value'] ) ) {
					return false;
				}

				if ( 'token' == $item['type'] ) {
					if ( ! in_array( $item['value'], $allowed_tokens[ $format_type ] ) ) {
						return false;
					}
				}
			}
		}

		return true;
	}

	/**
	 * Combines the previous values of title formats, stored as array in site options,
	 * with the new values that are provided.
	 *
	 * @param array $new_formats Array containing new title formats.
	 *
	 * @return array $result Array of updated title formats, or empty array if no update was performed.
	 */
	public static function update_title_formats( $new_formats ) {
		// Empty array signals that custom title shouldn't be used.
		$empty_formats = array(
			'front_page' => array(),
			'posts'      => array(),
			'pages'      => array(),
			'groups'     => array(),
			'archives'   => array(),
		);

		$previous_formats = self::get_custom_title_formats();

		$result = array_merge( $empty_formats, $previous_formats, $new_formats );

		if ( update_option( self::TITLE_FORMATS_OPTION, $result ) ) {
			return $result;
		}

		return array();
	}
}