Няма описание

jetpack-seo-titles.php 9.0KB

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