PluginProbe
Polylang / 3.5.1
Polylang v3.5.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 / model.php

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

1,052 lines 33.8 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 * Translatable objects registry.
28 *
29 * @since 3.4
30 *
31 * @var PLL_Translatable_Objects
32 */
33 public $translatable_objects;
34
35 /**
36 * Translated post model.
37 *
38 * @var PLL_Translated_Post
39 */
40 public $post;
41
42 /**
43 * Translated term model.
44 *
45 * @var PLL_Translated_Term
46 */
47 public $term;
48
49 /**
50 * Flag set to true during the language objects creation.
51 *
52 * @var bool
53 */
54 private $is_creating_language_objects = false;
55
56 /**
57 * Tells if {@see PLL_Model::get_languages_list()} can be used.
58 *
59 * @var bool
60 */
61 private $languages_ready = false;
62
63 /**
64 * Constructor.
65 * Setups translated objects sub models.
66 * Setups filters and actions.
67 *
68 * @since 1.2
69 *
70 * @param array $options Polylang options.
71 */
72 public function __construct( &$options ) {
73 $this->options = &$options;
74
75 $this->cache = new PLL_Cache();
76 $this->translatable_objects = new PLL_Translatable_Objects();
77 $this->post = $this->translatable_objects->register( new PLL_Translated_Post( $this ) ); // Translated post sub model.
78 $this->term = $this->translatable_objects->register( new PLL_Translated_Term( $this ) ); // Translated term sub model.
79
80 // We need to clean languages cache when editing a language and when modifying the permalink structure.
81 add_action( 'edited_term_taxonomy', array( $this, 'clean_languages_cache' ), 10, 2 );
82 add_action( 'update_option_permalink_structure', array( $this, 'clean_languages_cache' ) );
83 add_action( 'update_option_siteurl', array( $this, 'clean_languages_cache' ) );
84 add_action( 'update_option_home', array( $this, 'clean_languages_cache' ) );
85
86 add_filter( 'get_terms_args', array( $this, 'get_terms_args' ) );
87
88 // Just in case someone would like to display the language description ;).
89 add_filter( 'language_description', '__return_empty_string' );
90 }
91
92 /**
93 * Checks if there are languages or not.
94 *
95 * @since 3.3
96 *
97 * @return bool True if there are, false otherwise.
98 */
99 public function has_languages() {
100 if ( ! empty( $this->cache->get( 'languages' ) ) ) {
101 return true;
102 }
103
104 if ( ! empty( get_transient( 'pll_languages_list' ) ) ) {
105 return true;
106 }
107
108 return ! empty( $this->get_language_terms() );
109 }
110
111 /**
112 * Returns the list of available languages.
113 * - Stores the list in a db transient (except flags), unless `PLL_CACHE_LANGUAGES` is set to false.
114 * - Caches the list (with flags) in a `PLL_Cache` object.
115 *
116 * @since 0.1
117 *
118 * @param array $args {
119 * @type bool $hide_empty Hides languages with no posts if set to `true` (defaults to `false`).
120 * @type bool $hide_default Hides default language from the list (default to `false`).
121 * @type string $fields Returns only that field if set; {@see PLL_Language} for a list of fields.
122 * }
123 * @return array List of PLL_Language objects or PLL_Language object properties.
124 */
125 public function get_languages_list( $args = array() ) {
126 if ( ! $this->are_languages_ready() ) {
127 _doing_it_wrong(
128 __METHOD__ . '()',
129 "It must not be called before the hook 'pll_pre_init'.",
130 '3.4'
131 );
132 }
133
134 $languages = $this->cache->get( 'languages' );
135
136 if ( ! is_array( $languages ) ) {
137 // Bail out early if languages are currently created to avoid an infinite loop.
138 if ( $this->is_creating_language_objects ) {
139 return array();
140 }
141
142 $this->is_creating_language_objects = true;
143
144 if ( ! pll_get_constant( 'PLL_CACHE_LANGUAGES', true ) ) {
145 // Create the languages from taxonomies.
146 $languages = $this->get_languages_from_taxonomies();
147 } else {
148 $languages = get_transient( 'pll_languages_list' );
149
150 if ( empty( $languages ) || ! is_array( $languages ) || empty( reset( $languages )['term_props'] ) ) { // Test `term_props` in case we got a transient older than 3.4.
151 // Create the languages from taxonomies.
152 $languages = $this->get_languages_from_taxonomies();
153 } else {
154 // Create the languages directly from arrays stored in the transient.
155 $languages = array_map(
156 array( new PLL_Language_Factory( $this->options ), 'get' ),
157 $languages
158 );
159
160 // Remove potential empty language.
161 $languages = array_filter( $languages );
162
163 // Re-index.
164 $languages = array_values( $languages );
165 }
166 }
167
168 /**
169 * Filters the list of languages *after* it is stored in the persistent cache.
170 * /!\ This filter is fired *before* the $polylang object is available.
171 *
172 * @since 1.8
173 * @since 3.4 Deprecated. If you used this hook to filter URLs, you may hook `'site_url'` instead.
174 * @deprecated
175 *
176 * @param PLL_Language[] $languages The list of language objects.
177 */
178 $languages = apply_filters_deprecated( 'pll_after_languages_cache', array( $languages ), '3.4' );
179
180 if ( $this->are_languages_ready() ) {
181 $this->cache->set( 'languages', $languages );
182 }
183
184 $this->is_creating_language_objects = false;
185 }
186
187 $languages = array_filter(
188 $languages,
189 function( $lang ) use ( $args ) {
190 $keep_empty = empty( $args['hide_empty'] ) || $lang->get_tax_prop( 'language', 'count' );
191 $keep_default = empty( $args['hide_default'] ) || ! $lang->is_default;
192 return $keep_empty && $keep_default;
193
194 }
195 );
196
197 $languages = array_values( $languages ); // Re-index.
198
199 return empty( $args['fields'] ) ? $languages : wp_list_pluck( $languages, $args['fields'] );
200 }
201
202 /**
203 * Tells if {@see PLL_Model::get_languages_list()} can be used.
204 *
205 * @since 3.4
206 *
207 * @return bool
208 */
209 public function are_languages_ready() {
210 return $this->languages_ready;
211 }
212
213 /**
214 * Sets the internal property `$languages_ready` to `true`, telling that {@see PLL_Model::get_languages_list()} can be used.
215 *
216 * @since 3.4
217 *
218 * @return void
219 */
220 public function set_languages_ready() {
221 $this->languages_ready = true;
222 }
223
224 /**
225 * Cleans language cache
226 * can be called directly with no parameter
227 * called by the 'edited_term_taxonomy' filter with 2 parameters when count needs to be updated
228 *
229 * @since 1.2
230 *
231 * @param int $term not used
232 * @param string $taxonomy taxonomy name
233 * @return void
234 */
235 public function clean_languages_cache( $term = 0, $taxonomy = null ) {
236 if ( empty( $taxonomy ) || 'language' === $taxonomy ) {
237 delete_transient( 'pll_languages_list' );
238 $this->cache->clean();
239 }
240 }
241
242 /**
243 * Don't query term metas when only our taxonomies are queried
244 *
245 * @since 2.3
246 *
247 * @param array $args WP_Term_Query arguments
248 * @return array
249 */
250 public function get_terms_args( $args ) {
251 $taxonomies = $this->translatable_objects->get_taxonomy_names();
252
253 if ( isset( $args['taxonomy'] ) && ! array_diff( (array) $args['taxonomy'], $taxonomies ) ) {
254 $args['update_term_meta_cache'] = false;
255 }
256 return $args;
257 }
258
259 /**
260 * Returns the language by its term_id, tl_term_id, slug or locale.
261 *
262 * @since 0.1
263 * @since 3.4 Allow to get a language by `term_taxonomy_id`.
264 *
265 * @param mixed $value `term_id`, `term_taxonomy_id`, `slug`, `locale`, or `w3c` of the queried language.
266 * `term_id` and `term_taxonomy_id` can be fetched for any language taxonomy.
267 * /!\ For the `term_taxonomy_id`, prefix the ID by `tt:` (ex: `"tt:{$tt_id}"`),
268 * this is to prevent confusion between `term_id` and `term_taxonomy_id`.
269 * @return PLL_Language|false Language object, false if no language found.
270 */
271 public function get_language( $value ) {
272 if ( is_object( $value ) ) {
273 return $value instanceof PLL_Language ? $value : $this->get_language( $value->term_id ); // Will force cast to PLL_Language.
274 }
275
276 $return = $this->cache->get( 'language:' . $value );
277
278 if ( $return instanceof PLL_Language ) {
279 return $return;
280 }
281
282 foreach ( $this->get_languages_list() as $lang ) {
283 foreach ( $lang->get_tax_props() as $props ) {
284 $this->cache->set( 'language:' . $props['term_id'], $lang );
285 $this->cache->set( 'language:tt:' . $props['term_taxonomy_id'], $lang );
286 }
287 $this->cache->set( 'language:' . $lang->slug, $lang );
288 $this->cache->set( 'language:' . $lang->locale, $lang );
289 $this->cache->set( 'language:' . $lang->w3c, $lang );
290 }
291
292 /** @var PLL_Language|false */
293 return $this->cache->get( 'language:' . $value );
294 }
295
296 /**
297 * Returns the default language.
298 *
299 * @since 3.4
300 *
301 * @return PLL_Language|false Default language object, `false` if no language found.
302 */
303 public function get_default_language() {
304 if ( empty( $this->options['default_lang'] ) ) {
305 return false;
306 }
307
308 return $this->get_language( $this->options['default_lang'] );
309 }
310
311 /**
312 * Adds terms clauses to the term query to filter them by languages.
313 *
314 * @since 1.2
315 *
316 * @param string[] $clauses The list of sql clauses in terms query.
317 * @param PLL_Language|false $lang PLL_Language object.
318 * @return string[] Modified list of clauses.
319 */
320 public function terms_clauses( $clauses, $lang ) {
321 if ( ! empty( $lang ) && false === strpos( $clauses['join'], 'pll_tr' ) ) {
322 $clauses['join'] .= $this->term->join_clause();
323 $clauses['where'] .= $this->term->where_clause( $lang );
324 }
325 return $clauses;
326 }
327
328 /**
329 * Returns post types that need to be translated.
330 * The post types list is cached for better better performance.
331 * The method waits for 'after_setup_theme' to apply the cache
332 * to allow themes adding the filter in functions.php.
333 *
334 * @since 1.2
335 *
336 * @param bool $filter True if we should return only valid registered post types.
337 * @return string[] Post type names for which Polylang manages languages and translations.
338 */
339 public function get_translated_post_types( $filter = true ) {
340 return $this->translatable_objects->get( 'post' )->get_translated_object_types( $filter );
341 }
342
343 /**
344 * Returns true if Polylang manages languages and translations for this post type.
345 *
346 * @since 1.2
347 *
348 * @param string|string[] $post_type Post type name or array of post type names.
349 * @return bool
350 */
351 public function is_translated_post_type( $post_type ) {
352 if ( empty( array_filter( (array) $post_type ) ) ) {
353 return false;
354 }
355
356 /** @var non-empty-array<non-empty-string>|non-empty-string $post_type */
357 return $this->translatable_objects->get( 'post' )->is_translated_object_type( $post_type );
358 }
359
360 /**
361 * Returns taxonomies that need to be translated.
362 * The taxonomies list is cached for better better performance.
363 * The method waits for 'after_setup_theme' to apply the cache
364 * to allow themes adding the filter in functions.php.
365 *
366 * @since 1.2
367 *
368 * @param bool $filter True if we should return only valid registered taxonomies.
369 * @return string[] Array of registered taxonomy names for which Polylang manages languages and translations.
370 */
371 public function get_translated_taxonomies( $filter = true ) {
372 return $this->translatable_objects->get( 'term' )->get_translated_object_types( $filter );
373 }
374
375 /**
376 * Returns true if Polylang manages languages and translations for this taxonomy.
377 *
378 * @since 1.2
379 *
380 * @param string|string[] $tax Taxonomy name or array of taxonomy names.
381 * @return bool
382 */
383 public function is_translated_taxonomy( $tax ) {
384 if ( empty( array_filter( (array) $tax ) ) ) {
385 return false;
386 }
387
388 /** @var non-empty-array<non-empty-string>|non-empty-string $tax */
389 return $this->translatable_objects->get( 'term' )->is_translated_object_type( $tax );
390 }
391
392 /**
393 * Return taxonomies that need to be filtered (post_format like).
394 *
395 * @since 1.7
396 *
397 * @param bool $filter True if we should return only valid registered taxonomies.
398 * @return string[] Array of registered taxonomy names.
399 */
400 public function get_filtered_taxonomies( $filter = true ) {
401 if ( did_action( 'after_setup_theme' ) ) {
402 static $taxonomies = null;
403 }
404
405 if ( empty( $taxonomies ) ) {
406 $taxonomies = array( 'post_format' => 'post_format' );
407
408 /**
409 * Filters the list of taxonomies not translatable but filtered by language.
410 * Includes only the post format by default
411 * The filter must be added soon in the WordPress loading process:
412 * in a function hooked to ‘plugins_loaded’ or directly in functions.php for themes.
413 *
414 * @since 1.7
415 *
416 * @param string[] $taxonomies List of taxonomy names.
417 * @param bool $is_settings True when displaying the list of custom taxonomies in Polylang settings.
418 */
419 $taxonomies = apply_filters( 'pll_filtered_taxonomies', $taxonomies, false );
420 }
421
422 return $filter ? array_intersect( $taxonomies, get_taxonomies() ) : $taxonomies;
423 }
424
425 /**
426 * Returns true if Polylang filters this taxonomy per language.
427 *
428 * @since 1.7
429 *
430 * @param string|string[] $tax Taxonomy name or array of taxonomy names.
431 * @return bool
432 */
433 public function is_filtered_taxonomy( $tax ) {
434 $taxonomies = $this->get_filtered_taxonomies( false );
435 return ( is_array( $tax ) && array_intersect( $tax, $taxonomies ) || in_array( $tax, $taxonomies ) );
436 }
437
438 /**
439 * Returns the query vars of all filtered taxonomies.
440 *
441 * @since 1.7
442 *
443 * @return string[]
444 */
445 public function get_filtered_taxonomies_query_vars() {
446 $query_vars = array();
447 foreach ( $this->get_filtered_taxonomies() as $filtered_tax ) {
448 $tax = get_taxonomy( $filtered_tax );
449 if ( ! empty( $tax ) && is_string( $tax->query_var ) ) {
450 $query_vars[] = $tax->query_var;
451 }
452 }
453 return $query_vars;
454 }
455
456 /**
457 * It is possible to have several terms with the same name in the same taxonomy ( one per language )
458 * but the native term_exists() will return true even if only one exists.
459 * So here the function adds the language parameter.
460 *
461 * @since 1.4
462 *
463 * @param string $term_name The term name.
464 * @param string $taxonomy Taxonomy name.
465 * @param int $parent Parent term id.
466 * @param string|PLL_Language $language The language slug or object.
467 * @return int The `term_id` of the found term. 0 otherwise.
468 *
469 * @phpstan-return int<0, max>
470 */
471 public function term_exists( $term_name, $taxonomy, $parent, $language ) {
472 global $wpdb;
473
474 $language = $this->get_language( $language );
475 if ( empty( $language ) ) {
476 return 0;
477 }
478
479 $term_name = trim( wp_unslash( $term_name ) );
480 $term_name = _wp_specialchars( $term_name );
481
482 $select = "SELECT t.term_id FROM $wpdb->terms AS t";
483 $join = " INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id";
484 $join .= $this->term->join_clause();
485 $where = $wpdb->prepare( ' WHERE tt.taxonomy = %s AND t.name = %s', $taxonomy, $term_name );
486 $where .= $this->term->where_clause( $language );
487
488 if ( $parent > 0 ) {
489 $where .= $wpdb->prepare( ' AND tt.parent = %d', $parent );
490 }
491
492 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
493 $term_id = $wpdb->get_var( $select . $join . $where );
494 return max( 0, (int) $term_id );
495 }
496
497 /**
498 * Checks if a term slug exists in a given language, taxonomy, hierarchy.
499 *
500 * @since 1.9
501 * @since 2.8 Moved from PLL_Share_Term_Slug::term_exists() to PLL_Model::term_exists_by_slug().
502 *
503 * @param string $slug The term slug to test.
504 * @param string|PLL_Language $language The language slug or object.
505 * @param string $taxonomy Optional taxonomy name.
506 * @param int $parent Optional parent term id.
507 * @return int The `term_id` of the found term. 0 otherwise.
508 */
509 public function term_exists_by_slug( $slug, $language, $taxonomy = '', $parent = 0 ) {
510 global $wpdb;
511
512 $language = $this->get_language( $language );
513 if ( empty( $language ) ) {
514 return 0;
515 }
516
517 $select = "SELECT t.term_id FROM {$wpdb->terms} AS t";
518 $join = " INNER JOIN {$wpdb->term_taxonomy} AS tt ON t.term_id = tt.term_id";
519 $join .= $this->term->join_clause();
520 $where = $wpdb->prepare( ' WHERE t.slug = %s', $slug );
521 $where .= $this->term->where_clause( $language );
522
523 if ( ! empty( $taxonomy ) ) {
524 $where .= $wpdb->prepare( ' AND tt.taxonomy = %s', $taxonomy );
525 }
526
527 if ( $parent > 0 ) {
528 $where .= $wpdb->prepare( ' AND tt.parent = %d', $parent );
529 }
530
531 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
532 return $wpdb->get_var( $select . $join . $where );
533 }
534
535
536 /**
537 * Returns the number of posts per language in a date, author or post type archive.
538 *
539 * @since 1.2
540 *
541 * @param PLL_Language $lang PLL_Language instance.
542 * @param array $q {
543 * WP_Query arguments:
544 *
545 * @type string|string[] $post_type Post type or array of post types.
546 * @type int $m Combination YearMonth. Accepts any four-digit year and month.
547 * @type int $year Four-digit year.
548 * @type int $monthnum Two-digit month.
549 * @type int $day Day of the month.
550 * @type int $author Author id.
551 * @type string $author_name User 'user_nicename'.
552 * @type string $post_format Post format.
553 * @type string $post_status Post status.
554 * }
555 * @return int
556 *
557 * @phpstan-param array{
558 * post_type?: non-falsy-string|array<non-falsy-string>,
559 * post_status?: non-falsy-string,
560 * m?: numeric-string,
561 * year?: positive-int,
562 * monthnum?: int<1, 12>,
563 * day?: int<1, 31>,
564 * author?: int<1, max>,
565 * author_name?: non-falsy-string,
566 * post_format?: non-falsy-string
567 * } $q
568 * @phpstan-return int<0, max>
569 */
570 public function count_posts( $lang, $q = array() ) {
571 global $wpdb;
572
573 $q = array_merge( array( 'post_type' => 'post', 'post_status' => 'publish' ), $q );
574
575 if ( ! is_array( $q['post_type'] ) ) {
576 $q['post_type'] = array( $q['post_type'] );
577 }
578
579 foreach ( $q['post_type'] as $key => $type ) {
580 if ( ! post_type_exists( $type ) ) {
581 unset( $q['post_type'][ $key ] );
582 }
583 }
584
585 if ( empty( $q['post_type'] ) ) {
586 $q['post_type'] = array( 'post' ); // We *need* a post type.
587 }
588
589 $cache_key = 'pll_count_posts_' . md5( maybe_serialize( $q ) );
590 $counts = wp_cache_get( $cache_key, 'counts' );
591
592 if ( ! is_array( $counts ) ) {
593 $counts = array();
594 $select = "SELECT pll_tr.term_taxonomy_id, COUNT( * ) AS num_posts FROM {$wpdb->posts}";
595 $join = $this->post->join_clause();
596 $where = sprintf( " WHERE post_status = '%s'", esc_sql( $q['post_status'] ) );
597 $where .= sprintf( " AND {$wpdb->posts}.post_type IN ( '%s' )", implode( "', '", esc_sql( $q['post_type'] ) ) );
598 $where .= $this->post->where_clause( $this->get_languages_list() );
599 $groupby = ' GROUP BY pll_tr.term_taxonomy_id';
600
601 if ( ! empty( $q['m'] ) ) {
602 $q['m'] = '' . preg_replace( '|[^0-9]|', '', $q['m'] );
603 $where .= $wpdb->prepare( " AND YEAR( {$wpdb->posts}.post_date ) = %d", substr( $q['m'], 0, 4 ) );
604 if ( strlen( $q['m'] ) > 5 ) {
605 $where .= $wpdb->prepare( " AND MONTH( {$wpdb->posts}.post_date ) = %d", substr( $q['m'], 4, 2 ) );
606 }
607 if ( strlen( $q['m'] ) > 7 ) {
608 $where .= $wpdb->prepare( " AND DAYOFMONTH( {$wpdb->posts}.post_date ) = %d", substr( $q['m'], 6, 2 ) );
609 }
610 }
611
612 if ( ! empty( $q['year'] ) ) {
613 $where .= $wpdb->prepare( " AND YEAR( {$wpdb->posts}.post_date ) = %d", $q['year'] );
614 }
615
616 if ( ! empty( $q['monthnum'] ) ) {
617 $where .= $wpdb->prepare( " AND MONTH( {$wpdb->posts}.post_date ) = %d", $q['monthnum'] );
618 }
619
620 if ( ! empty( $q['day'] ) ) {
621 $where .= $wpdb->prepare( " AND DAYOFMONTH( {$wpdb->posts}.post_date ) = %d", $q['day'] );
622 }
623
624 if ( ! empty( $q['author_name'] ) ) {
625 $author = get_user_by( 'slug', sanitize_title_for_query( $q['author_name'] ) );
626 if ( $author ) {
627 $q['author'] = $author->ID;
628 }
629 }
630
631 if ( ! empty( $q['author'] ) ) {
632 $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_author = %d", $q['author'] );
633 }
634
635 // Filtered taxonomies ( post_format ).
636 foreach ( $this->get_filtered_taxonomies_query_vars() as $tax_qv ) {
637
638 if ( ! empty( $q[ $tax_qv ] ) ) {
639 $join .= " INNER JOIN {$wpdb->term_relationships} AS tr ON tr.object_id = {$wpdb->posts}.ID";
640 $join .= " INNER JOIN {$wpdb->term_taxonomy} AS tt ON tt.term_taxonomy_id = tr.term_taxonomy_id";
641 $join .= " INNER JOIN {$wpdb->terms} AS t ON t.term_id = tt.term_id";
642 $where .= $wpdb->prepare( ' AND t.slug = %s', $q[ $tax_qv ] );
643 }
644 }
645
646 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
647 $res = $wpdb->get_results( $select . $join . $where . $groupby, ARRAY_A );
648 foreach ( (array) $res as $row ) {
649 $counts[ $row['term_taxonomy_id'] ] = $row['num_posts'];
650 }
651
652 wp_cache_set( $cache_key, $counts, 'counts' );
653 }
654
655 $term_taxonomy_id = $lang->get_tax_prop( 'language', 'term_taxonomy_id' );
656 return empty( $counts[ $term_taxonomy_id ] ) ? 0 : $counts[ $term_taxonomy_id ];
657 }
658
659 /**
660 * Setup the links model based on options.
661 *
662 * @since 1.2
663 *
664 * @return PLL_Links_Model
665 */
666 public function get_links_model() {
667 $c = array( 'Directory', 'Directory', 'Subdomain', 'Domain' );
668 $class = get_option( 'permalink_structure' ) ? 'PLL_Links_' . $c[ $this->options['force_lang'] ] : 'PLL_Links_Default';
669
670 /**
671 * Filters the links model class to use.
672 * /!\ this filter is fired *before* the $polylang object is available.
673 *
674 * @since 2.1.1
675 *
676 * @param string $class A class name: PLL_Links_Default, PLL_Links_Directory, PLL_Links_Subdomain, PLL_Links_Domain.
677 */
678 $class = apply_filters( 'pll_links_model', $class );
679
680 return new $class( $this );
681 }
682
683 /**
684 * Returns a list of object IDs without language (used in settings and wizard).
685 *
686 * @since 0.9
687 * @since 2.2.6 Added the `$limit` parameter.
688 * @since 3.4 Added the `$types` parameter.
689 *
690 * @param int $limit Optional. Max number of IDs to return. Defaults to -1 (no limit).
691 * @param string[] $types Optional. Types to handle (@see PLL_Translatable_Object::get_type()). Defaults to
692 * an empty array (all types).
693 * @return int[][]|false {
694 * IDs of objects without language.
695 *
696 * @type int[] $posts Array of post ids.
697 * @type int[] $terms Array of term ids.
698 * }
699 *
700 * @phpstan-param -1|positive-int $limit
701 */
702 public function get_objects_with_no_lang( $limit = -1, array $types = array() ) {
703 /**
704 * Filters the max number of IDs to return when searching objects with no language.
705 * This filter can be used to decrease the memory usage in case the number of objects
706 * without language is too big. Using a negative value is equivalent to have no limit.
707 *
708 * @since 2.2.6
709 * @since 3.4 Added the `$types` parameter.
710 *
711 * @param int $limit Max number of IDs to retrieve from the database.
712 * @param string[] $types Types to handle (@see PLL_Translatable_Object::get_type()). An empty array means all
713 * types.
714 */
715 $limit = apply_filters( 'get_objects_with_no_lang_limit', $limit, $types );
716 $limit = $limit < 1 ? -1 : max( (int) $limit, 1 );
717 $objects = array();
718
719 foreach ( $this->translatable_objects as $type => $object ) {
720 if ( ! empty( $types ) && ! in_array( $type, $types, true ) ) {
721 continue;
722 }
723
724 $ids = $object->get_objects_with_no_lang( $limit );
725
726 if ( empty( $ids ) ) {
727 continue;
728 }
729
730 // The trailing 's' in the array key is for backward compatibility.
731 $objects[ "{$type}s" ] = $ids;
732 }
733
734 $objects = ! empty( $objects ) ? $objects : false;
735
736 /**
737 * Filters the list of IDs of untranslated objects.
738 *
739 * @since 0.9
740 * @since 3.4 Added the `$limit` and `$types` parameters.
741 *
742 * @param int[][]|false $objects List of lists of object IDs, `false` if no IDs found.
743 * @param int $limit Max number of IDs to retrieve from the database.
744 * @param string[] $types Types to handle (@see PLL_Translatable_Object::get_type()). An empty array
745 * means all types.
746 */
747 return apply_filters( 'pll_get_objects_with_no_lang', $objects, $limit, $types );
748 }
749
750 /**
751 * Returns ids of post without language.
752 *
753 * @since 3.1
754 *
755 * @param string|string[] $post_types A translated post type or an array of translated post types.
756 * @param int $limit Max number of objects to return. `-1` to return all of them.
757 * @return int[]
758 *
759 * @phpstan-param -1|positive-int $limit
760 * @phpstan-return list<positive-int>
761 */
762 public function get_posts_with_no_lang( $post_types, $limit ) {
763 return $this->translatable_objects->get( 'post' )->get_objects_with_no_lang( $limit, (array) $post_types );
764 }
765
766 /**
767 * Returns ids of terms without language.
768 *
769 * @since 3.1
770 *
771 * @param string|string[] $taxonomies A translated taxonomy or an array of taxonomies post types.
772 * @param int $limit Max number of objects to return. `-1` to return all of them.
773 * @return int[]
774 *
775 * @phpstan-param -1|positive-int $limit
776 * @phpstan-return list<positive-int>
777 */
778 public function get_terms_with_no_lang( $taxonomies, $limit ) {
779 return $this->translatable_objects->get( 'term' )->get_objects_with_no_lang( $limit, (array) $taxonomies );
780 }
781
782 /**
783 * Assigns the default language to objects in mass.
784 *
785 * @since 1.2
786 * @since 3.4 Moved from PLL_Admin_Model class.
787 * Removed `$limit` parameter, added `$lang` and `$types` parameters.
788 *
789 * @param PLL_Language|null $lang Optional. The language to assign to objects. Defaults to `null` (default language).
790 * @param string[] $types Optional. Types to handle (@see PLL_Translatable_Object::get_type()). Defaults
791 * to an empty array (all types).
792 * @return void
793 */
794 public function set_language_in_mass( $lang = null, array $types = array() ) {
795 if ( ! $lang instanceof PLL_Language ) {
796 $lang = $this->get_default_language();
797
798 if ( empty( $lang ) ) {
799 return;
800 }
801 }
802
803 // 1000 is an arbitrary value that will be filtered by `get_objects_with_no_lang_limit`.
804 $nolang = $this->get_objects_with_no_lang( 1000, $types );
805
806 if ( empty( $nolang ) ) {
807 return;
808 }
809
810 /**
811 * Keep track of types where we set the language:
812 * those are types where we may have more items to process if we have more than 1000 items in total.
813 * This will prevent unecessary SQL queries in the next recursion: if we have 0 items in this recursion for
814 * a type, we'll still have 0 in the next one, no need for a new query.
815 */
816 $types_with_objects = array();
817
818 foreach ( $this->translatable_objects as $type => $object ) {
819 if ( empty( $nolang[ "{$type}s" ] ) ) {
820 continue;
821 }
822
823 if ( ! empty( $types ) && ! in_array( $type, $types, true ) ) {
824 continue;
825 }
826
827 $object->set_language_in_mass( $nolang[ "{$type}s" ], $lang );
828 $types_with_objects[] = $type;
829 }
830
831 if ( empty( $types_with_objects ) ) {
832 return;
833 }
834
835 $this->set_language_in_mass( $lang, $types_with_objects );
836 }
837
838 /**
839 * Filters the ORDERBY clause of the languages query.
840 *
841 * This allows to order languages terms by `taxonomy` first then by `term_group` and `term_id`.
842 * Ordering terms by taxonomy allows not to mix terms between all language taxomonomies.
843 * Having the "language' taxonomy first is important for {@see PLL_Admin_Model:delete_language()}.
844 *
845 * @since 3.2.3
846 *
847 * @param string $orderby `ORDERBY` clause of the terms query.
848 * @param array $args An array of term query arguments.
849 * @param string[] $taxonomies An array of taxonomy names.
850 * @return string
851 */
852 public function filter_language_terms_orderby( $orderby, $args, $taxonomies ) {
853 $allowed_taxonomies = $this->translatable_objects->get_taxonomy_names( array( 'language' ) );
854
855 if ( ! is_array( $taxonomies ) || ! empty( array_diff( $taxonomies, $allowed_taxonomies ) ) ) {
856 return $orderby;
857 }
858
859 if ( empty( $orderby ) || ! is_string( $orderby ) ) {
860 return $orderby;
861 }
862
863 if ( ! preg_match( '@^(?<alias>[^.]+)\.term_group$@', $orderby, $matches ) ) {
864 return $orderby;
865 }
866
867 return sprintf( 'tt.taxonomy = \'language\' DESC, %1$s.term_group, %1$s.term_id', $matches['alias'] );
868 }
869
870 /**
871 * Maybe adds the missing language terms for 3rd party language taxonomies.
872 *
873 * @since 3.4
874 *
875 * @return void
876 */
877 public function maybe_create_language_terms() {
878 $registered_taxonomies = array_diff(
879 $this->translatable_objects->get_taxonomy_names( array( 'language' ) ),
880 // Exclude the post and term language taxonomies from the list.
881 array( $this->post->get_tax_language(), $this->term->get_tax_language() )
882 );
883
884 if ( empty( $registered_taxonomies ) ) {
885 // No 3rd party language taxonomies.
886 return;
887 }
888
889 // We have at least one 3rd party language taxonomy.
890 $known_taxonomies = ! empty( $this->options['language_taxonomies'] ) && is_array( $this->options['language_taxonomies'] ) ? $this->options['language_taxonomies'] : array();
891 $new_taxonomies = array_diff( $registered_taxonomies, $known_taxonomies );
892
893 if ( empty( $new_taxonomies ) ) {
894 // No new 3rd party language taxonomies.
895 return;
896 }
897
898 // We have at least one unknown 3rd party language taxonomy.
899 foreach ( $this->get_languages_list() as $language ) {
900 $this->update_secondary_language_terms( $language->slug, $language->name, $language, $new_taxonomies );
901 }
902
903 // Clear the cache, so the new `term_id` and `term_taxonomy_id` appear in the languages list.
904 $this->clean_languages_cache();
905
906 // Keep the previous values, so this is triggered only once per taxonomy.
907 $this->options['language_taxonomies'] = array_merge( $known_taxonomies, $new_taxonomies );
908 update_option( 'polylang', $this->options );
909 }
910
911 /**
912 * Updates or adds new terms for a secondary language taxonomy (aka not 'language').
913 *
914 * @since 3.4
915 *
916 * @param string $slug Language term slug (with or without the `pll_` prefix).
917 * @param string $name Language name (label).
918 * @param PLL_Language|null $language Optional. A language object. Required to update the existing terms.
919 * @param string[] $taxonomies Optional. List of language taxonomies to deal with. An empty value means
920 * all of them. Defauls to all taxonomies.
921 * @return void
922 *
923 * @phpstan-param non-empty-string $slug
924 * @phpstan-param non-empty-string $name
925 * @phpstan-param array<non-empty-string> $taxonomies
926 */
927 protected function update_secondary_language_terms( $slug, $name, PLL_Language $language = null, array $taxonomies = array() ) {
928 $slug = 0 === strpos( $slug, 'pll_' ) ? $slug : "pll_$slug";
929
930 foreach ( $this->translatable_objects->get_secondary_translatable_objects() as $object ) {
931 if ( ! empty( $taxonomies ) && ! in_array( $object->get_tax_language(), $taxonomies, true ) ) {
932 // Not in the list.
933 continue;
934 }
935
936 if ( ! empty( $language ) ) {
937 $term_id = $language->get_tax_prop( $object->get_tax_language(), 'term_id' );
938 } else {
939 $term_id = 0;
940 }
941
942 if ( empty( $term_id ) ) {
943 // Attempt to repair the language if a term has been deleted by a database cleaning tool.
944 wp_insert_term( $name, $object->get_tax_language(), array( 'slug' => $slug ) );
945 continue;
946 }
947
948 /** @var PLL_Language $language */
949 if ( "pll_{$language->slug}" !== $slug || $language->name !== $name ) {
950 // Something has changed.
951 wp_update_term( $term_id, $object->get_tax_language(), array( 'slug' => $slug, 'name' => $name ) );
952 }
953 }
954 }
955
956 /**
957 * Returns the list of available languages, based on the language taxonomy terms.
958 * Stores the list in a db transient and in a `PLL_Cache` object.
959 *
960 * @since 3.4
961 *
962 * @return PLL_Language[] An array of `PLL_Language` objects, array keys are the type.
963 *
964 * @phpstan-return list<PLL_Language>
965 */
966 protected function get_languages_from_taxonomies() {
967 $terms_by_slug = array();
968
969 foreach ( $this->get_language_terms() as $term ) {
970 // Except for language taxonomy term slugs, remove 'pll_' prefix from the other language taxonomy term slugs.
971 $key = 'language' === $term->taxonomy ? $term->slug : substr( $term->slug, 4 );
972 $terms_by_slug[ $key ][ $term->taxonomy ] = $term;
973 }
974
975 /**
976 * @var (
977 * array{
978 * string: array{
979 * language: WP_Term,
980 * }&array<non-empty-string, WP_Term>
981 * }
982 * ) $terms_by_slug
983 */
984 $languages = array_filter(
985 array_map(
986 array( new PLL_Language_Factory( $this->options ), 'get_from_terms' ),
987 array_values( $terms_by_slug )
988 )
989 );
990
991 /**
992 * Filters the list of languages *before* it is stored in the persistent cache.
993 * /!\ This filter is fired *before* the $polylang object is available.
994 *
995 * @since 1.7.5
996 * @since 3.4 Deprecated.
997 * @deprecated
998 *
999 * @param PLL_Language[] $languages The list of language objects.
1000 * @param PLL_Model $model PLL_Model object.
1001 */
1002 $languages = apply_filters_deprecated( 'pll_languages_list', array( $languages, $this ), '3.4', 'pll_additional_language_data' );
1003
1004 if ( ! $this->are_languages_ready() ) {
1005 // Do not cache an incomplete list.
1006 /** @var list<PLL_Language> $languages */
1007 return $languages;
1008 }
1009
1010 /**
1011 * Don't store directly objects as it badly break with some hosts ( GoDaddy ) due to race conditions when using object cache.
1012 * Thanks to captin411 for catching this!
1013 *
1014 * @see https://wordpress.org/support/topic/fatal-error-pll_model_languages_list?replies=8#post-6782255
1015 */
1016 $languages_data = array_map(
1017 function ( $language ) {
1018 return $language->to_array( 'db' );
1019 },
1020 $languages
1021 );
1022
1023 set_transient( 'pll_languages_list', $languages_data );
1024
1025 /** @var list<PLL_Language> $languages */
1026 return $languages;
1027 }
1028
1029 /**
1030 * Returns the list of existing language terms.
1031 * - Returns all terms, that are or not assigned to posts.
1032 * - Terms are ordered by `term_group` and `term_id` (see `PLL_Model->filter_language_terms_orderby()`).
1033 *
1034 * @since 3.2.3
1035 *
1036 * @return WP_Term[]
1037 */
1038 protected function get_language_terms() {
1039 add_filter( 'get_terms_orderby', array( $this, 'filter_language_terms_orderby' ), 10, 3 );
1040 $terms = get_terms(
1041 array(
1042 'taxonomy' => $this->translatable_objects->get_taxonomy_names( array( 'language' ) ),
1043 'orderby' => 'term_group',
1044 'hide_empty' => false,
1045 )
1046 );
1047 remove_filter( 'get_terms_orderby', array( $this, 'filter_language_terms_orderby' ) );
1048
1049 return empty( $terms ) || is_wp_error( $terms ) ? array() : $terms;
1050 }
1051 }
1052