PluginProbe
Polylang / 3.4
Polylang v3.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.4, at include/filters.php

450 lines 14.7 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
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 $numbered_pages = get_pages( $args );
213 $pages = ! $numbered_pages ? $pages : $numbered_pages;
214 }
215
216 $ids = wp_list_pluck( $pages, 'ID' );
217
218 if ( ! $once ) {
219 // Filters the queried list of pages by language.
220 $ids = array_intersect( $ids, $this->get_related_page_ids( $language, 'IN', $args ) );
221
222 foreach ( $pages as $key => $page ) {
223 if ( ! in_array( $page->ID, $ids ) ) {
224 unset( $pages[ $key ] );
225 }
226 }
227
228 $pages = array_values( $pages ); // In case 3rd parties suppose the existence of $pages[0].
229 }
230
231 // Not done by WP but extremely useful for performance when manipulating taxonomies.
232 update_object_term_cache( $ids, $args['post_type'] );
233
234 $once = false; // In case get_pages() is called another time.
235 return $pages;
236 }
237
238 /**
239 * Get page ids related to a get_pages() in or not in a given language.
240 *
241 * @since 3.2
242 *
243 * @param PLL_Language $language The language to use in the relationship
244 * @param string $relation 'IN' or 'NOT IN'.
245 * @param array $args Array of get_pages() arguments.
246 * @return int[]
247 */
248 protected function get_related_page_ids( $language, $relation, $args ) {
249 $r = array(
250 'lang' => '', // Ensure this query is not filtered.
251 'numberposts' => -1,
252 'nopaging' => true,
253 'post_type' => $args['post_type'],
254 'post_status' => $args['post_status'],
255 'fields' => 'ids',
256 'tax_query' => array(
257 array(
258 'taxonomy' => 'language',
259 'field' => 'term_taxonomy_id', // Since WP 3.5.
260 'terms' => $language->get_tax_prop( 'language', 'term_taxonomy_id' ),
261 'operator' => $relation,
262 ),
263 ),
264 );
265
266 return get_posts( $r );
267 }
268
269 /**
270 * Modifies the sql request for get_adjacent_post to filter by the current language.
271 *
272 * @since 0.1
273 *
274 * @param string $sql The JOIN clause in the SQL.
275 * @param bool $in_same_term Whether post should be in a same taxonomy term.
276 * @param int[] $excluded_terms Array of excluded term IDs.
277 * @param string $taxonomy Taxonomy. Used to identify the term used when `$in_same_term` is true.
278 * @param WP_Post $post WP_Post object.
279 * @return string Modified JOIN clause.
280 */
281 public function posts_join( $sql, $in_same_term, $excluded_terms, $taxonomy, $post ) {
282 return $this->model->is_translated_post_type( $post->post_type ) && ! empty( $this->curlang ) ? $sql . $this->model->post->join_clause( 'p' ) : $sql;
283 }
284
285 /**
286 * Modifies the sql request for wp_get_archives and get_adjacent_post to filter by the current language.
287 *
288 * @since 0.1
289 *
290 * @param string $sql The WHERE clause in the SQL.
291 * @param bool $in_same_term Whether post should be in a same taxonomy term.
292 * @param int[] $excluded_terms Array of excluded term IDs.
293 * @param string $taxonomy Taxonomy. Used to identify the term used when `$in_same_term` is true.
294 * @param WP_Post $post WP_Post object.
295 * @return string Modified WHERE clause.
296 */
297 public function posts_where( $sql, $in_same_term, $excluded_terms, $taxonomy, $post ) {
298 return $this->model->is_translated_post_type( $post->post_type ) && ! empty( $this->curlang ) ? $sql . $this->model->post->where_clause( $this->curlang ) : $sql;
299 }
300
301 /**
302 * Converts WordPress locale to valid W3 locale in html language attributes
303 *
304 * @since 1.8
305 *
306 * @param string $output language attributes
307 * @return string
308 */
309 public function language_attributes( $output ) {
310 if ( $language = $this->model->get_language( is_admin() ? get_user_locale() : get_locale() ) ) {
311 $output = str_replace( '"' . get_bloginfo( 'language' ) . '"', '"' . $language->get_locale( 'display' ) . '"', $output );
312 }
313 return $output;
314 }
315
316 /**
317 * Translates the site title in emails sent to the user (change email, reset password)
318 * It is necessary to filter the email because WP evaluates the site title before calling switch_to_locale()
319 *
320 * @since 2.1.3
321 *
322 * @param string[] $email Email contents.
323 * @return string[] Translated email contents.
324 */
325 public function translate_user_email( $email ) {
326 $blog_name = wp_specialchars_decode( pll__( get_option( 'blogname' ) ), ENT_QUOTES );
327 $email['subject'] = sprintf( $email['subject'], $blog_name );
328 $email['message'] = str_replace( '###SITENAME###', $blog_name, $email['message'] );
329 return $email;
330 }
331
332 /**
333 * Translates the privacy policy page, on both frontend and admin
334 *
335 * @since 2.3.6
336 *
337 * @param int $id Privacy policy page id
338 * @return int
339 */
340 public function translate_page_for_privacy_policy( $id ) {
341 return empty( $this->curlang ) ? $id : $this->model->post->get( $id, $this->curlang );
342 }
343
344 /**
345 * Prevents edit and delete links for the translations of the privacy policy page for non admin
346 *
347 * @since 2.3.7
348 *
349 * @param array $caps The user's actual capabilities.
350 * @param string $cap Capability name.
351 * @param int $user_id The user ID.
352 * @param array $args Adds the context to the cap. The category id.
353 * @return array
354 */
355 public function fix_privacy_policy_page_editing( $caps, $cap, $user_id, $args ) {
356 if ( in_array( $cap, array( 'edit_page', 'edit_post', 'delete_page', 'delete_post' ) ) ) {
357 $privacy_page = get_option( 'wp_page_for_privacy_policy' );
358 if ( $privacy_page && array_intersect( $args, $this->model->post->get_translations( $privacy_page ) ) ) {
359 $caps = array_merge( $caps, map_meta_cap( 'manage_privacy_options', $user_id ) );
360 }
361 }
362
363 return $caps;
364 }
365
366 /**
367 * Register our personal data exporter
368 *
369 * @since 2.3.6
370 *
371 * @param array $exporters Personal data exporters
372 * @return array
373 */
374 public function register_personal_data_exporter( $exporters ) {
375 $exporters[] = array(
376 'exporter_friendly_name' => __( 'Translated user descriptions', 'polylang' ),
377 'callback' => array( $this, 'user_data_exporter' ),
378 );
379 return $exporters;
380 }
381
382 /**
383 * Export translated user description as WP exports only the description in the default language
384 *
385 * @since 2.3.6
386 *
387 * @param string $email_address User email address
388 * @return array Personal data
389 */
390 public function user_data_exporter( $email_address ) {
391 $email_address = trim( $email_address );
392 $data_to_export = array();
393 $user_data_to_export = array();
394
395 if ( $user = get_user_by( 'email', $email_address ) ) {
396 foreach ( $this->model->get_languages_list() as $lang ) {
397 if ( ! $lang->is_default && $value = get_user_meta( $user->ID, 'description_' . $lang->slug, true ) ) {
398 $user_data_to_export[] = array(
399 /* translators: %s is a language native name */
400 'name' => sprintf( __( 'User description - %s', 'polylang' ), $lang->name ),
401 'value' => $value,
402 );
403 }
404 }
405
406 if ( ! empty( $user_data_to_export ) ) {
407 $data_to_export[] = array(
408 'group_id' => 'user',
409 'group_label' => __( 'User', 'polylang' ),
410 'item_id' => "user-{$user->ID}",
411 'data' => $user_data_to_export,
412 );
413 }
414 }
415
416 return array(
417 'data' => $data_to_export,
418 'done' => true,
419 );
420 }
421
422 /**
423 * Filters default query arguments for checking if a term exists.
424 * In `term_exists()`, WP 6.0 uses `get_terms()`, which is filtered by language by Polylang.
425 * This filter prevents `term_exists()` to be filtered by language.
426 *
427 * @since 3.2
428 *
429 * @param array $defaults An array of arguments passed to get_terms().
430 * @param int|string $term The term to check. Accepts term ID, slug, or name.
431 * @param string $taxonomy The taxonomy name to use. An empty string indicates the search is against all taxonomies.
432 * @return array
433 */
434 public function term_exists_default_query_args( $defaults, $term, $taxonomy ) {
435 if ( ! empty( $taxonomy ) && ! $this->model->is_translated_taxonomy( $taxonomy ) ) {
436 return $defaults;
437 }
438
439 if ( ! is_array( $defaults ) ) {
440 $defaults = array();
441 }
442
443 if ( ! isset( $defaults['lang'] ) ) {
444 $defaults['lang'] = '';
445 }
446
447 return $defaults;
448 }
449 }
450