PluginProbe
Polylang / 3.2.4
Polylang v3.2.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.2.4, at include/filters.php

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