PluginProbe
Polylang / 3.3.3
Polylang v3.3.3
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.3.3, at include/model.php

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