PluginProbe
Polylang / 3.7.4
Polylang v3.7.4
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 / include / filters.php

filters.php in Polylang 3.7.4, at include/filters.php

473 lines 15.4 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 * Setup filters common to admin and frontend
8 *
9 * @since 1.4
10 */
11 class PLL_Filters {
12 /**
13 * Stores the plugin options.
14 *
15 * @var array
16 */
17 public $options;
18
19 /**
20 * @var PLL_Model
21 */
22 public $model;
23
24 /**
25 * Instance of a child class of PLL_Links_Model.
26 *
27 * @var PLL_Links_Model
28 */
29 public $links_model;
30
31 /**
32 * Current language.
33 *
34 * @var PLL_Language|null
35 */
36 public $curlang;
37
38 /**
39 * Constructor: setups filters
40 *
41 * @since 1.4
42 *
43 * @param object $polylang The Polylang object.
44 */
45 public function __construct( &$polylang ) {
46 global $wp_version;
47
48 $this->links_model = &$polylang->links_model;
49 $this->model = &$polylang->model;
50 $this->options = &$polylang->options;
51 $this->curlang = &$polylang->curlang;
52
53 // Deletes our cache for sticky posts when the list is updated.
54 add_action( 'update_option_sticky_posts', array( $this, 'delete_sticky_posts_cache' ) );
55 add_action( 'add_option_sticky_posts', array( $this, 'delete_sticky_posts_cache' ) );
56 add_action( 'delete_option_sticky_posts', array( $this, 'delete_sticky_posts_cache' ) );
57
58 // Filters the comments according to the current language
59 add_action( 'parse_comment_query', array( $this, 'parse_comment_query' ) );
60 add_filter( 'comments_clauses', array( $this, 'comments_clauses' ), 10, 2 );
61
62 // Filters the get_pages function according to the current language
63 if ( version_compare( $wp_version, '6.3-alpha', '<' ) ) {
64 // Backward compatibility with WP < 6.3.
65 add_filter( 'get_pages', array( $this, 'get_pages' ), 10, 2 );
66 }
67 add_filter( 'get_pages_query_args', array( $this, 'get_pages_query_args' ), 10, 2 );
68
69 // Rewrites next and previous post links to filter them by language
70 add_filter( 'get_previous_post_join', array( $this, 'posts_join' ), 10, 5 );
71 add_filter( 'get_next_post_join', array( $this, 'posts_join' ), 10, 5 );
72 add_filter( 'get_previous_post_where', array( $this, 'posts_where' ), 10, 5 );
73 add_filter( 'get_next_post_where', array( $this, 'posts_where' ), 10, 5 );
74
75 // Converts the locale to a valid W3C locale
76 add_filter( 'language_attributes', array( $this, 'language_attributes' ) );
77
78 // Translate the site title in emails sent to users
79 add_filter( 'password_change_email', array( $this, 'translate_user_email' ) );
80 add_filter( 'email_change_email', array( $this, 'translate_user_email' ) );
81
82 // Translates the privacy policy page
83 add_filter( 'option_wp_page_for_privacy_policy', array( $this, 'translate_page_for_privacy_policy' ), 20 ); // Since WP 4.9.6
84 add_filter( 'map_meta_cap', array( $this, 'fix_privacy_policy_page_editing' ), 10, 4 );
85
86 // Personal data exporter
87 add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_personal_data_exporter' ), 0 ); // Since WP 4.9.6
88
89 // Fix for `term_exists()`.
90 add_filter( 'term_exists_default_query_args', array( $this, 'term_exists_default_query_args' ), 0, 3 ); // Since WP 6.0.0.
91 }
92
93 /**
94 * Deletes the cache for multilingual sticky posts.
95 *
96 * @since 2.8.4
97 *
98 * @return void
99 */
100 public function delete_sticky_posts_cache() {
101 wp_cache_delete( 'sticky_posts', 'options' );
102 }
103
104 /**
105 * Get the language to filter a comments query.
106 *
107 * @since 2.0
108 * @since 3.1 Always returns an array. Renamed from get_comments_queried_language().
109 *
110 * @param WP_Comment_Query $query WP_Comment_Query object.
111 * @return PLL_Language[] The languages to use in the filter.
112 */
113 protected function get_comments_queried_languages( $query ) {
114 // Don't filter comments if comment ids or post ids are specified.
115 $plucked = wp_array_slice_assoc( $query->query_vars, array( 'comment__in', 'parent', 'post_id', 'post__in', 'post_parent' ) );
116 $fields = array_filter( $plucked );
117 if ( ! empty( $fields ) ) {
118 return array();
119 }
120
121 // Don't filter comments if a non translated post type is specified.
122 if ( ! empty( $query->query_vars['post_type'] ) && ! $this->model->is_translated_post_type( $query->query_vars['post_type'] ) ) {
123 return array();
124 }
125
126 // If comments are queried with a 'lang' parameter, keeps only language codes.
127 if ( isset( $query->query_vars['lang'] ) ) {
128 $languages = is_string( $query->query_vars['lang'] ) ? explode( ',', $query->query_vars['lang'] ) : $query->query_vars['lang'];
129 if ( is_array( $languages ) ) {
130 $languages = array_map( array( $this->model, 'get_language' ), $languages );
131 return array_filter( $languages );
132 }
133 }
134
135 if ( ! empty( $this->curlang ) ) {
136 return array( $this->curlang );
137 }
138
139 return array();
140 }
141
142 /**
143 * Adds a language dependent cache domain when querying comments.
144 * Useful as the 'lang' parameter is not included in cache key by WordPress.
145 * Needed since WP 4.6 as comments have been added to persistent cache. See #36906, #37419.
146 *
147 * @since 2.0
148 *
149 * @param WP_Comment_Query $query WP_Comment_Query object.
150 * @return void
151 */
152 public function parse_comment_query( $query ) {
153 $lang = $this->get_comments_queried_languages( $query );
154 if ( ! empty( $lang ) ) {
155 $lang = wp_list_pluck( $lang, 'slug' );
156 $key = '_' . implode( ',', $lang );
157 $query->query_vars['cache_domain'] = empty( $query->query_vars['cache_domain'] ) ? 'pll' . $key : $query->query_vars['cache_domain'] . $key;
158 }
159 }
160
161 /**
162 * Filters the comments according to the current language.
163 * Used by the recent comments widget and admin language filter.
164 *
165 * @since 0.2
166 *
167 * @param string[] $clauses SQL clauses.
168 * @param WP_Comment_Query $query WP_Comment_Query object.
169 * @return string[] Modified $clauses.
170 */
171 public function comments_clauses( $clauses, $query ) {
172 global $wpdb;
173
174 $lang = $this->get_comments_queried_languages( $query );
175
176 if ( ! empty( $lang ) ) {
177 $lang = wp_list_pluck( $lang, 'slug' );
178
179 // If this clause is not already added by WP.
180 if ( false === strpos( $clauses['join'], "JOIN $wpdb->posts ON $wpdb->posts.ID" ) ) {
181 $clauses['join'] .= " JOIN $wpdb->posts ON $wpdb->posts.ID = $wpdb->comments.comment_post_ID";
182 }
183
184 $clauses['join'] .= $this->model->post->join_clause();
185 $clauses['where'] .= $this->model->post->where_clause( $lang );
186 }
187 return $clauses;
188 }
189
190 /**
191 * Filters get_pages() per language.
192 *
193 * @since 1.4
194 *
195 * @param WP_Post[] $pages An array of pages already queried.
196 * @param array $args Array of get_pages() arguments.
197 * @return WP_Post[] Modified list of pages.
198 */
199 public function get_pages( $pages, $args ) {
200 if ( isset( $args['lang'] ) && empty( $args['lang'] ) ) {
201 return $pages;
202 }
203
204 $language = empty( $args['lang'] ) ? $this->curlang : $this->model->get_language( $args['lang'] );
205
206 if ( empty( $language ) || empty( $pages ) || ! $this->model->is_translated_post_type( $args['post_type'] ) ) {
207 return $pages;
208 }
209
210 static $once = false;
211
212 if ( ! empty( $args['number'] ) && ! $once ) {
213 // We are obliged to redo the get_pages() query if we want to get the right number.
214 $once = true; // Avoid infinite loop.
215
216 // Take care that 'exclude' argument accepts integer or strings too.
217 $args['exclude'] = array_merge( wp_parse_id_list( $args['exclude'] ), $this->get_related_page_ids( $language, 'NOT IN', $args ) ); // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
218 $numbered_pages = get_pages( $args );
219 $pages = ! $numbered_pages ? $pages : $numbered_pages;
220 }
221
222 $ids = wp_list_pluck( $pages, 'ID' );
223
224 if ( ! $once ) {
225 // Filters the queried list of pages by language.
226 $ids = array_intersect( $ids, $this->get_related_page_ids( $language, 'IN', $args ) );
227
228 foreach ( $pages as $key => $page ) {
229 if ( ! in_array( $page->ID, $ids ) ) {
230 unset( $pages[ $key ] );
231 }
232 }
233
234 $pages = array_values( $pages ); // In case 3rd parties suppose the existence of $pages[0].
235 }
236
237 // Not done by WP but extremely useful for performance when manipulating taxonomies.
238 update_object_term_cache( $ids, $args['post_type'] );
239
240 $once = false; // In case get_pages() is called another time.
241 return $pages;
242 }
243
244 /**
245 * Filters the WP_Query in get_pages() per language.
246 *
247 * @since 3.4.3
248 *
249 * @param array $query_args Array of arguments passed to WP_Query.
250 * @param array $parsed_args Array of get_pages() arguments.
251 * @return array Array of arguments passed to WP_Query with the language.
252 */
253 public function get_pages_query_args( $query_args, $parsed_args ) {
254 if ( isset( $parsed_args['lang'] ) ) {
255 $query_args['lang'] = $parsed_args['lang'];
256 }
257
258 return $query_args;
259 }
260
261 /**
262 * Get page ids related to a get_pages() in or not in a given language.
263 *
264 * @since 3.2
265 *
266 * @param PLL_Language $language The language to use in the relationship
267 * @param string $relation 'IN' or 'NOT IN'.
268 * @param array $args Array of get_pages() arguments.
269 * @return int[]
270 */
271 protected function get_related_page_ids( $language, $relation, $args ) {
272 $r = array(
273 'lang' => '', // Ensure this query is not filtered.
274 'numberposts' => -1,
275 'nopaging' => true,
276 'post_type' => $args['post_type'],
277 'post_status' => $args['post_status'],
278 'fields' => 'ids',
279 'tax_query' => array(
280 array(
281 'taxonomy' => 'language',
282 'field' => 'term_taxonomy_id', // Since WP 3.5.
283 'terms' => $language->get_tax_prop( 'language', 'term_taxonomy_id' ),
284 'operator' => $relation,
285 ),
286 ),
287 );
288
289 return get_posts( $r );
290 }
291
292 /**
293 * Modifies the sql request for get_adjacent_post to filter by the current language.
294 *
295 * @since 0.1
296 *
297 * @param string $sql The JOIN clause in the SQL.
298 * @param bool $in_same_term Whether post should be in a same taxonomy term.
299 * @param int[] $excluded_terms Array of excluded term IDs.
300 * @param string $taxonomy Taxonomy. Used to identify the term used when `$in_same_term` is true.
301 * @param WP_Post $post WP_Post object.
302 * @return string Modified JOIN clause.
303 */
304 public function posts_join( $sql, $in_same_term, $excluded_terms, $taxonomy, $post ) {
305 return $this->model->is_translated_post_type( $post->post_type ) && ! empty( $this->curlang ) ? $sql . $this->model->post->join_clause( 'p' ) : $sql;
306 }
307
308 /**
309 * Modifies the sql request for wp_get_archives and get_adjacent_post to filter by the current language.
310 *
311 * @since 0.1
312 *
313 * @param string $sql The WHERE clause in the SQL.
314 * @param bool $in_same_term Whether post should be in a same taxonomy term.
315 * @param int[] $excluded_terms Array of excluded term IDs.
316 * @param string $taxonomy Taxonomy. Used to identify the term used when `$in_same_term` is true.
317 * @param WP_Post $post WP_Post object.
318 * @return string Modified WHERE clause.
319 */
320 public function posts_where( $sql, $in_same_term, $excluded_terms, $taxonomy, $post ) {
321 return $this->model->is_translated_post_type( $post->post_type ) && ! empty( $this->curlang ) ? $sql . $this->model->post->where_clause( $this->curlang ) : $sql;
322 }
323
324 /**
325 * Converts WordPress locale to valid W3 locale in html language attributes
326 *
327 * @since 1.8
328 *
329 * @param string $output language attributes
330 * @return string
331 */
332 public function language_attributes( $output ) {
333 if ( $language = $this->model->get_language( is_admin() ? get_user_locale() : get_locale() ) ) {
334 $output = str_replace( '"' . get_bloginfo( 'language' ) . '"', '"' . $language->get_locale( 'display' ) . '"', $output );
335 }
336 return $output;
337 }
338
339 /**
340 * Translates the site title in emails sent to the user (change email, reset password)
341 * It is necessary to filter the email because WP evaluates the site title before calling switch_to_locale()
342 *
343 * @since 2.1.3
344 *
345 * @param string[] $email Email contents.
346 * @return string[] Translated email contents.
347 */
348 public function translate_user_email( $email ) {
349 $blog_name = wp_specialchars_decode( pll__( get_option( 'blogname' ) ), ENT_QUOTES );
350 $email['subject'] = sprintf( $email['subject'], $blog_name );
351 $email['message'] = str_replace( '###SITENAME###', $blog_name, $email['message'] );
352 return $email;
353 }
354
355 /**
356 * Translates the privacy policy page, on both frontend and admin
357 *
358 * @since 2.3.6
359 *
360 * @param int $id Privacy policy page id
361 * @return int
362 */
363 public function translate_page_for_privacy_policy( $id ) {
364 return empty( $this->curlang ) ? $id : $this->model->post->get( $id, $this->curlang );
365 }
366
367 /**
368 * Prevents edit and delete links for the translations of the privacy policy page for non admin
369 *
370 * @since 2.3.7
371 *
372 * @param array $caps The user's actual capabilities.
373 * @param string $cap Capability name.
374 * @param int $user_id The user ID.
375 * @param array $args Adds the context to the cap. The category id.
376 * @return array
377 */
378 public function fix_privacy_policy_page_editing( $caps, $cap, $user_id, $args ) {
379 if ( in_array( $cap, array( 'edit_page', 'edit_post', 'delete_page', 'delete_post' ) ) ) {
380 $privacy_page = get_option( 'wp_page_for_privacy_policy' );
381 if ( $privacy_page && array_intersect( $args, $this->model->post->get_translations( $privacy_page ) ) ) {
382 $caps = array_merge( $caps, map_meta_cap( 'manage_privacy_options', $user_id ) );
383 }
384 }
385
386 return $caps;
387 }
388
389 /**
390 * Register our personal data exporter
391 *
392 * @since 2.3.6
393 *
394 * @param array $exporters Personal data exporters
395 * @return array
396 */
397 public function register_personal_data_exporter( $exporters ) {
398 $exporters[] = array(
399 'exporter_friendly_name' => __( 'Translated user descriptions', 'polylang' ),
400 'callback' => array( $this, 'user_data_exporter' ),
401 );
402 return $exporters;
403 }
404
405 /**
406 * Export translated user description as WP exports only the description in the default language
407 *
408 * @since 2.3.6
409 *
410 * @param string $email_address User email address
411 * @return array Personal data
412 */
413 public function user_data_exporter( $email_address ) {
414 $email_address = trim( $email_address );
415 $data_to_export = array();
416 $user_data_to_export = array();
417
418 if ( $user = get_user_by( 'email', $email_address ) ) {
419 foreach ( $this->model->get_languages_list() as $lang ) {
420 if ( ! $lang->is_default && $value = get_user_meta( $user->ID, 'description_' . $lang->slug, true ) ) {
421 $user_data_to_export[] = array(
422 /* translators: %s is a language native name */
423 'name' => sprintf( __( 'User description - %s', 'polylang' ), $lang->name ),
424 'value' => $value,
425 );
426 }
427 }
428
429 if ( ! empty( $user_data_to_export ) ) {
430 $data_to_export[] = array(
431 'group_id' => 'user',
432 'group_label' => __( 'User', 'polylang' ),
433 'item_id' => "user-{$user->ID}",
434 'data' => $user_data_to_export,
435 );
436 }
437 }
438
439 return array(
440 'data' => $data_to_export,
441 'done' => true,
442 );
443 }
444
445 /**
446 * Filters default query arguments for checking if a term exists.
447 * In `term_exists()`, WP 6.0 uses `get_terms()`, which is filtered by language by Polylang.
448 * This filter prevents `term_exists()` to be filtered by language.
449 *
450 * @since 3.2
451 *
452 * @param array $defaults An array of arguments passed to get_terms().
453 * @param int|string $term The term to check. Accepts term ID, slug, or name.
454 * @param string $taxonomy The taxonomy name to use. An empty string indicates the search is against all taxonomies.
455 * @return array
456 */
457 public function term_exists_default_query_args( $defaults, $term, $taxonomy ) {
458 if ( ! empty( $taxonomy ) && ! $this->model->is_translated_taxonomy( $taxonomy ) ) {
459 return $defaults;
460 }
461
462 if ( ! is_array( $defaults ) ) {
463 $defaults = array();
464 }
465
466 if ( ! isset( $defaults['lang'] ) ) {
467 $defaults['lang'] = '';
468 }
469
470 return $defaults;
471 }
472 }
473