PluginProbe
Polylang / trunk
Polylang vtrunk
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 trunk, at src/Model/Languages.php

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