PluginProbe
Polylang / 3.8.6
Polylang v3.8.6
3.8.9 3.8.8 3.8.7 3.8.6 3.8.5 3.8.4 3.8.3 2.7 2.7.0.1 2.7.1 2.7.2 2.7.3 2.7.4 2.8 2.8.1 2.8.2 2.8.3 2.8.4 2.9 2.9.1 2.9.2 3.0 3.0.1 3.0.2 3.0.3 All 233 releases
polylang / src / integrations / wpseo / wpseo.php

wpseo.php in Polylang 3.8.6, at src/integrations/wpseo/wpseo.php

518 lines 16.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package Polylang
4 */
5
6 /**
7 * Manages the compatibility with Yoast SEO
8 * Version tested: 15.9.2
9 *
10 * @since 2.3
11 */
12 class PLL_WPSEO {
13 /**
14 * Translate options and add specific filters and actions
15 *
16 * @since 1.6.4
17 */
18 public function init() {
19 add_action( 'wp_loaded', array( $this, 'wpseo_translate_options' ) );
20 add_filter( 'wpseo_dynamic_permalinks_enabled', '__return_true', 999 );
21
22 if ( PLL() instanceof PLL_Frontend ) {
23 // Filters sitemap queries to remove inactive language or to get
24 // one sitemap per language when using multiple domains or subdomains
25 // because WPSEO does not accept several domains or subdomains in one sitemap
26 add_filter( 'wpseo_posts_join', array( $this, 'wpseo_posts_join' ), 10, 2 );
27 add_filter( 'wpseo_posts_where', array( $this, 'wpseo_posts_where' ), 10, 2 );
28 add_filter( 'wpseo_typecount_join', array( $this, 'wpseo_posts_join' ), 10, 2 );
29 add_filter( 'wpseo_typecount_where', array( $this, 'wpseo_posts_where' ), 10, 2 );
30
31 if ( PLL()->options['force_lang'] > 1 ) {
32 add_filter( 'wpseo_enable_xml_sitemap_transient_caching', '__return_false' ); // Disable cache! otherwise WPSEO keeps only one domain (thanks to Junaid Bhura)
33 add_filter( 'home_url', array( $this, 'wpseo_home_url' ), 10, 2 ); // Fix home_url
34 add_action( 'setup_theme', array( $this, 'maybe_deactivate_sitemap' ) ); // Deactivate sitemaps for inactive languages.
35 } else {
36 // Get all terms in all languages when the language is set from the content or directory name
37 add_filter( 'get_terms_args', array( $this, 'wpseo_remove_terms_filter' ) );
38 add_action( 'pre_get_posts', array( $this, 'before_sitemap' ), 0 ); // Needs to be fired before WPSEO_Sitemaps::redirect()
39 }
40
41 add_filter( 'pll_home_url_white_list', array( $this, 'wpseo_home_url_white_list' ) );
42 add_filter( 'wpseo_frontend_presenters', array( $this, 'wpseo_frontend_presenters' ) );
43 add_filter( 'wpseo_canonical', array( $this, 'wpseo_canonical' ) );
44 add_filter( 'wpseo_frontend_presentation', array( $this, 'frontend_presentation' ) );
45 add_filter( 'wpseo_breadcrumb_indexables', array( $this, 'breadcrumb_indexables' ) );
46 } else {
47 add_filter( 'pll_copy_post_metas', array( $this, 'copy_post_metas' ), 10, 4 );
48 add_filter( 'pll_translate_post_meta', array( $this, 'translate_post_meta' ), 10, 3 );
49 add_filter( 'pll_post_metas_to_export', array( $this, 'export_post_metas' ) );
50
51 // Yoast SEO adds the columns hooks only for the 'inline-save' action. We need them for 'pll_update_post_rows' too.
52 if ( wp_doing_ajax() && isset( $_POST['action'] ) && 'pll_update_post_rows' === $_POST['action'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
53 $GLOBALS['wpseo_meta_columns'] = new WPSEO_Meta_Columns();
54 }
55 }
56 }
57
58 /**
59 * Registers options for translation.
60 *
61 * @since 2.9
62 */
63 public function wpseo_translate_options() {
64 if ( method_exists( 'WPSEO_Options', 'clear_cache' ) ) {
65 WPSEO_Options::clear_cache();
66 }
67
68 $keys = array(
69 'title-*',
70 'metadesc-*',
71 'bctitle-*',
72 'breadcrumbs-sep',
73 'breadcrumbs-home',
74 'breadcrumbs-prefix',
75 'breadcrumbs-archiveprefix',
76 'breadcrumbs-searchprefix',
77 'breadcrumbs-404crumb',
78 'company_name',
79 'rssbefore',
80 'rssafter',
81 'social-title-*',
82 'social-description-*',
83 );
84
85 new PLL_Translate_Option( 'wpseo_titles', array_fill_keys( $keys, 1 ), array( 'context' => 'wordpress-seo' ) );
86
87 $keys = array(
88 'og_frontpage_title',
89 'og_frontpage_desc',
90 );
91
92 new PLL_Translate_Option( 'wpseo_social', array_fill_keys( $keys, 1 ), array( 'context' => 'wordpress-seo' ) );
93 }
94
95 /**
96 * Fixes the home url as well as the stylesheet url,
97 * only when using multiple domains or subdomains.
98 *
99 * @since 1.6.4
100 *
101 * @param string $url The complete URL including scheme and path.
102 * @param string $path Path relative to the home URL.
103 * @return $url
104 */
105 public function wpseo_home_url( $url, $path ) {
106 if ( empty( $path ) ) {
107 $path = ltrim( (string) wp_parse_url( pll_get_requested_url(), PHP_URL_PATH ), '/' );
108 }
109
110 if ( preg_match( '#sitemap(_index)?\.xml|([^\/]+?)-?sitemap([0-9]+)?\.xml|([a-z]+)?-?sitemap\.xsl#', $path ) ) {
111 $url = PLL()->links_model->switch_language_in_link( $url, PLL()->curlang );
112 }
113
114 return $url;
115 }
116
117 /**
118 * Get active languages for the sitemaps
119 *
120 * @since 2.0
121 *
122 * @return array list of active language slugs, empty if all languages are active
123 */
124 protected function wpseo_get_active_languages() {
125 $languages = PLL()->model->get_languages_list();
126 if ( wp_list_filter( $languages, array( 'active' => false ) ) ) {
127 return wp_list_pluck( wp_list_filter( $languages, array( 'active' => false ), 'NOT' ), 'slug' );
128 }
129 return array();
130 }
131
132 /**
133 * Modifies the sql request for posts sitemaps.
134 *
135 * @since 1.6.4
136 *
137 * @param string $sql JOIN clause.
138 * @param string $post_type Post type.
139 * @return string
140 */
141 public function wpseo_posts_join( $sql, $post_type ) {
142 return pll_is_translated_post_type( $post_type ) ? $sql . PLL()->model->post->join_clause() : $sql;
143 }
144
145 /**
146 * Modifies the sql request for posts sitemaps.
147 *
148 * @since 1.6.4
149 *
150 * @param string $sql WHERE clause.
151 * @param string $post_type Post type.
152 * @return string
153 */
154 public function wpseo_posts_where( $sql, $post_type ) {
155 if ( ! pll_is_translated_post_type( $post_type ) ) {
156 return $sql;
157 }
158
159 if ( PLL()->options['force_lang'] > 1 && PLL()->curlang instanceof PLL_Language ) {
160 return $sql . PLL()->model->post->where_clause( PLL()->curlang );
161 }
162
163 $languages = $this->wpseo_get_active_languages();
164
165 if ( empty( $languages ) ) { // Empty when all languages are active.
166 $languages = pll_languages_list();
167 }
168
169 return $sql . PLL()->model->post->where_clause( $languages );
170 }
171
172 /**
173 * Removes the language filter (and remove inactive languages) for the taxonomy sitemaps
174 * Only when the language is set from the content or directory name
175 *
176 * @since 1.0.3
177 *
178 * @param array $args get_terms arguments
179 * @return array modified list of arguments
180 */
181 public function wpseo_remove_terms_filter( $args ) {
182 if ( isset( $GLOBALS['wp_query']->query['sitemap'] ) ) {
183 $args['lang'] = implode( ',', $this->wpseo_get_active_languages() );
184 }
185 return $args;
186 }
187
188 /**
189 * Deactivates the sitemap for inactive languages when using subdomains or multiple domains
190 *
191 * @since 2.6.1
192 */
193 public function maybe_deactivate_sitemap() {
194 global $wpseo_sitemaps;
195
196 if ( isset( $wpseo_sitemaps ) ) {
197 $active_languages = $this->wpseo_get_active_languages();
198 if ( ! empty( $active_languages ) && ! in_array( pll_current_language(), $active_languages ) ) {
199 remove_action( 'pre_get_posts', array( $wpseo_sitemaps, 'redirect' ), 1 );
200 }
201 }
202 }
203
204 /**
205 * Add filters before the sitemap is evaluated and outputted.
206 *
207 * @since 2.6
208 *
209 * @param WP_Query $query Instance of WP_Query being filtered.
210 */
211 public function before_sitemap( $query ) {
212 $type = $query->get( 'sitemap' );
213
214 // Add the post post type archives in all languages to the sitemap
215 // Add the homepages for all languages to the sitemap when the front page displays posts
216 if ( $type && pll_is_translated_post_type( $type ) && ( 'post' !== $type || ! get_option( 'page_on_front' ) ) ) {
217 add_filter( "wpseo_sitemap_{$type}_content", array( $this, 'add_post_type_archive' ) );
218 }
219 }
220
221 /**
222 * Generates a post type archive sitemap url
223 *
224 * @since 2.6.1
225 *
226 * @param string $link The url.
227 * @param string $post_type The post type name.
228 * @return string Formatted sitemap url.
229 */
230 protected function format_sitemap_url( $link, $post_type ) {
231 global $wpseo_sitemaps;
232
233 return $wpseo_sitemaps->renderer->sitemap_url(
234 array(
235 'loc' => $link,
236 'mod' => WPSEO_Sitemaps::get_last_modified_gmt( $post_type ),
237 'pri' => 1,
238 'chf' => 'daily',
239 )
240 );
241 }
242
243 /**
244 * Adds the home and post type archives urls for all (active) languages to the sitemap
245 *
246 * @since 2.6
247 *
248 * @param string $str additional urls to sitemap post
249 * @return string
250 */
251 public function add_post_type_archive( $str ) {
252 $post_type = substr( substr( current_filter(), 14 ), 0, -8 );
253 $post_type_obj = get_post_type_object( $post_type );
254 $languages = wp_list_filter( PLL()->model->get_languages_list(), array( 'active' => false ), 'NOT' );
255
256 if ( 'post' === $post_type ) {
257 if ( ! empty( PLL()->options['hide_default'] ) ) {
258 // The home url is of course already added by WPSEO.
259 $languages = wp_list_filter( $languages, array( 'slug' => pll_default_language() ), 'NOT' );
260 }
261
262 foreach ( $languages as $lang ) {
263 $str .= $this->format_sitemap_url( pll_home_url( $lang->slug ), $post_type );
264 }
265 } elseif ( $post_type_obj->has_archive ) {
266 // Exclude cases where a post type archive is attached to a page (ex: WooCommerce).
267 $slug = ( true === $post_type_obj->has_archive ) ? $post_type_obj->rewrite['slug'] : $post_type_obj->has_archive;
268
269 if ( ! wpcom_vip_get_page_by_path( $slug ) ) {
270 // The post type archive in the current language is already added by WPSEO.
271 $languages = wp_list_filter( $languages, array( 'slug' => pll_current_language() ), 'NOT' );
272
273 foreach ( $languages as $lang ) {
274 PLL()->curlang = $lang; // Switch the language to get the correct archive link.
275 $link = get_post_type_archive_link( $post_type );
276 $str .= $this->format_sitemap_url( $link, $post_type );
277 }
278 }
279 }
280
281 return $str;
282 }
283
284 /**
285 * Filters the home url.
286 *
287 * @since 1.1.2
288 *
289 * @param array $arr The list of files or functions for which `home_url()` must be filtered.
290 * @return array
291 */
292 public function wpseo_home_url_white_list( $arr ) {
293 return array_merge( $arr, array( array( 'file' => 'wordpress-seo' ) ) );
294 }
295
296 /**
297 * Get alternate language codes for Opengraph.
298 *
299 * @since 2.7.3
300 *
301 * @return string[]
302 */
303 protected function get_ogp_alternate_languages() {
304 $alternates = array();
305
306 foreach ( PLL()->model->get_languages_list() as $language ) {
307 if ( isset( PLL()->curlang ) && PLL()->curlang->slug !== $language->slug && PLL()->links->get_translation_url( $language ) && isset( $language->facebook ) ) {
308 $alternates[] = $language->facebook;
309 }
310 }
311
312 // There is a risk that 2 languages have the same Facebook locale. So let's make sure to output each locale only once.
313 return array_unique( $alternates );
314 }
315
316 /**
317 * Adds opengraph support for translations
318 *
319 * @since 2.7.3
320 *
321 * @param array $presenters An array of objects implementing Abstract_Indexable_Presenter
322 * @return array
323 */
324 public function wpseo_frontend_presenters( $presenters ) {
325 $_presenters = array();
326
327 foreach ( $presenters as $presenter ) {
328 $_presenters[] = $presenter;
329 if ( $presenter instanceof Yoast\WP\SEO\Presenters\Open_Graph\Locale_Presenter ) {
330 foreach ( $this->get_ogp_alternate_languages() as $lang ) {
331 $_presenters[] = new PLL_WPSEO_OGP( $lang );
332 }
333 }
334 }
335 return $_presenters;
336 }
337
338 /**
339 * Fixes the canonical front page url as unlike WP, WPSEO does not add a trailing slash to the canonical front page url.
340 *
341 * @since 1.7.10
342 *
343 * @param string $url The canonical URL evaluated by Yoast SEO.
344 * @return $url
345 */
346 public function wpseo_canonical( $url ) {
347 return is_front_page( $url ) && get_option( 'permalink_structure' ) ? trailingslashit( $url ) : $url;
348 }
349
350 /**
351 * Fixes the links and strings stored in the indexable table since Yoast SEO 14.0
352 *
353 * @since 2.8.2
354 *
355 * @param object $presentation The indexable presentation.
356 * @return object
357 */
358 public function frontend_presentation( $presentation ) {
359 switch ( $presentation->model->object_type ) {
360 case 'home-page':
361 $presentation->model->title = WPSEO_Options::get( 'title-home-wpseo' );
362 $presentation->model->description = WPSEO_Options::get( 'metadesc-home-wpseo' );
363 $presentation->model->open_graph_title = WPSEO_Options::get( 'og_frontpage_title' );
364 $presentation->model->open_graph_description = WPSEO_Options::get( 'og_frontpage_desc' );
365 break;
366
367 case 'post-type-archive':
368 if ( pll_is_translated_post_type( $presentation->model->object_sub_type ) ) {
369 $presentation->model->title = WPSEO_Options::get( 'title-ptarchive-' . $presentation->model->object_sub_type );
370 $presentation->model->description = WPSEO_Options::get( 'metadesc-ptarchive-' . $presentation->model->object_sub_type );
371 }
372 break;
373
374 case 'system-page':
375 switch ( $presentation->model->object_sub_type ) {
376 case '404':
377 $presentation->model->title = WPSEO_Options::get( 'title-404-wpseo' );
378 break;
379 case 'search-result':
380 $presentation->model->title = WPSEO_Options::get( 'title-search-wpseo' );
381 break;
382 }
383 break;
384 }
385
386 return $presentation;
387 }
388
389 /**
390 * Fixes the breadcrumb links and strings stored in the indexable table since Yoast SEO 14.0.
391 *
392 * In version 17.0, the breadcrumb links do not honor the filter `wpseo_dynamic_permalinks_enabled`.
393 *
394 * @since 2.8.3
395 *
396 * @param array $indexables An array of Indexable objects.
397 * @return array
398 */
399 public function breadcrumb_indexables( $indexables ) {
400 foreach ( $indexables as &$indexable ) {
401 if ( 'home-page' === $indexable->object_type || ( 'post' === $indexable->object_type && 'page' === $indexable->object_sub_type && get_option( 'page_on_front' ) === $indexable->object_id ) ) {
402 // Handles both when the front page displays the list of posts or a static page.
403 $indexable->permalink = pll_home_url();
404 $indexable->breadcrumb_title = pll__( WPSEO_Options::get( 'breadcrumbs-home' ) );
405 } elseif ( 'post' === $indexable->object_type && 'page' === $indexable->object_sub_type && get_option( 'page_for_posts' ) === $indexable->object_id ) {
406 $indexable->permalink = get_permalink( $indexable->object_id );
407 } elseif ( 'post-type-archive' === $indexable->object_type && pll_is_translated_post_type( $indexable->object_sub_type ) ) {
408 $indexable->permalink = get_post_type_archive_link( $indexable->object_sub_type );
409 $breadcrumb_title = WPSEO_Options::get( 'bctitle-ptarchive-' . $indexable->object_sub_type );
410 $breadcrumb_title = $breadcrumb_title ?: $indexable->breadcrumb_title; // The option may be empty.
411 $indexable->breadcrumb_title = pll__( $breadcrumb_title );
412 } elseif ( 'term' === $indexable->object_type && pll_is_translated_taxonomy( $indexable->object_sub_type ) ) {
413 $indexable->permalink = get_term_link( $indexable->object_id );
414 }
415 }
416
417 return $indexables;
418 }
419
420 /**
421 * Copies or synchronizes the metas.
422 *
423 * @since 2.3.3
424 *
425 * @param string[] $keys List of custom fields names.
426 * @param bool $sync True if it is synchronization, false if it is a copy.
427 * @param int $from Id of the post from which we copy information.
428 * @param int $to Id of the post to which we paste information.
429 * @return array
430 */
431 public function copy_post_metas( $keys, $sync, $from, $to ) {
432 if ( ! $sync ) {
433 // Text requiring translation.
434 $keys = array_merge( $keys, $this->get_translatable_meta_keys() );
435
436 // Copy the image urls.
437 $keys[] = '_yoast_wpseo_opengraph-image';
438 $keys[] = '_yoast_wpseo_twitter-image';
439
440 $keys[] = '_yoast_wpseo_meta-robots-noindex';
441 $keys[] = '_yoast_wpseo_meta-robots-nofollow';
442 $keys[] = '_yoast_wpseo_meta-robots-adv';
443 }
444
445 $taxonomies = get_taxonomies(
446 array(
447 'hierarchical' => true,
448 'public' => true,
449 )
450 );
451
452 $sync_taxonomies = PLL()->sync->taxonomies->get_taxonomies_to_copy( $sync, $from, $to );
453
454 $taxonomies = array_intersect( $taxonomies, $sync_taxonomies );
455
456 foreach ( $taxonomies as $taxonomy ) {
457 $keys[] = '_yoast_wpseo_primary_' . $taxonomy;
458 }
459
460 return $keys;
461 }
462
463 /**
464 * Translate the primary term during the synchronization process
465 *
466 * @since 2.3.3
467 *
468 * @param int $value Meta value.
469 * @param string $key Meta key.
470 * @param string $lang Language of target.
471 * @return int
472 */
473 public function translate_post_meta( $value, $key, $lang ) {
474 if ( 0 !== strpos( $key, '_yoast_wpseo_primary_' ) ) {
475 return $value;
476 }
477
478 $taxonomy = str_replace( '_yoast_wpseo_primary_', '', $key );
479 if ( ! PLL()->model->is_translated_taxonomy( $taxonomy ) ) {
480 return $value;
481 }
482
483 return pll_get_term( $value, $lang );
484 }
485
486 /**
487 * Adds the yoast translatable metas to export.
488 *
489 * @param array $metas An array of post metas (keyed with meta keys) to export.
490 * @return array The modified array of post metas to export.
491 */
492 public function export_post_metas( $metas ) {
493 $metas_to_export = array_fill_keys( $this->get_translatable_meta_keys(), 1 );
494
495 return array_merge( $metas, $metas_to_export );
496 }
497
498 /**
499 * Returns the meta keys with translatable text.
500 *
501 * @since 3.3
502 *
503 * @return string[]
504 */
505 protected function get_translatable_meta_keys() {
506 return array(
507 '_yoast_wpseo_title',
508 '_yoast_wpseo_metadesc',
509 '_yoast_wpseo_bctitle',
510 '_yoast_wpseo_focuskw',
511 '_yoast_wpseo_opengraph-title',
512 '_yoast_wpseo_opengraph-description',
513 '_yoast_wpseo_twitter-title',
514 '_yoast_wpseo_twitter-description',
515 );
516 }
517 }
518