PluginProbe
Polylang / 3.8.7
Polylang v3.8.7
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 / src / Model / Languages.php

Languages.php in Polylang 3.8.7, at src/Model/Languages.php

1,409 lines 46.4 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 namespace WP_Syntex\Polylang\Model;
7
8 use PLL_Cache;
9 use PLL_Language;
10 use PLL_Language_Factory;
11 use PLL_Translatable_Objects;
12 use WP_Error;
13 use WP_Term;
14 use WP_Syntex\Polylang\Options\Options;
15
16 defined( 'ABSPATH' ) || exit;
17
18 /**
19 * Model for the languages.
20 *
21 * @since 3.7
22 */
23 class Languages {
24 public const INNER_LOCALE_PATTERN = '[a-z]{2,3}(?:_[A-Z]{2})?(?:_[a-z0-9]+)?';
25 public const INNER_SLUG_PATTERN = '[a-z][a-z0-9_-]*';
26
27 public const LOCALE_PATTERN = '^' . self::INNER_LOCALE_PATTERN . '$';
28 public const SLUG_PATTERN = '^' . self::INNER_SLUG_PATTERN . '$';
29
30 public const TRANSIENT_NAME = 'pll_languages_list';
31 private const CACHE_KEY = 'languages';
32
33 /**
34 * Polylang's options.
35 *
36 * @var Options
37 */
38 private $options;
39
40 /**
41 * Translatable objects registry.
42 *
43 * @var PLL_Translatable_Objects
44 */
45 private $translatable_objects;
46
47 /**
48 * Internal non persistent cache object.
49 *
50 * @var PLL_Cache<mixed>
51 */
52 private $cache;
53
54 /**
55 * Flag set to true during the language objects creation.
56 *
57 * @var bool
58 */
59 private $is_creating_list = false;
60
61 /**
62 * Tells if {@see WP_Syntex\Polylang\Model\Languages::get_list()} can be used.
63 *
64 * @var bool
65 */
66 private $languages_ready = false;
67
68 /**
69 * List of automatic language proxies.
70 *
71 * @var Languages_Proxy_Interface[]
72 *
73 * @phpstan-var array<non-falsy-string, Languages_Proxy_Interface>
74 */
75 private $automatic_proxies = array();
76
77 /**
78 * List of language proxies.
79 *
80 * @var Languages_Proxy_Interface[]
81 *
82 * @phpstan-var array<non-falsy-string, Languages_Proxy_Interface>
83 */
84 private $proxies = array();
85
86 /**
87 * Constructor.
88 *
89 * @since 3.7
90 *
91 * @param Options $options Polylang's options.
92 * @param PLL_Translatable_Objects $translatable_objects Translatable objects registry.
93 * @param PLL_Cache $cache Internal non persistent cache object.
94 *
95 * @phpstan-param PLL_Cache<mixed> $cache
96 */
97 public function __construct( Options $options, PLL_Translatable_Objects $translatable_objects, PLL_Cache $cache ) {
98 $this->options = $options;
99 $this->translatable_objects = $translatable_objects;
100 $this->cache = $cache;
101 }
102
103 /**
104 * Returns the language by its term_id, tl_term_id, slug or locale.
105 *
106 * @since 0.1
107 * @since 3.4 Allow to get a language by `term_taxonomy_id`.
108 * @since 3.7 Moved from `PLL_Model::get_language()` to `WP_Syntex\Polylang\Model\Languages::get()`.
109 *
110 * @param mixed $value `term_id`, `term_taxonomy_id`, `slug`, `locale`, or `w3c` of the queried language.
111 * `term_id` and `term_taxonomy_id` can be fetched for any language taxonomy.
112 * /!\ For the `term_taxonomy_id`, prefix the ID by `tt:` (ex: `"tt:{$tt_id}"`),
113 * this is to prevent confusion between `term_id` and `term_taxonomy_id`.
114 * @return PLL_Language|false Language object, false if no language found.
115 *
116 * @phpstan-param PLL_Language|WP_Term|int|string $value
117 */
118 public function get( $value ) {
119 if ( $value instanceof PLL_Language ) {
120 return $value;
121 }
122
123 // Cast WP_Term to PLL_Language.
124 if ( $value instanceof WP_Term ) {
125 return $this->get( $value->term_id );
126 }
127
128 $return = $this->cache->get( 'language:' . $value );
129
130 if ( $return instanceof PLL_Language ) {
131 return $return;
132 }
133
134 foreach ( $this->get_list() as $lang ) {
135 foreach ( $lang->get_tax_props() as $props ) {
136 $this->cache->set( 'language:' . $props['term_id'], $lang );
137 $this->cache->set( 'language:tt:' . $props['term_taxonomy_id'], $lang );
138 }
139 $this->cache->set( 'language:' . $lang->slug, $lang );
140 $this->cache->set( 'language:' . $lang->locale, $lang );
141 $this->cache->set( 'language:' . $lang->w3c, $lang );
142 }
143
144 /** @var PLL_Language|false */
145 return $this->cache->get( 'language:' . $value );
146 }
147
148 /**
149 * Adds a new language and creates a default category for this language.
150 *
151 * @since 1.2
152 * @since 3.7 Moved from `PLL_Admin_Model::add_language()` to `WP_Syntex\Polylang\Model\Languages::add()`.
153 * @since 3.8 Returns the new object language.
154 *
155 * @param array $args {
156 * Arguments used to create the language.
157 *
158 * @type string $locale WordPress locale. If something wrong is used for the locale, the .mo files will
159 * not be loaded...
160 * @type string $name Optional. Language name (used only for display). Default to the language name from {@see src/settings/languages.php}.
161 * @type string $slug Optional. Language code (ideally 2-letters ISO 639-1 language code). Default to the language code from {@see src/settings/languages.php}.
162 * @type bool $rtl Optional. True if rtl language, false otherwise. Default is false.
163 * @type bool $is_rtl Optional. True if rtl language, false otherwise. Will be converted to rtl. Default is false.
164 * @type int $term_group Optional. Language order when displayed. Default is 0.
165 * @type string $flag Optional. Country code, {@see src/settings/flags.php}.
166 * @type string $flag_code Optional. Country code, {@see src/settings/flags.php}. Will be converted to flag.
167 * @type bool $no_default_cat Optional. If set, no default category will be created for this language. Default is false.
168 * }
169 * @return PLL_Language|WP_Error The object language on success, a `WP_Error` otherwise.
170 *
171 * @phpstan-param array{
172 * name?: string,
173 * slug?: string,
174 * locale?: string,
175 * rtl?: bool,
176 * is_rtl?: bool,
177 * term_group?: int|numeric-string,
178 * flag?: string,
179 * flag_code?: string,
180 * no_default_cat?: bool
181 * } $args
182 */
183 public function add( $args ) {
184 $args['rtl'] = $args['rtl'] ?? $args['is_rtl'] ?? null;
185 $args['flag'] = $args['flag'] ?? $args['flag_code'] ?? null;
186 $args['term_group'] = $args['term_group'] ?? 0;
187
188 if ( ! empty( $args['locale'] ) && ( ! isset( $args['name'] ) || ! isset( $args['slug'] ) ) ) {
189 $languages = include POLYLANG_DIR . '/src/settings/languages.php';
190 if ( ! empty( $languages[ $args['locale'] ] ) ) {
191 $found = $languages[ $args['locale'] ];
192 $args['name'] = $args['name'] ?? $found['name'];
193 $args['slug'] = $args['slug'] ?? $found['code'];
194 $args['rtl'] = $args['rtl'] ?? 'rtl' === $found['dir'];
195 $args['flag'] = $args['flag'] ?? $found['flag'];
196 }
197 }
198
199 $errors = $this->validate_lang( $args );
200 if ( $errors->has_errors() ) {
201 return $errors;
202 }
203
204 /**
205 * @phpstan-var array{
206 * name: non-empty-string,
207 * slug: non-empty-string,
208 * locale: non-empty-string,
209 * rtl: bool,
210 * term_group: int|numeric-string,
211 * flag?: non-empty-string,
212 * no_default_cat?: bool
213 * } $args
214 */
215 // First the language taxonomy.
216 $result = wp_insert_term(
217 $args['name'],
218 'language',
219 array(
220 'slug' => $args['slug'],
221 'description' => $this->build_metas( $args ),
222 )
223 );
224 if ( is_wp_error( $result ) ) {
225 return new WP_Error( 'pll_add_language', __( 'Could not add the language.', 'polylang' ) );
226 }
227
228 $id = (int) $result['term_id'];
229
230 $result = wp_update_term( $id, 'language', array( 'term_group' => (int) $args['term_group'] ) ); // Can't set the term group directly in `wp_insert_term()`.
231 if ( is_wp_error( $result ) ) {
232 return new WP_Error( 'pll_add_language', __( 'Could not set the language order.', 'polylang' ) );
233 }
234
235 // The other language taxonomies.
236 $errors = $this->update_secondary_language_terms( $args['slug'], $args['name'] );
237 if ( $errors->has_errors() ) {
238 return $errors;
239 }
240
241 if ( empty( $this->options['default_lang'] ) ) {
242 // If this is the first language created, set it as default language
243 $this->options['default_lang'] = $args['slug'];
244 }
245
246 // Refresh languages.
247 $this->clean_cache();
248 $new_language = $this->get( $id );
249
250 if ( ! $new_language ) {
251 return new WP_Error( 'pll_add_language', __( 'Could not add the language.', 'polylang' ) );
252 }
253
254 flush_rewrite_rules(); // Refresh rewrite rules.
255
256 /**
257 * Fires after a language is added.
258 *
259 * @since 1.9
260 *
261 * @param array $args {
262 * Arguments used to create the language.
263 *
264 * @type string $name Language name (used only for display).
265 * @type string $slug Language code (ideally 2-letters ISO 639-1 language code).
266 * @type string $locale WordPress locale. If something wrong is used for the locale, the .mo files will
267 * not be loaded...
268 * @type bool $rtl True if rtl language, false otherwise.
269 * @type int $term_group Language order when displayed.
270 * @type string $flag Optional. Country code, {@see src/settings/flags.php}.
271 * @type bool $no_default_cat Optional. If set, no default category will be created for this language.
272 * }
273 */
274 do_action( 'pll_add_language', $args );
275
276 return $new_language;
277 }
278
279 /**
280 * Updates language properties.
281 *
282 * @since 1.2
283 * @since 3.7 Moved from `PLL_Admin_Model::update_language()` to `WP_Syntex\Polylang\Model\Languages::update()`.
284 * @since 3.8 Returns the updated object language.
285 *
286 * @param array $args {
287 * Arguments used to modify the language.
288 *
289 * @type int $lang_id ID of the language to modify.
290 * @type string $name Optional. Language name (used only for display).
291 * @type string $slug Optional. Language code (ideally 2-letters ISO 639-1 language code).
292 * @type string $locale Optional. WordPress locale. If something wrong is used for the locale, the .mo files will
293 * not be loaded...
294 * @type bool $rtl Optional. True if rtl language, false otherwise.
295 * @type bool $is_rtl Optional. True if rtl language, false otherwise. Will be converted to rtl.
296 * @type int $term_group Optional. Language order when displayed.
297 * @type string $flag Optional, country code, {@see src/settings/flags.php}.
298 * @type string $flag_code Optional. Country code, {@see src/settings/flags.php}. Will be converted to flag.
299 * }
300 * @return PLL_Language|WP_Error The updated object language on success, a `WP_Error` otherwise.
301 *
302 * @phpstan-param array{
303 * lang_id: int|numeric-string,
304 * name?: string,
305 * slug?: string,
306 * locale?: string,
307 * rtl?: bool,
308 * is_rtl?: bool,
309 * term_group?: int|numeric-string,
310 * flag?: string,
311 * flag_code?: string
312 * } $args
313 */
314 public function update( $args ) {
315 $id = (int) $args['lang_id'];
316 $lang = $this->get( $id );
317
318 if ( empty( $lang ) ) {
319 return new WP_Error( 'pll_invalid_language_id', __( 'The language does not seem to exist.', 'polylang' ) );
320 }
321
322 $args['locale'] = $args['locale'] ?? $lang->locale;
323 $args['name'] = $args['name'] ?? $lang->name;
324 $args['slug'] = $args['slug'] ?? $lang->slug;
325 $args['rtl'] = $args['rtl'] ?? $args['is_rtl'] ?? $lang->is_rtl;
326 $args['flag'] = $args['flag'] ?? $args['flag_code'] ?? $lang->flag_code;
327 $args['term_group'] = $args['term_group'] ?? $lang->term_group;
328
329 $errors = $this->validate_lang( $args, $lang );
330 if ( $errors->has_errors() ) {
331 return $errors;
332 }
333
334 /**
335 * @phpstan-var array{
336 * lang_id: int|numeric-string,
337 * name: non-empty-string,
338 * slug: non-empty-string,
339 * locale: non-empty-string,
340 * rtl: bool,
341 * term_group: int|numeric-string,
342 * flag?: non-empty-string
343 * } $args
344 */
345 // Update links to this language in posts and terms in case the slug has been modified.
346 $slug = $args['slug'];
347 $old_slug = $lang->slug;
348
349 // Update the language itself.
350 $errors = $this->update_secondary_language_terms( $args['slug'], $args['name'], $lang );
351 if ( $errors->has_errors() ) {
352 return $errors;
353 }
354
355 $result = wp_update_term(
356 $lang->get_tax_prop( 'language', 'term_id' ),
357 'language',
358 array(
359 'slug' => $slug,
360 'name' => $args['name'],
361 'description' => $this->build_metas( $args ),
362 'term_group' => (int) $args['term_group'],
363 )
364 );
365 if ( is_wp_error( $result ) ) {
366 return new WP_Error( 'pll_update_language', __( 'Could not update the language.', 'polylang' ) );
367 }
368
369 if ( $old_slug !== $slug ) {
370 // Update the language slug in translations.
371 $errors = $this->update_translations( $old_slug, $slug );
372 if ( $errors->has_errors() ) {
373 return $errors;
374 }
375
376 // Update language option in widgets.
377 foreach ( $GLOBALS['wp_registered_widgets'] as $widget ) {
378 if ( ! empty( $widget['callback'][0] ) && ! empty( $widget['params'][0]['number'] ) ) {
379 $obj = $widget['callback'][0];
380 $number = $widget['params'][0]['number'];
381 if ( is_object( $obj ) && method_exists( $obj, 'get_settings' ) && method_exists( $obj, 'save_settings' ) ) {
382 $settings = $obj->get_settings();
383 if ( isset( $settings[ $number ]['pll_lang'] ) && $settings[ $number ]['pll_lang'] == $old_slug ) {
384 $settings[ $number ]['pll_lang'] = $slug;
385 $obj->save_settings( $settings );
386 }
387 }
388 }
389 }
390
391 // Update menus locations in options.
392 $nav_menus = $this->options->get( 'nav_menus' );
393
394 if ( ! empty( $nav_menus ) ) {
395 foreach ( $nav_menus as $theme => $locations ) {
396 foreach ( array_keys( $locations ) as $location ) {
397 if ( isset( $nav_menus[ $theme ][ $location ][ $old_slug ] ) ) {
398 $nav_menus[ $theme ][ $location ][ $slug ] = $nav_menus[ $theme ][ $location ][ $old_slug ];
399 unset( $nav_menus[ $theme ][ $location ][ $old_slug ] );
400 }
401 }
402 }
403
404 $this->options->set( 'nav_menus', $nav_menus );
405 }
406
407 /*
408 * Update domains in options.
409 * This must happen after the term is saved (see `Options\Business\Domains::sanitize()`).
410 */
411 $domains = $this->options->get( 'domains' );
412
413 if ( isset( $domains[ $old_slug ] ) ) {
414 $domains[ $slug ] = $domains[ $old_slug ];
415 unset( $domains[ $old_slug ] );
416 $this->options->set( 'domains', $domains );
417 }
418
419 /*
420 * Update the default language option if necessary.
421 * This must happen after the term is saved (see `Options\Business\Default_Lang::sanitize()`).
422 */
423 if ( $lang->is_default ) {
424 $this->options->set( 'default_lang', $slug );
425 }
426 }
427
428 // Refresh languages.
429 $this->clean_cache();
430 $updated_language = $this->get( $id );
431
432 if ( ! $updated_language ) {
433 return new WP_Error( 'pll_update_language', __( 'Could not update the language.', 'polylang' ) );
434 }
435
436 // Refresh rewrite rules.
437 flush_rewrite_rules();
438
439 /**
440 * Fires after a language is updated.
441 *
442 * @since 1.9
443 * @since 3.2 Added $lang parameter.
444 *
445 * @param array $args {
446 * Arguments used to modify the language. @see WP_Syntex\Polylang\Model\Languages::update().
447 *
448 * @type string $name Language name (used only for display).
449 * @type string $slug Language code (ideally 2-letters ISO 639-1 language code).
450 * @type string $locale WordPress locale.
451 * @type bool $rtl True if rtl language, false otherwise.
452 * @type int $term_group Language order when displayed.
453 * @type string $no_default_cat Optional, if set, no default category has been created for this language.
454 * @type string $flag Optional, country code, @see src/settings/flags.php.
455 * }
456 * @param PLL_Language $lang Previous value of the language being edited.
457 */
458 do_action( 'pll_update_language', $args, $lang );
459
460 return $updated_language;
461 }
462
463 /**
464 * Deletes a language.
465 *
466 * @since 1.2
467 * @since 3.7 Moved from `PLL_Admin_Model::delete_language()` to `WP_Syntex\Polylang\Model\Languages::delete()`.
468 *
469 * @param int $lang_id Language term_id.
470 * @return bool
471 */
472 public function delete( $lang_id ): bool {
473 $lang = $this->get( (int) $lang_id );
474
475 if ( empty( $lang ) ) {
476 return false;
477 }
478
479 // Oops! We are deleting the default language...
480 // Need to do this before losing the information for default category translations.
481 if ( $lang->is_default ) {
482 $slugs = $this->get_list( array( 'fields' => 'slug' ) );
483 $slugs = array_diff( $slugs, array( $lang->slug ) );
484
485 if ( ! empty( $slugs ) ) {
486 $this->update_default( reset( $slugs ) ); // Arbitrary choice...
487 } else {
488 unset( $this->options['default_lang'] );
489 }
490 }
491
492 // Delete the translations.
493 $this->update_translations( $lang->slug );
494
495 // Delete language option in widgets.
496 foreach ( $GLOBALS['wp_registered_widgets'] as $widget ) {
497 if ( ! empty( $widget['callback'][0] ) && ! empty( $widget['params'][0]['number'] ) ) {
498 $obj = $widget['callback'][0];
499 $number = $widget['params'][0]['number'];
500 if ( is_object( $obj ) && method_exists( $obj, 'get_settings' ) && method_exists( $obj, 'save_settings' ) ) {
501 $settings = $obj->get_settings();
502 if ( isset( $settings[ $number ]['pll_lang'] ) && $settings[ $number ]['pll_lang'] == $lang->slug ) {
503 unset( $settings[ $number ]['pll_lang'] );
504 $obj->save_settings( $settings );
505 }
506 }
507 }
508 }
509
510 // Delete menus locations.
511 $nav_menus = $this->options->get( 'nav_menus' );
512
513 if ( ! empty( $nav_menus ) ) {
514 foreach ( $nav_menus as $theme => $locations ) {
515 foreach ( array_keys( $locations ) as $location ) {
516 unset( $nav_menus[ $theme ][ $location ][ $lang->slug ] );
517 }
518 }
519
520 $this->options->set( 'nav_menus', $nav_menus );
521 }
522
523 // Delete users options.
524 delete_metadata( 'user', 0, 'pll_filter_content', '', true );
525 delete_metadata( 'user', 0, "description_{$lang->slug}", '', true );
526
527 // Delete domain.
528 $domains = $this->options->get( 'domains' );
529 unset( $domains[ $lang->slug ] );
530 $this->options->set( 'domains', $domains );
531
532 /*
533 * Delete the language itself.
534 *
535 * Reverses the language taxonomies order is required to make sure 'language' is deleted in last.
536 *
537 * The initial order with the 'language' taxonomy at the beginning of 'PLL_Language::term_props' property
538 * is done by {@see PLL_Model::filter_terms_orderby()}
539 */
540 foreach ( array_reverse( $lang->get_tax_props( 'term_id' ) ) as $taxonomy_name => $term_id ) {
541 wp_delete_term( $term_id, $taxonomy_name );
542 }
543
544 // Refresh languages.
545 $this->clean_cache();
546 $this->get_list();
547
548 flush_rewrite_rules(); // refresh rewrite rules
549 return true;
550 }
551
552 /**
553 * Checks if there are languages or not.
554 *
555 * @since 3.3
556 * @since 3.7 Moved from `PLL_Model::has_languages()` to `WP_Syntex\Polylang\Model\Languages::has()`.
557 *
558 * @return bool True if there are, false otherwise.
559 */
560 public function has(): bool {
561 if ( ! empty( $this->cache->get( self::CACHE_KEY ) ) ) {
562 return true;
563 }
564
565 if ( ! empty( get_transient( self::TRANSIENT_NAME ) ) ) {
566 return true;
567 }
568
569 return ! empty( $this->get_terms() );
570 }
571
572 /**
573 * Returns the list of available languages.
574 * - Stores the list in a db transient (except flags), unless `PLL_CACHE_LANGUAGES` is set to false.
575 * - Caches the list (with flags) in a `PLL_Cache` object.
576 *
577 * @since 0.1
578 * @since 3.7 Moved from `PLL_Model::get_languages_list()` to `WP_Syntex\Polylang\Model\Languages::get_list()`.
579 *
580 * @param array $args {
581 * @type string $fields Returns only that field if set; {@see PLL_Language} for a list of fields.
582 * @type bool $hide_empty Hides languages with no posts if set to `true` (defaults to `false`). Deprecated, use the `hide_empty` filter instead.
583 * @type bool $hide_default Hides default language from the list (default to `false`). Deprecated, use the `hide_default` filter instead.
584 * }
585 * @return array List of PLL_Language objects or PLL_Language object properties.
586 */
587 public function get_list( $args = array() ): array {
588 if ( ! $this->are_ready() ) {
589 _doing_it_wrong(
590 __METHOD__ . '()',
591 "It must not be called before the hook 'pll_pre_init'.",
592 '3.4'
593 );
594 }
595
596 if ( ! empty( $args['hide_empty'] ) ) {
597 _deprecated_argument(
598 __METHOD__,
599 '3.8',
600 sprintf(
601 /* translators: 1: argument name, 2: PHP code snippet. */
602 esc_html__( 'The argument %1$s has been replaced by %2$s.', 'polylang' ),
603 '<code>hide_empty</code>',
604 '<code>PLL()->model->languages->filter( \'hide_empty\' )->get_list()</code>'
605 )
606 );
607 }
608
609 if ( ! empty( $args['hide_default'] ) ) {
610 _deprecated_argument(
611 __METHOD__,
612 '3.8',
613 sprintf(
614 /* translators: 1: argument name, 2: PHP code snippet. */
615 esc_html__( 'The argument %1$s has been replaced by %2$s.', 'polylang' ),
616 '<code>hide_default</code>',
617 '<code>PLL()->model->languages->filter( \'hide_default\' )->get_list()</code>'
618 )
619 );
620 }
621
622 $languages = $this->cache->get( self::CACHE_KEY );
623
624 if ( ! is_array( $languages ) ) {
625 // Bail out early if languages are currently created to avoid an infinite loop.
626 if ( $this->is_creating_list ) {
627 return array();
628 }
629
630 $this->is_creating_list = true;
631
632 if ( ! pll_get_constant( 'PLL_CACHE_LANGUAGES', true ) ) {
633 // Create the languages from taxonomies.
634 $languages = $this->get_from_taxonomies();
635 } else {
636 $languages = get_transient( self::TRANSIENT_NAME );
637
638 if ( empty( $languages ) || ! is_array( $languages ) || empty( reset( $languages )['term_props'] ) ) { // Test `term_props` in case we got a transient older than 3.4.
639 // Create the languages from taxonomies.
640 $languages = $this->get_from_taxonomies();
641 } else {
642 // Create the languages directly from arrays stored in the transient.
643 $languages = array_map(
644 array( new PLL_Language_Factory( $this->options ), 'get' ),
645 $languages
646 );
647
648 // Remove potential empty language.
649 $languages = array_filter( $languages );
650
651 // Re-index.
652 $languages = array_values( $languages );
653 }
654 }
655
656 /**
657 * Filters the list of languages *after* it is stored in the persistent cache.
658 * /!\ This filter is fired *before* the $polylang object is available.
659 *
660 * @since 1.8
661 * @since 3.4 Deprecated. If you used this hook to filter URLs, you may hook `'site_url'` instead.
662 * @deprecated
663 *
664 * @param PLL_Language[] $languages The list of language objects.
665 */
666 $languages = apply_filters_deprecated( 'pll_after_languages_cache', array( $languages ), '3.4' );
667
668 foreach ( $this->automatic_proxies as $proxy ) {
669 $languages = $proxy->filter( $languages );
670 }
671
672 if ( $this->are_ready() ) {
673 $this->cache->set( self::CACHE_KEY, $languages );
674 }
675
676 $this->is_creating_list = false;
677 }
678
679 // Backward compatibility with Polylang < 3.8.
680 $languages = array_filter(
681 $languages,
682 function ( $lang ) use ( $args ) {
683 $keep_empty = empty( $args['hide_empty'] ) || $lang->get_tax_prop( 'language', 'count' );
684 $keep_default = empty( $args['hide_default'] ) || ! $lang->is_default;
685 return $keep_empty && $keep_default;
686 }
687 );
688
689 $languages = array_values( $languages ); // Re-index.
690 return $this->maybe_convert_list( $languages, (array) $args );
691 }
692
693 /**
694 * Tells if {@see WP_Syntex\Polylang\Model\Languages::get_list()} can be used.
695 *
696 * @since 3.4
697 * @since 3.7 Moved from `PLL_Model::are_languages_ready()` to `WP_Syntex\Polylang\Model\Languages::are_ready()`.
698 *
699 * @return bool
700 */
701 public function are_ready(): bool {
702 return $this->languages_ready;
703 }
704
705 /**
706 * Sets the internal property `$languages_ready` to `true`, telling that {@see WP_Syntex\Polylang\Model\Languages::get_list()} can be used.
707 *
708 * @since 3.4
709 * @since 3.7 Moved from `PLL_Model::set_languages_ready()` to `WP_Syntex\Polylang\Model\Languages::set_ready()`.
710 *
711 * @return void
712 */
713 public function set_ready(): void {
714 $this->languages_ready = true;
715 }
716
717 /**
718 * Returns the default language.
719 *
720 * @since 3.4
721 * @since 3.7 Moved from `PLL_Model::get_default_language()` to `WP_Syntex\Polylang\Model\Languages::get_default()`.
722 *
723 * @return PLL_Language|false Default language object, `false` if no language found.
724 */
725 public function get_default() {
726 if ( ! empty( $this->options['default_lang'] ) ) {
727 return $this->get( $this->options['default_lang'] );
728 }
729
730 // It happens that there the default language is lost. We require one, so let's select one arbitrarily.
731 $terms = $this->get_terms();
732 if ( empty( $terms ) ) {
733 return false;
734 }
735
736 $default_term = reset( $terms );
737 $this->update_default( $default_term->slug );
738
739 return $this->get( $this->options['default_lang'] );
740 }
741
742 /**
743 * Updates the default language.
744 * Takes care to update default category, nav menu locations, and flushes cache and rewrite rules.
745 *
746 * @since 1.8
747 * @since 3.7 Moved from `PLL_Admin_Model::update_default_lang()` to `WP_Syntex\Polylang\Model\Languages::update_default()`.
748 * Returns a `WP_Error` object.
749 *
750 * @param string $slug New language slug.
751 * @return WP_Error A `WP_Error` object containing possible errors during slug validation/sanitization.
752 */
753 public function update_default( $slug ): WP_Error {
754 $prev_default_lang = $this->options->get( 'default_lang' );
755
756 if ( $prev_default_lang === $slug ) {
757 return new WP_Error();
758 }
759
760 $errors = $this->options->set( 'default_lang', $slug );
761
762 if ( $errors->has_errors() ) {
763 return $errors;
764 }
765
766 // The nav menus stored in theme locations should be in the default language.
767 $theme = get_stylesheet();
768 if ( ! empty( $this->options['nav_menus'][ $theme ] ) ) {
769 $menus = array();
770
771 foreach ( $this->options['nav_menus'][ $theme ] as $key => $loc ) {
772 $menus[ $key ] = empty( $loc[ $slug ] ) ? 0 : $loc[ $slug ];
773 }
774 set_theme_mod( 'nav_menu_locations', $menus );
775 }
776
777 /**
778 * Fires when a default language is updated.
779 *
780 * @since 3.1
781 * @since 3.7 The previous default language's slug is passed as 2nd param.
782 * The default language is updated before this hook is fired.
783 *
784 * @param string $slug New default language's slug.
785 * @param string $prev_default_lang Previous default language's slug.
786 */
787 do_action( 'pll_update_default_lang', $slug, $prev_default_lang );
788
789 // Update options.
790
791 $this->clean_cache();
792 flush_rewrite_rules();
793
794 return new WP_Error();
795 }
796
797 /**
798 * Maybe adds the missing language terms for 3rd party language taxonomies.
799 *
800 * @since 3.4
801 * @since 3.7 Moved from `PLL_Model::maybe_create_language_terms()` to `WP_Syntex\Polylang\Model\Languages::maybe_create_terms()`.
802 *
803 * @return void
804 */
805 public function maybe_create_terms(): void {
806 $registered_taxonomies = array_diff(
807 $this->translatable_objects->get_taxonomy_names( array( 'language' ) ),
808 // Exclude the post and term language taxonomies from the list.
809 array(
810 $this->translatable_objects->get( 'post' )->get_tax_language(),
811 $this->translatable_objects->get( 'term' )->get_tax_language(),
812 )
813 );
814
815 if ( empty( $registered_taxonomies ) ) {
816 // No 3rd party language taxonomies.
817 return;
818 }
819
820 // We have at least one 3rd party language taxonomy.
821 $known_taxonomies = get_option( 'pll_language_taxonomies', array() );
822 $known_taxonomies = is_array( $known_taxonomies ) ? $known_taxonomies : array();
823 $new_taxonomies = array_diff( $registered_taxonomies, $known_taxonomies );
824
825 if ( empty( $new_taxonomies ) ) {
826 // No new 3rd party language taxonomies.
827 return;
828 }
829
830 // We have at least one unknown 3rd party language taxonomy.
831 foreach ( $this->get_list() as $language ) {
832 $this->update_secondary_language_terms( $language->slug, $language->name, $language, $new_taxonomies );
833 }
834
835 // Clear the cache, so the new `term_id` and `term_taxonomy_id` appear in the languages list.
836 $this->clean_cache();
837
838 // Keep the previous values, so this is triggered only once per taxonomy.
839 update_option( 'pll_language_taxonomies', array_merge( $known_taxonomies, $new_taxonomies ) );
840 }
841
842 /**
843 * Cleans language cache.
844 *
845 * @since 3.7
846 *
847 * @return void
848 */
849 public function clean_cache(): void {
850 delete_transient( self::TRANSIENT_NAME );
851 $this->clean_local_cache();
852 }
853
854 /**
855 * Cleans local language cache.
856 *
857 * @since 3.8
858 *
859 * @return void
860 */
861 public function clean_local_cache(): void {
862 $this->cache->clean();
863 }
864
865 /**
866 * Deletes the transient from the options table since WordPress does not do it when using object cache.
867 *
868 * @since 3.8
869 *
870 * @return void
871 */
872 public function delete_transient_from_options_table(): void {
873 if ( wp_using_ext_object_cache() || wp_installing() ) {
874 delete_option( '_transient_' . self::TRANSIENT_NAME );
875 }
876 }
877
878 /**
879 * Applies arguments that change the type of the elements of the given list of languages.
880 *
881 * @since 3.8
882 *
883 * @param PLL_Language[] $languages The list of language objects.
884 * @param array $args {
885 * @type string $fields Optional. Returns only that field if set; {@see PLL_Language} for a list of fields.
886 * }
887 * @return array List of `PLL_Language` objects or `PLL_Language` object properties.
888 */
889 public function maybe_convert_list( array $languages, array $args ): array {
890 if ( ! empty( $args['fields'] ) ) {
891 return wp_list_pluck( $languages, $args['fields'] );
892 }
893 return $languages;
894 }
895
896 /**
897 * Registers languages proxies.
898 *
899 * @since 3.8
900 *
901 * @param Languages_Proxy_Interface $proxy Proxy instance.
902 * @param string $mode Optional. Tell how the proxy must be applied. Possible values are:
903 * - `callable`: the proxy must be called manually with `filter()`.
904 * - `automatic`: the proxy is meant to be always called, automatically.
905 * Default is `callable`.
906 * @return self
907 *
908 * @phpstan-param 'callable'|'automatic' $mode
909 */
910 public function register_proxy( Languages_Proxy_Interface $proxy, string $mode = 'callable' ): self {
911 if ( 'automatic' === $mode ) {
912 $this->automatic_proxies[ $proxy->key() ] = $proxy;
913 } else {
914 $this->proxies[ $proxy->key() ] = $proxy;
915 }
916 return $this;
917 }
918
919 /**
920 * Stacks a proxy that will filter the list of languages.
921 *
922 * @since 3.8
923 *
924 * @param string $key Proxy's key.
925 * @return Languages_Proxies
926 */
927 public function filter( string $key ): Languages_Proxies {
928 return new Languages_Proxies( $this, $this->proxies, $key );
929 }
930
931 /**
932 * Checks if a given value matches the locale format.
933 *
934 * @since 3.8.5
935 *
936 * @param string $value The value to check.
937 * @return bool
938 */
939 public static function is_locale( $value ): bool {
940 return is_string( $value ) && (bool) preg_match( '#' . self::LOCALE_PATTERN . '#', $value );
941 }
942
943 /**
944 * Builds the language metas into an array and serializes it, to be stored in the term description.
945 *
946 * @since 3.4
947 * @since 3.7 Moved from `PLL_Admin_Model::build_language_metas()` to `WP_Syntex\Polylang\Model\Languages::build_metas()`.
948 *
949 * @param array $args {
950 * Arguments used to build the language metas.
951 *
952 * @type string $name Language name (used only for display).
953 * @type string $slug Language code (ideally 2-letters ISO 639-1 language code).
954 * @type string $locale WordPress locale. If something wrong is used for the locale, the .mo files will not
955 * be loaded...
956 * @type bool $rtl True if rtl language, false otherwise.
957 * @type int $term_group Language order when displayed.
958 * @type int $lang_id Optional, ID of the language to modify. An empty value means the language is being
959 * created.
960 * @type string $flag Optional, country code, {@see src/settings/flags.php}.
961 * }
962 * @return string The serialized description array updated.
963 *
964 * @phpstan-param array{
965 * name: non-empty-string,
966 * slug: non-empty-string,
967 * locale: non-empty-string,
968 * rtl: bool,
969 * term_group: int|numeric-string,
970 * lang_id?: int|numeric-string,
971 * flag?: non-empty-string
972 * } $args
973 */
974 protected function build_metas( array $args ): string {
975 if ( ! empty( $args['lang_id'] ) ) {
976 $language_term = get_term( (int) $args['lang_id'] );
977
978 if ( $language_term instanceof WP_Term ) {
979 $old_data = maybe_unserialize( $language_term->description );
980 }
981 }
982
983 if ( empty( $old_data ) || ! is_array( $old_data ) ) {
984 $old_data = array();
985 }
986
987 $new_data = array(
988 'locale' => $args['locale'],
989 'rtl' => ! empty( $args['rtl'] ),
990 'flag_code' => empty( $args['flag'] ) ? '' : $args['flag'],
991 );
992
993 /**
994 * Allow to add data to store for a language.
995 * `$locale`, `$rtl`, and `$flag_code` cannot be overwritten.
996 *
997 * @since 3.4
998 *
999 * @param mixed[] $add_data Data to add.
1000 * @param mixed[] $args {
1001 * Arguments used to create the language.
1002 *
1003 * @type string $name Language name (used only for display).
1004 * @type string $slug Language code (ideally 2-letters ISO 639-1 language code).
1005 * @type string $locale WordPress locale. If something wrong is used for the locale, the .mo files will
1006 * not be loaded...
1007 * @type bool $rtl True if rtl language, false otherwise.
1008 * @type int $term_group Language order when displayed.
1009 * @type int $lang_id Optional, ID of the language to modify. An empty value means the language is
1010 * being created.
1011 * @type string $flag Optional, country code, {@see src/settings/flags.php}.
1012 * }
1013 * @param mixed[] $new_data New data.
1014 * @param mixed[] $old_data {
1015 * Original data. Contains at least the following:
1016 *
1017 * @type string $locale WordPress locale.
1018 * @type bool $rtl True if rtl language, false otherwise.
1019 * @type string $flag_code Country code.
1020 * }
1021 */
1022 $add_data = apply_filters( 'pll_language_metas', array(), $args, $new_data, $old_data );
1023 // Don't allow to overwrite `$locale`, `$rtl`, and `$flag_code`.
1024 $new_data = array_merge( $old_data, $add_data, $new_data );
1025
1026 /** @var non-empty-string $serialized maybe_serialize() cannot return anything else than a string when fed by an array. */
1027 $serialized = maybe_serialize( $new_data );
1028 return $serialized;
1029 }
1030
1031 /**
1032 * Validates data entered when creating or updating a language.
1033 *
1034 * @since 0.4
1035 * @since 3.7 Moved from `PLL_Admin_Model::validate_lang()` to `WP_Syntex\Polylang\Model\Languages::validate_lang()`.
1036 *
1037 * @param array $args Parameters of {@see WP_Syntex\Polylang\Model\Languages::add() or @see WP_Syntex\Polylang\Model\Languages::update()}.
1038 * @param PLL_Language|null $lang Optional the language currently updated, the language is created if not set.
1039 * @return WP_Error
1040 *
1041 * @phpstan-param array{
1042 * locale?: string,
1043 * slug?: string,
1044 * name?: string,
1045 * flag?: string
1046 * } $args
1047 */
1048 protected function validate_lang( $args, ?PLL_Language $lang = null ): WP_Error {
1049 $errors = new WP_Error();
1050
1051 // Validate locale with the same pattern as WP 4.3. See #28303.
1052 if ( empty( $args['locale'] ) || ! self::is_locale( $args['locale'] ) ) {
1053 $errors->add( 'pll_invalid_locale', __( 'Enter a valid WordPress locale', 'polylang' ) );
1054 }
1055
1056 // Validate slug characters.
1057 if ( empty( $args['slug'] ) || ! preg_match( '#' . self::SLUG_PATTERN . '#', $args['slug'] ) ) {
1058 $errors->add( 'pll_invalid_slug', __( 'The language code contains invalid characters', 'polylang' ) );
1059 }
1060
1061 // Validate slug is unique.
1062 foreach ( $this->get_list() as $language ) {
1063 if ( ! empty( $args['slug'] ) && $language->slug === $args['slug'] && ( null === $lang || $lang->term_id !== $language->term_id ) ) {
1064 $errors->add( 'pll_non_unique_slug', __( 'The language code must be unique', 'polylang' ) );
1065 }
1066 }
1067
1068 // Validate name.
1069 // No need to sanitize it as `wp_insert_term()` will do it for us.
1070 if ( empty( $args['name'] ) ) {
1071 $errors->add( 'pll_invalid_name', __( 'The language must have a name', 'polylang' ) );
1072 }
1073
1074 // Validate flag.
1075 if ( ! empty( $args['flag'] ) && ! is_readable( POLYLANG_DIR . '/flags/' . $args['flag'] . '.png' ) ) {
1076 $flag = PLL_Language::get_flag_information( $args['flag'] );
1077
1078 if ( ! empty( $flag['url'] ) ) {
1079 $response = function_exists( 'vip_safe_wp_remote_get' ) ? vip_safe_wp_remote_get( sanitize_url( $flag['url'] ) ) : wp_remote_get( sanitize_url( $flag['url'] ) );
1080 }
1081
1082 if ( empty( $response ) || is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
1083 $errors->add( 'pll_invalid_flag', __( 'The flag does not exist', 'polylang' ) );
1084 }
1085 }
1086
1087 return $errors;
1088 }
1089
1090 /**
1091 * Updates the translations when a language slug has been modified in settings or deletes them when a language is removed.
1092 *
1093 * @since 0.5
1094 * @since 3.7 Moved from `PLL_Admin_Model::update_translations()` to `WP_Syntex\Polylang\Model\Languages::update_translations()`.
1095 * Visibility changed from public to protected.
1096 *
1097 * @global $wpdb wpdb global instance.
1098 *
1099 * @param string $old_slug The old language slug.
1100 * @param string $new_slug Optional, the new language slug, if not set it means that the language has been deleted.
1101 * @return WP_Error
1102 *
1103 * @phpstan-param non-empty-string $old_slug
1104 */
1105 protected function update_translations( $old_slug, $new_slug = '' ): WP_Error {
1106 global $wpdb;
1107
1108 $term_ids = array();
1109 $dr = array();
1110 $dt = array();
1111 $ut = array();
1112 $errors = new WP_Error();
1113
1114 $taxonomies = $this->translatable_objects->get_taxonomy_names( array( 'translations' ) );
1115 $terms = get_terms( array( 'taxonomy' => $taxonomies ) );
1116
1117 if ( is_array( $terms ) ) {
1118 foreach ( $terms as $term ) {
1119 $term_ids[ $term->taxonomy ][] = $term->term_id;
1120 $tr = maybe_unserialize( $term->description );
1121 $tr = is_array( $tr ) ? $tr : array();
1122
1123 /**
1124 * Filters the unserialized translation group description before it is
1125 * updated when a language is deleted or a language slug is changed.
1126 *
1127 * @since 3.2
1128 *
1129 * @param (int|string[])[] $tr {
1130 * List of translations with lang codes as array keys and IDs as array values.
1131 * Also in this array:
1132 *
1133 * @type string[] $sync List of synchronized translations with lang codes as array keys and array values.
1134 * }
1135 * @param string $old_slug The old language slug.
1136 * @param string $new_slug The new language slug.
1137 * @param WP_Term $term The term containing the post or term translation group.
1138 */
1139 $tr = apply_filters( 'update_translation_group', $tr, $old_slug, $new_slug, $term );
1140
1141 if ( ! empty( $tr[ $old_slug ] ) ) {
1142 if ( $new_slug ) {
1143 $tr[ $new_slug ] = $tr[ $old_slug ]; // Suppress this for delete.
1144 } else {
1145 $dr['id'][] = (int) $tr[ $old_slug ];
1146 $dr['tt'][] = (int) $term->term_taxonomy_id;
1147 }
1148 unset( $tr[ $old_slug ] );
1149
1150 if ( empty( $tr ) || 1 == count( $tr ) ) {
1151 $dt['t'][] = (int) $term->term_id;
1152 $dt['tt'][] = (int) $term->term_taxonomy_id;
1153 } else {
1154 $ut['case'][] = array( $term->term_id, maybe_serialize( $tr ) );
1155 $ut['in'][] = (int) $term->term_id;
1156 }
1157 }
1158 }
1159 }
1160
1161 // Delete relationships.
1162 if ( ! empty( $dr ) ) {
1163 $result = $wpdb->query(
1164 $wpdb->prepare(
1165 sprintf(
1166 "DELETE FROM {$wpdb->term_relationships} WHERE object_id IN (%s) AND term_taxonomy_id IN (%s)",
1167 implode( ',', array_fill( 0, count( $dr['id'] ), '%d' ) ),
1168 implode( ',', array_fill( 0, count( $dr['tt'] ), '%d' ) )
1169 ),
1170 array_merge( $dr['id'], $dr['tt'] )
1171 )
1172 );
1173 if ( false === $result ) {
1174 $errors->add( 'pll_delete_relationships', __( 'Could not delete relationships.', 'polylang' ) );
1175 }
1176 }
1177
1178 // Delete terms.
1179 if ( ! empty( $dt ) ) {
1180 $result = $wpdb->query(
1181 $wpdb->prepare(
1182 sprintf(
1183 "DELETE FROM {$wpdb->terms} WHERE term_id IN (%s)",
1184 implode( ',', array_fill( 0, count( $dt['t'] ), '%d' ) )
1185 ),
1186 $dt['t']
1187 )
1188 );
1189 if ( false === $result ) {
1190 $errors->add( 'pll_delete_terms', __( 'Could not delete translation groups.', 'polylang' ) );
1191 }
1192
1193 $result = $wpdb->query(
1194 $wpdb->prepare(
1195 sprintf(
1196 "DELETE FROM {$wpdb->term_taxonomy} WHERE term_taxonomy_id IN (%s)",
1197 implode( ',', array_fill( 0, count( $dt['tt'] ), '%d' ) )
1198 ),
1199 $dt['tt']
1200 )
1201 );
1202 if ( false === $result ) {
1203 $errors->add( 'pll_delete_term_taxonomy', __( 'Could not delete translation groups.', 'polylang' ) );
1204 }
1205 }
1206
1207 // Update terms.
1208 if ( ! empty( $ut ) ) {
1209 $result = $wpdb->query(
1210 $wpdb->prepare(
1211 sprintf(
1212 "UPDATE {$wpdb->term_taxonomy} SET description = ( CASE term_id %s END ) WHERE term_id IN (%s)",
1213 implode( ' ', array_fill( 0, count( $ut['case'] ), 'WHEN %d THEN %s' ) ),
1214 implode( ',', array_fill( 0, count( $ut['in'] ), '%d' ) )
1215 ),
1216 array_merge( array_merge( ...$ut['case'] ), $ut['in'] )
1217 )
1218 );
1219 if ( false === $result ) {
1220 $errors->add( 'pll_update_term_taxonomy', __( 'Could not update translation groups.', 'polylang' ) );
1221 }
1222 }
1223
1224 if ( ! empty( $term_ids ) ) {
1225 foreach ( $term_ids as $taxonomy => $ids ) {
1226 clean_term_cache( $ids, $taxonomy );
1227 }
1228 }
1229
1230 return $errors;
1231 }
1232
1233 /**
1234 * Updates or adds new terms for a secondary language taxonomy (aka not 'language').
1235 *
1236 * @since 3.4
1237 * @since 3.7 Moved from `PLL_Model::update_secondary_language_terms()` to `WP_Syntex\Polylang\Model\Languages::update_secondary_language_terms()`.
1238 *
1239 * @param string $slug Language term slug (with or without the `pll_` prefix).
1240 * @param string $name Language name (label).
1241 * @param PLL_Language|null $language Optional. A language object. Required to update the existing terms.
1242 * @param string[] $taxonomies Optional. List of language taxonomies to deal with. An empty value means
1243 * all of them. Defaults to all taxonomies.
1244 * @return WP_Error
1245 *
1246 * @phpstan-param non-empty-string $slug
1247 * @phpstan-param non-empty-string $name
1248 * @phpstan-param array<non-empty-string> $taxonomies
1249 */
1250 protected function update_secondary_language_terms( $slug, $name, ?PLL_Language $language = null, array $taxonomies = array() ): WP_Error {
1251 $slug = 0 === strpos( $slug, 'pll_' ) ? $slug : "pll_$slug";
1252 $errors = new WP_Error();
1253
1254 foreach ( $this->translatable_objects->get_secondary_translatable_objects() as $object ) {
1255 if ( ! empty( $taxonomies ) && ! in_array( $object->get_tax_language(), $taxonomies, true ) ) {
1256 // Not in the list.
1257 continue;
1258 }
1259
1260 if ( ! empty( $language ) ) {
1261 $term_id = $language->get_tax_prop( $object->get_tax_language(), 'term_id' );
1262 } else {
1263 $term_id = 0;
1264 }
1265
1266 if ( empty( $term_id ) ) {
1267 // Attempt to repair the language if a term has been deleted by a database cleaning tool.
1268 $result = wp_insert_term( $name, $object->get_tax_language(), array( 'slug' => $slug ) );
1269 if ( is_wp_error( $result ) ) {
1270 $errors->add(
1271 'pll_add_secondary_language_terms',
1272 /* translators: %s is a taxonomy name */
1273 sprintf( __( 'Could not add secondary language term for taxonomy %s.', 'polylang' ), $object->get_tax_language() )
1274 );
1275 }
1276 continue;
1277 }
1278
1279 /** @var PLL_Language $language */
1280 if ( "pll_{$language->slug}" !== $slug || $language->name !== $name ) {
1281 // Something has changed.
1282 $result = wp_update_term( $term_id, $object->get_tax_language(), array( 'slug' => $slug, 'name' => $name ) );
1283 if ( is_wp_error( $result ) ) {
1284 $errors->add(
1285 'pll_update_secondary_language_terms',
1286 /* translators: %s is a taxonomy name */
1287 sprintf( __( 'Could not update secondary language term for taxonomy %s.', 'polylang' ), $object->get_tax_language() )
1288 );
1289 }
1290 }
1291 }
1292
1293 return $errors;
1294 }
1295
1296 /**
1297 * Returns the list of available languages, based on the language taxonomy terms.
1298 * Stores the list in a db transient and in a `PLL_Cache` object.
1299 *
1300 * @since 3.4
1301 * @since 3.7 Moved from `PLL_Model::get_languages_from_taxonomies()` to `WP_Syntex\Polylang\Model\Languages::get_from_taxonomies()`.
1302 *
1303 * @return PLL_Language[] An array of `PLL_Language` objects, array keys are the type.
1304 *
1305 * @phpstan-return list<PLL_Language>
1306 */
1307 protected function get_from_taxonomies(): array {
1308 $terms_by_slug = array();
1309
1310 foreach ( $this->get_terms() as $term ) {
1311 // Except for language taxonomy term slugs, remove 'pll_' prefix from the other language taxonomy term slugs.
1312 $key = 'language' === $term->taxonomy ? $term->slug : substr( $term->slug, 4 );
1313 $terms_by_slug[ $key ][ $term->taxonomy ] = $term;
1314 }
1315
1316 /**
1317 * @var (
1318 * array{
1319 * string: array{
1320 * language: WP_Term,
1321 * }&array<non-empty-string, WP_Term>
1322 * }
1323 * ) $terms_by_slug
1324 */
1325 $languages = array_filter(
1326 array_map(
1327 array( new PLL_Language_Factory( $this->options ), 'get_from_terms' ),
1328 array_values( $terms_by_slug )
1329 )
1330 );
1331
1332 /**
1333 * Filters the list of languages *before* it is stored in the persistent cache.
1334 * /!\ This filter is fired *before* the $polylang object is available.
1335 *
1336 * @since 1.7.5
1337 * @since 3.4 Deprecated.
1338 * @deprecated
1339 *
1340 * @param PLL_Language[] $languages The list of language objects.
1341 * @param Language $model `Language` object.
1342 */
1343 $languages = apply_filters_deprecated( 'pll_languages_list', array( $languages, $this ), '3.4', 'pll_additional_language_data' );
1344
1345 if ( ! $this->are_ready() ) {
1346 // Do not cache an incomplete list.
1347 /** @var list<PLL_Language> $languages */
1348 return $languages;
1349 }
1350
1351 /*
1352 * Don't store directly objects as it badly break with some hosts ( GoDaddy ) due to race conditions when using object cache.
1353 * Thanks to captin411 for catching this!
1354 *
1355 * @see https://wordpress.org/support/topic/fatal-error-pll_model_languages_list?replies=8#post-6782255
1356 */
1357 $languages_data = array_map(
1358 function ( $language ) {
1359 return $language->to_array( 'db' );
1360 },
1361 $languages
1362 );
1363
1364 set_transient( self::TRANSIENT_NAME, $languages_data );
1365
1366 /** @var list<PLL_Language> $languages */
1367 return $languages;
1368 }
1369
1370 /**
1371 * Returns the list of existing language terms.
1372 * - Returns all terms, that are or not assigned to posts.
1373 * - Terms are ordered by `term_group` and `term_id` (see `WP_Syntex\Polylang\Model\Languages::filter_terms_orderby()`).
1374 *
1375 * @since 3.2.3
1376 * @since 3.7 Moved from `PLL_Model::get_language_terms()` to `WP_Syntex\Polylang\Model\Languages::get_terms()`.
1377 *
1378 * @return WP_Term[]
1379 */
1380 protected function get_terms(): array {
1381 $terms = get_terms(
1382 array(
1383 'taxonomy' => $this->translatable_objects->get_taxonomy_names( array( 'language' ) ),
1384 'hide_empty' => false,
1385 )
1386 );
1387
1388 if ( empty( $terms ) || is_wp_error( $terms ) ) {
1389 return array();
1390 }
1391
1392 $callback = static function ( $a, $b ) {
1393 if ( $a->taxonomy !== $b->taxonomy ) {
1394 // Sort terms by 'language' taxonomy first.
1395 return 'language' === $a->taxonomy ? -1 : 1;
1396 }
1397 if ( $a->term_group !== $b->term_group ) {
1398 // Then by term_group.
1399 return $a->term_group < $b->term_group ? -1 : 1;
1400 }
1401 // Then by term_id.
1402 return $a->term_id < $b->term_id ? -1 : 1;
1403 };
1404
1405 usort( $terms, $callback );
1406 return $terms;
1407 }
1408 }
1409