PluginProbe
Polylang / 3.2.6
Polylang v3.2.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 / include / model.php

model.php in Polylang 3.2.6, at include/model.php

759 lines 25.5 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 * Setups the language and translations model based on WordPress taxonomies
8 *
9 * @since 1.2
10 */
11 class PLL_Model {
12 /**
13 * Internal non persistent cache object.
14 *
15 * @var PLL_Cache
16 */
17 public $cache;
18
19 /**
20 * Stores the plugin options.
21 *
22 * @var array
23 */
24 public $options;
25
26 /**
27 * Translated post model.
28 *
29 * @var PLL_Translated_Post
30 */
31 public $post;
32
33 /**
34 * Translated term model.
35 *
36 * @var PLL_Translated_Term
37 */
38 public $term;
39
40 /**
41 * Constructor.
42 * Setups translated objects sub models.
43 * Setups filters and actions.
44 *
45 * @since 1.2
46 *
47 * @param array $options Polylang options.
48 */
49 public function __construct( &$options ) {
50 $this->options = &$options;
51
52 $this->cache = new PLL_Cache();
53 $this->post = new PLL_Translated_Post( $this ); // Translated post sub model.
54 $this->term = new PLL_Translated_Term( $this ); // Translated term sub model.
55
56 // We need to clean languages cache when editing a language and when modifying the permalink structure.
57 add_action( 'edited_term_taxonomy', array( $this, 'clean_languages_cache' ), 10, 2 );
58 add_action( 'update_option_permalink_structure', array( $this, 'clean_languages_cache' ) );
59 add_action( 'update_option_siteurl', array( $this, 'clean_languages_cache' ) );
60 add_action( 'update_option_home', array( $this, 'clean_languages_cache' ) );
61
62 add_filter( 'get_terms_args', array( $this, 'get_terms_args' ) );
63
64 // Just in case someone would like to display the language description ;).
65 add_filter( 'language_description', '__return_empty_string' );
66 }
67
68 /**
69 * Returns the list of available languages.
70 * - Stores the list in a db transient ( except flags ), unless PLL_CACHE_LANGUAGES is set to false.
71 * - Caches the list ( with flags ) in a PLL_Cache object.
72 *
73 * @since 0.1
74 *
75 * @param array $args {
76 * @type bool $hide_empty Hides languages with no posts if set to true ( defaults to false ).
77 * @type string $fields Returns only that field if set; {@see PLL_Language} for a list of fields.
78 * }
79 * @return array List of PLL_Language objects or PLL_Language object properties.
80 */
81 public function get_languages_list( $args = array() ) {
82 if ( false === $languages = $this->cache->get( 'languages' ) ) {
83 $languages = array();
84
85 // Create the languages from taxonomies.
86 if ( ( defined( 'PLL_CACHE_LANGUAGES' ) && ! PLL_CACHE_LANGUAGES ) || false === ( $languages = get_transient( 'pll_languages_list' ) ) ) {
87 $languages = array();
88
89 $post_languages = $this->get_language_terms();
90
91 $term_languages = get_terms( 'term_language', array( 'hide_empty' => false ) );
92 $term_languages = empty( $term_languages ) || is_wp_error( $term_languages ) ?
93 array() : array_combine( wp_list_pluck( $term_languages, 'slug' ), $term_languages );
94
95 if ( ! empty( $post_languages ) && ! empty( $term_languages ) ) {
96 foreach ( $post_languages as $k => $v ) {
97 if ( isset( $term_languages[ 'pll_' . $v->slug ] ) ) {
98 $languages[ $k ] = new PLL_Language( $v, $term_languages[ 'pll_' . $v->slug ] );
99 }
100 }
101
102 // We will need the languages list to allow its access in the filter below.
103 $this->cache->set( 'languages', $languages );
104
105 /**
106 * Filters the list of languages *before* it is stored in the persistent cache.
107 * /!\ This filter is fired *before* the $polylang object is available.
108 *
109 * @since 1.7.5
110 *
111 * @param PLL_Language[] $languages The list of language objects.
112 * @param PLL_Model $model PLL_Model object.
113 */
114 $languages = apply_filters( 'pll_languages_list', $languages, $this );
115
116 /*
117 * Don't store directly objects as it badly break with some hosts ( GoDaddy ) due to race conditions when using object cache.
118 * Thanks to captin411 for catching this!
119 * @see https://wordpress.org/support/topic/fatal-error-pll_model_languages_list?replies=8#post-6782255
120 */
121 set_transient( 'pll_languages_list', array_map( 'get_object_vars', $languages ) );
122 }
123 }
124
125 // Create the languages directly from arrays stored in transients.
126 else {
127 foreach ( $languages as $k => $v ) {
128 $languages[ $k ] = new PLL_Language( $v );
129 }
130 }
131
132 /**
133 * Filters the list of languages *after* it is stored in the persistent cache.
134 * /!\ This filter is fired *before* the $polylang object is available.
135 *
136 * @since 1.8
137 *
138 * @param PLL_Language[] $languages The list of language objects.
139 */
140 $languages = apply_filters( 'pll_after_languages_cache', $languages );
141 $this->cache->set( 'languages', $languages );
142 }
143
144 $args = wp_parse_args( $args, array( 'hide_empty' => false ) );
145
146 // Remove empty languages if requested.
147 if ( $args['hide_empty'] ) {
148 $languages = wp_list_filter( $languages, array( 'count' => 0 ), 'NOT' );
149 }
150
151 return empty( $args['fields'] ) ? $languages : wp_list_pluck( $languages, $args['fields'] );
152 }
153
154 /**
155 * Cleans language cache
156 * can be called directly with no parameter
157 * called by the 'edited_term_taxonomy' filter with 2 parameters when count needs to be updated
158 *
159 * @since 1.2
160 *
161 * @param int $term not used
162 * @param string $taxonomy taxonomy name
163 * @return void
164 */
165 public function clean_languages_cache( $term = 0, $taxonomy = null ) {
166 if ( empty( $taxonomy ) || 'language' == $taxonomy ) {
167 delete_transient( 'pll_languages_list' );
168 $this->cache->clean();
169 }
170 }
171
172 /**
173 * Don't query term metas when only our taxonomies are queried
174 *
175 * @since 2.3
176 *
177 * @param array $args WP_Term_Query arguments
178 * @return array
179 */
180 public function get_terms_args( $args ) {
181 if ( isset( $args['taxonomy'] ) && ! array_diff( (array) $args['taxonomy'], array( 'language', 'term_language', 'post_translations', 'term_translations' ) ) ) {
182 $args['update_term_meta_cache'] = false;
183 }
184 return $args;
185 }
186
187 /**
188 * Returns the language by its term_id, tl_term_id, slug or locale.
189 *
190 * @since 0.1
191 *
192 * @param mixed $value term_id, tl_term_id, slug or locale of the queried language.
193 * @return PLL_Language|false Language object, false if no language found.
194 */
195 public function get_language( $value ) {
196 if ( is_object( $value ) ) {
197 return $value instanceof PLL_Language ? $value : $this->get_language( $value->term_id ); // will force cast to PLL_Language
198 }
199
200 if ( false === $return = $this->cache->get( 'language:' . $value ) ) {
201 foreach ( $this->get_languages_list() as $lang ) {
202 $this->cache->set( 'language:' . $lang->term_id, $lang );
203 $this->cache->set( 'language:' . $lang->tl_term_id, $lang );
204 $this->cache->set( 'language:' . $lang->slug, $lang );
205 $this->cache->set( 'language:' . $lang->locale, $lang );
206 $this->cache->set( 'language:' . $lang->w3c, $lang );
207 }
208 $return = $this->cache->get( 'language:' . $value );
209 }
210
211 return $return;
212 }
213
214 /**
215 * Adds terms clauses to the term query to filter them by languages.
216 *
217 * @since 1.2
218 *
219 * @param string[] $clauses The list of sql clauses in terms query.
220 * @param PLL_Language $lang PLL_Language object.
221 * @return string[] Modified list of clauses.
222 */
223 public function terms_clauses( $clauses, $lang ) {
224 if ( ! empty( $lang ) && false === strpos( $clauses['join'], 'pll_tr' ) ) {
225 $clauses['join'] .= $this->term->join_clause();
226 $clauses['where'] .= $this->term->where_clause( $lang );
227 }
228 return $clauses;
229 }
230
231 /**
232 * Returns post types that need to be translated.
233 * The post types list is cached for better better performance.
234 * The method waits for 'after_setup_theme' to apply the cache
235 * to allow themes adding the filter in functions.php.
236 *
237 * @since 1.2
238 *
239 * @param bool $filter True if we should return only valid registered post types.
240 * @return string[] Post type names for which Polylang manages languages and translations.
241 */
242 public function get_translated_post_types( $filter = true ) {
243 if ( false === $post_types = $this->cache->get( 'post_types' ) ) {
244 $post_types = array( 'post' => 'post', 'page' => 'page', 'wp_block' => 'wp_block' );
245
246 if ( ! empty( $this->options['post_types'] ) && is_array( $this->options['post_types'] ) ) {
247 $post_types = array_merge( $post_types, array_combine( $this->options['post_types'], $this->options['post_types'] ) );
248 }
249
250 if ( empty( $this->options['media_support'] ) ) {
251 unset( $post_types['attachment'] ); // In case the post type attachment is stored in the option.
252 } else {
253 $post_types['attachment'] = 'attachment';
254 }
255
256 /**
257 * Filters the list of post types available for translation.
258 * The default are post types which have the parameter ‘public’ set to true.
259 * The filter must be added soon in the WordPress loading process:
260 * in a function hooked to ‘plugins_loaded’ or directly in functions.php for themes.
261 *
262 * @since 0.8
263 *
264 * @param string[] $post_types List of post type names.
265 * @param bool $is_settings True when displaying the list of custom post types in Polylang settings.
266 */
267 $post_types = apply_filters( 'pll_get_post_types', $post_types, false );
268
269 if ( did_action( 'after_setup_theme' ) ) {
270 $this->cache->set( 'post_types', $post_types );
271 }
272 }
273
274 return $filter ? array_intersect( $post_types, get_post_types() ) : $post_types;
275 }
276
277 /**
278 * Returns true if Polylang manages languages and translations for this post type.
279 *
280 * @since 1.2
281 *
282 * @param string|string[] $post_type Post type name or array of post type names.
283 * @return bool
284 */
285 public function is_translated_post_type( $post_type ) {
286 $post_types = $this->get_translated_post_types( false );
287 return ( is_array( $post_type ) && array_intersect( $post_type, $post_types ) || in_array( $post_type, $post_types ) || 'any' === $post_type && ! empty( $post_types ) );
288 }
289
290 /**
291 * Returns taxonomies that need to be translated.
292 *
293 * @since 1.2
294 *
295 * @param bool $filter True if we should return only valid registered taxonomies.
296 * @return string[] Array of registered taxonomy names for which Polylang manages languages and translations.
297 */
298 public function get_translated_taxonomies( $filter = true ) {
299 if ( false === $taxonomies = $this->cache->get( 'taxonomies' ) ) {
300 $taxonomies = array( 'category' => 'category', 'post_tag' => 'post_tag' );
301
302 if ( ! empty( $this->options['taxonomies'] ) && is_array( $this->options['taxonomies'] ) ) {
303 $taxonomies = array_merge( $taxonomies, array_combine( $this->options['taxonomies'], $this->options['taxonomies'] ) );
304 }
305
306 /**
307 * Filters the list of taxonomies available for translation.
308 * The default are taxonomies which have the parameter ‘public’ set to true.
309 * The filter must be added soon in the WordPress loading process:
310 * in a function hooked to ‘plugins_loaded’ or directly in functions.php for themes.
311 *
312 * @since 0.8
313 *
314 * @param string[] $taxonomies List of taxonomy names.
315 * @param bool $is_settings True when displaying the list of custom taxonomies in Polylang settings.
316 */
317 $taxonomies = apply_filters( 'pll_get_taxonomies', $taxonomies, false );
318 if ( did_action( 'after_setup_theme' ) ) {
319 $this->cache->set( 'taxonomies', $taxonomies );
320 }
321 }
322
323 return $filter ? array_intersect( $taxonomies, get_taxonomies() ) : $taxonomies;
324 }
325
326 /**
327 * Returns true if Polylang manages languages and translations for this taxonomy.
328 *
329 * @since 1.2
330 *
331 * @param string|string[] $tax Taxonomy name or array of taxonomy names.
332 * @return bool
333 */
334 public function is_translated_taxonomy( $tax ) {
335 $taxonomies = $this->get_translated_taxonomies( false );
336 return ( is_array( $tax ) && array_intersect( $tax, $taxonomies ) || in_array( $tax, $taxonomies ) );
337 }
338
339 /**
340 * Return staxonomies that need to be filtered ( post_format like ).
341 *
342 * @since 1.7
343 *
344 * @param bool $filter True if we should return only valid registered taxonomies.
345 * @return string[] Array of registered taxonomy names.
346 */
347 public function get_filtered_taxonomies( $filter = true ) {
348 if ( did_action( 'after_setup_theme' ) ) {
349 static $taxonomies = null;
350 }
351
352 if ( empty( $taxonomies ) ) {
353 $taxonomies = array( 'post_format' => 'post_format' );
354
355 /**
356 * Filters the list of taxonomies not translatable but filtered by language.
357 * Includes only the post format by default
358 * The filter must be added soon in the WordPress loading process:
359 * in a function hooked to ‘plugins_loaded’ or directly in functions.php for themes.
360 *
361 * @since 1.7
362 *
363 * @param string[] $taxonomies List of taxonomy names.
364 * @param bool $is_settings True when displaying the list of custom taxonomies in Polylang settings.
365 */
366 $taxonomies = apply_filters( 'pll_filtered_taxonomies', $taxonomies, false );
367 }
368
369 return $filter ? array_intersect( $taxonomies, get_taxonomies() ) : $taxonomies;
370 }
371
372 /**
373 * Returns true if Polylang filters this taxonomy per language.
374 *
375 * @since 1.7
376 *
377 * @param string|string[] $tax Taxonomy name or array of taxonomy names.
378 * @return bool
379 */
380 public function is_filtered_taxonomy( $tax ) {
381 $taxonomies = $this->get_filtered_taxonomies( false );
382 return ( is_array( $tax ) && array_intersect( $tax, $taxonomies ) || in_array( $tax, $taxonomies ) );
383 }
384
385 /**
386 * Returns the query vars of all filtered taxonomies.
387 *
388 * @since 1.7
389 *
390 * @return array
391 */
392 public function get_filtered_taxonomies_query_vars() {
393 $query_vars = array();
394 foreach ( $this->get_filtered_taxonomies() as $filtered_tax ) {
395 $tax = get_taxonomy( $filtered_tax );
396 if ( ! empty( $tax ) ) {
397 $query_vars[] = $tax->query_var;
398 }
399 }
400 return $query_vars;
401 }
402
403 /**
404 * It is possible to have several terms with the same name in the same taxonomy ( one per language )
405 * but the native term_exists() will return true even if only one exists.
406 * So here the function adds the language parameter.
407 *
408 * @since 1.4
409 *
410 * @param string $term_name The term name.
411 * @param string $taxonomy Taxonomy name.
412 * @param int $parent Parent term id.
413 * @param string|PLL_Language $language The language slug or object.
414 * @return null|int The term_id of the found term.
415 */
416 public function term_exists( $term_name, $taxonomy, $parent, $language ) {
417 global $wpdb;
418
419 $term_name = trim( wp_unslash( $term_name ) );
420 $term_name = _wp_specialchars( $term_name );
421
422 $select = "SELECT t.term_id FROM $wpdb->terms AS t";
423 $join = " INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id";
424 $join .= $this->term->join_clause();
425 $where = $wpdb->prepare( ' WHERE tt.taxonomy = %s AND t.name = %s', $taxonomy, $term_name );
426 $where .= $this->term->where_clause( $this->get_language( $language ) );
427
428 if ( $parent > 0 ) {
429 $where .= $wpdb->prepare( ' AND tt.parent = %d', $parent );
430 }
431
432 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
433 return $wpdb->get_var( $select . $join . $where );
434 }
435
436 /**
437 * Checks if a term slug exists in a given language, taxonomy, hierarchy.
438 *
439 * @since 1.9
440 * @since 2.8 Moved from PLL_Share_Term_Slug::term_exists() to PLL_Model::term_exists_by_slug().
441 *
442 * @param string $slug The term slug to test.
443 * @param string|PLL_Language $language The language slug or object.
444 * @param string $taxonomy Optional taxonomy name.
445 * @param int $parent Optional parent term id.
446 * @return null|int The term_id of the found term.
447 */
448 public function term_exists_by_slug( $slug, $language, $taxonomy = '', $parent = 0 ) {
449 global $wpdb;
450
451 $select = "SELECT t.term_id FROM {$wpdb->terms} AS t";
452 $join = " INNER JOIN {$wpdb->term_taxonomy} AS tt ON t.term_id = tt.term_id";
453 $join .= $this->term->join_clause();
454 $where = $wpdb->prepare( ' WHERE t.slug = %s', $slug );
455 $where .= $this->term->where_clause( $this->get_language( $language ) );
456
457 if ( ! empty( $taxonomy ) ) {
458 $where .= $wpdb->prepare( ' AND tt.taxonomy = %s', $taxonomy );
459 }
460
461 if ( $parent > 0 ) {
462 $where .= $wpdb->prepare( ' AND tt.parent = %d', $parent );
463 }
464
465 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
466 return $wpdb->get_var( $select . $join . $where );
467 }
468
469
470 /**
471 * Gets the number of posts per language in a date, author or post type archive.
472 *
473 * @since 1.2
474 *
475 * @param PLL_Language $lang PLL_Language instance.
476 * @param array $q {
477 * WP_Query arguments:
478 *
479 * @type string|string[] $post_type Post type or array of post types.
480 * @type int $m Combination YearMonth. Accepts any four-digit year and month.
481 * @type int $year Four-digit year.
482 * @type int $monthnum Two-digit month.
483 * @type int $day Day of the month.
484 * @type int $author Author id.
485 * @type string $author_name User 'user_nicename'.
486 * @type string $post_format Post format.
487 * @type string $post_status Post status.
488 * }
489 * @return int
490 */
491 public function count_posts( $lang, $q = array() ) {
492 global $wpdb;
493
494 $q = wp_parse_args( $q, array( 'post_type' => 'post', 'post_status' => 'publish' ) );
495
496 if ( ! is_array( $q['post_type'] ) ) {
497 $q['post_type'] = array( $q['post_type'] );
498 }
499
500 foreach ( $q['post_type'] as $key => $type ) {
501 if ( ! post_type_exists( $type ) ) {
502 unset( $q['post_type'][ $key ] );
503 }
504 }
505
506 if ( empty( $q['post_type'] ) ) {
507 $q['post_type'] = array( 'post' ); // We *need* a post type.
508 }
509
510 $cache_key = 'pll_count_posts_' . md5( maybe_serialize( $q ) );
511 $counts = wp_cache_get( $cache_key, 'counts' );
512
513 if ( false === $counts ) {
514 $counts = array();
515
516 $select = "SELECT pll_tr.term_taxonomy_id, COUNT( * ) AS num_posts FROM {$wpdb->posts}";
517 $join = $this->post->join_clause();
518 $where = sprintf( " WHERE post_status = '%s'", esc_sql( $q['post_status'] ) );
519 $where .= sprintf( " AND {$wpdb->posts}.post_type IN ( '%s' )", implode( "', '", esc_sql( $q['post_type'] ) ) );
520 $where .= $this->post->where_clause( $this->get_languages_list() );
521 $groupby = ' GROUP BY pll_tr.term_taxonomy_id';
522
523 if ( ! empty( $q['m'] ) ) {
524 $q['m'] = '' . preg_replace( '|[^0-9]|', '', $q['m'] );
525 $where .= $wpdb->prepare( " AND YEAR( {$wpdb->posts}.post_date ) = %d", substr( $q['m'], 0, 4 ) );
526 if ( strlen( $q['m'] ) > 5 ) {
527 $where .= $wpdb->prepare( " AND MONTH( {$wpdb->posts}.post_date ) = %d", substr( $q['m'], 4, 2 ) );
528 }
529 if ( strlen( $q['m'] ) > 7 ) {
530 $where .= $wpdb->prepare( " AND DAYOFMONTH( {$wpdb->posts}.post_date ) = %d", substr( $q['m'], 6, 2 ) );
531 }
532 }
533
534 if ( ! empty( $q['year'] ) ) {
535 $where .= $wpdb->prepare( " AND YEAR( {$wpdb->posts}.post_date ) = %d", $q['year'] );
536 }
537
538 if ( ! empty( $q['monthnum'] ) ) {
539 $where .= $wpdb->prepare( " AND MONTH( {$wpdb->posts}.post_date ) = %d", $q['monthnum'] );
540 }
541
542 if ( ! empty( $q['day'] ) ) {
543 $where .= $wpdb->prepare( " AND DAYOFMONTH( {$wpdb->posts}.post_date ) = %d", $q['day'] );
544 }
545
546 if ( ! empty( $q['author_name'] ) ) {
547 $author = get_user_by( 'slug', sanitize_title_for_query( $q['author_name'] ) );
548 if ( $author ) {
549 $q['author'] = $author->ID;
550 }
551 }
552
553 if ( ! empty( $q['author'] ) ) {
554 $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_author = %d", $q['author'] );
555 }
556
557 // Filtered taxonomies ( post_format ).
558 foreach ( $this->get_filtered_taxonomies_query_vars() as $tax_qv ) {
559
560 if ( ! empty( $q[ $tax_qv ] ) ) {
561 $join .= " INNER JOIN {$wpdb->term_relationships} AS tr ON tr.object_id = {$wpdb->posts}.ID";
562 $join .= " INNER JOIN {$wpdb->term_taxonomy} AS tt ON tt.term_taxonomy_id = tr.term_taxonomy_id";
563 $join .= " INNER JOIN {$wpdb->terms} AS t ON t.term_id = tt.term_id";
564 $where .= $wpdb->prepare( ' AND t.slug = %s', $q[ $tax_qv ] );
565 }
566 }
567
568 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
569 $res = $wpdb->get_results( $select . $join . $where . $groupby, ARRAY_A );
570 foreach ( (array) $res as $row ) {
571 $counts[ $row['term_taxonomy_id'] ] = $row['num_posts'];
572 }
573
574 wp_cache_set( $cache_key, $counts, 'counts' );
575 }
576
577 return empty( $counts[ $lang->term_taxonomy_id ] ) ? 0 : $counts[ $lang->term_taxonomy_id ];
578 }
579
580 /**
581 * Setup the links model based on options.
582 *
583 * @since 1.2
584 *
585 * @return PLL_Links_Model
586 */
587 public function get_links_model() {
588 $c = array( 'Directory', 'Directory', 'Subdomain', 'Domain' );
589 $class = get_option( 'permalink_structure' ) ? 'PLL_Links_' . $c[ $this->options['force_lang'] ] : 'PLL_Links_Default';
590
591 /**
592 * Filters the links model class to use.
593 * /!\ this filter is fired *before* the $polylang object is available.
594 *
595 * @since 2.1.1
596 *
597 * @param string $class A class name: PLL_Links_Default, PLL_Links_Directory, PLL_Links_Subdomain, PLL_Links_Domain.
598 */
599 $class = apply_filters( 'pll_links_model', $class );
600
601 return new $class( $this );
602 }
603
604 /**
605 * Returns posts and terms ids without language ( used in settings ).
606 *
607 * @since 0.9
608 * @since 2.2.6 Add the $limit argument.
609 *
610 * @param int $limit Max number of posts or terms to return. Defaults to -1 (no limit).
611 * @return array {
612 * Objects without language.
613 *
614 * @type int[] $posts Array of post ids.
615 * @type int[] $terms Array of term ids.
616 * }
617 */
618 public function get_objects_with_no_lang( $limit = -1 ) {
619 /**
620 * Filters the max number of posts or terms to return when searching objects with no language.
621 * This filter can be used to decrease the memory usage in case the number of objects
622 * without language is too big. Using a negative value is equivalent to have no limit.
623 *
624 * @since 2.2.6
625 *
626 * @param int $limit Max number of posts or terms to retrieve from the database.
627 */
628 $limit = (int) apply_filters( 'get_objects_with_no_lang_limit', $limit );
629
630 $posts = $this->get_posts_with_no_lang( $this->get_translated_post_types(), $limit );
631 $terms = $this->get_terms_with_no_lang( $this->get_translated_taxonomies(), $limit );
632
633 /**
634 * Filters the list of untranslated posts ids and terms ids
635 *
636 * @since 0.9
637 *
638 * @param array|false $objects false if no ids found, list of post and/or term ids otherwise.
639 */
640 return apply_filters( 'pll_get_objects_with_no_lang', empty( $posts ) && empty( $terms ) ? false : array( 'posts' => $posts, 'terms' => $terms ) );
641 }
642
643 /**
644 * Returns ids of post without language.
645 *
646 * @since 3.1
647 *
648 * @param string|string[] $post_types A translated post type or an array of translated post types.
649 * @param int $limit Max number of posts to return.
650 * @return int[]
651 */
652 public function get_posts_with_no_lang( $post_types, $limit ) {
653 return get_posts(
654 array(
655 'numberposts' => $limit,
656 'nopaging' => $limit <= 0,
657 'post_type' => $post_types,
658 'post_status' => 'any',
659 'fields' => 'ids',
660 'tax_query' => array(
661 array(
662 'taxonomy' => 'language',
663 'terms' => $this->get_languages_list( array( 'fields' => 'term_id' ) ),
664 'operator' => 'NOT IN',
665 ),
666 ),
667 )
668 );
669 }
670
671 /**
672 * Returns ids of terms without language.
673 *
674 * @since 3.1
675 *
676 * @param string|string[] $taxonomies A translated taxonomy or an array of taxonomies post types.
677 * @param int $limit Max number of terms to return.
678 * @return int[]
679 */
680 public function get_terms_with_no_lang( $taxonomies, $limit ) {
681 global $wpdb;
682
683 $taxonomies = (array) $taxonomies;
684
685 $sql = sprintf(
686 "SELECT {$wpdb->term_taxonomy}.term_id FROM {$wpdb->term_taxonomy}
687 WHERE taxonomy IN ('%s')
688 AND {$wpdb->term_taxonomy}.term_id NOT IN (
689 SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN (%s)
690 )
691 %s",
692 implode( "','", array_map( 'esc_sql', $taxonomies ) ),
693 implode( ',', array_map( 'intval', $this->get_languages_list( array( 'fields' => 'tl_term_taxonomy_id' ) ) ) ),
694 $limit > 0 ? sprintf( 'LIMIT %d', intval( $limit ) ) : ''
695 );
696
697 $key = md5( $sql );
698 $last_changed = wp_cache_get_last_changed( 'terms' );
699 $cache_key = "terms_no_lang:{$key}:{$last_changed}";
700
701 $term_ids = wp_cache_get( $cache_key, 'terms' );
702
703 if ( false === $term_ids ) {
704 $term_ids = $wpdb->get_col( $sql ); // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
705 wp_cache_set( $cache_key, $term_ids, 'terms' );
706 }
707
708 return $term_ids;
709 }
710
711 /**
712 * Filters the ORDERBY clause of the languages query.
713 * This allows to order languages by `term_group` and `term_id`.
714 *
715 * @since 3.2.3
716 *
717 * @param string $orderby `ORDERBY` clause of the terms query.
718 * @param array $args An array of term query arguments.
719 * @param string[] $taxonomies An array of taxonomy names.
720 * @return string
721 */
722 public function filter_language_terms_orderby( $orderby, $args, $taxonomies ) {
723 if ( ! is_array( $taxonomies ) || count( $taxonomies ) > 1 ) {
724 return $orderby;
725 }
726
727 if ( 'language' !== reset( $taxonomies ) ) {
728 return $orderby;
729 }
730
731 if ( empty( $orderby ) || ! is_string( $orderby ) ) {
732 return $orderby;
733 }
734
735 if ( ! preg_match( '@^(?<alias>[^.]+)\.term_group$@', $orderby, $matches ) ) {
736 return $orderby;
737 }
738
739 return sprintf( '%1$s.term_group, %1$s.term_id', $matches['alias'] );
740 }
741
742 /**
743 * Returns the list of existing language terms.
744 * - Returns all terms, that are or not assigned to posts.
745 * - Terms are ordered by `term_group` and `term_id` (see `PLL_Model->filter_language_terms_orderby()`).
746 *
747 * @since 3.2.3
748 *
749 * @return array<WP_Term>
750 */
751 protected function get_language_terms() {
752 add_filter( 'get_terms_orderby', array( $this, 'filter_language_terms_orderby' ), 10, 3 );
753 $post_languages = get_terms( array( 'taxonomy' => 'language', 'hide_empty' => false, 'orderby' => 'term_group' ) );
754 remove_filter( 'get_terms_orderby', array( $this, 'filter_language_terms_orderby' ) );
755
756 return empty( $post_languages ) || is_wp_error( $post_languages ) ? array() : $post_languages;
757 }
758 }
759