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

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